我正在阅读SQLite的常见问题 ,并在这段话中提到:
线程是邪恶的。 避免他们。
我不太了解“线程是邪恶的”这句话。 如果那是真的,那么有什么select呢?
我对线索的肤浅认识是:
注意:因为我不熟悉Windows上的线程,所以我希望讨论将限于Linux / Unix线程。
当人们说“线程是邪恶的”时,通常在“流程好”的语境中这样做。 线程隐含地共享所有的应用程序状态和句柄(并且线程本地人是选择性的)。 这意味着在访问共享数据时有很多机会忘记同步(或者甚至没有理解你需要同步!)。
进程具有独立的内存空间,并且它们之间的任何通信都是明确的。 而且,用于进程间通信的基元通常是这样的,你根本不需要同步(例如管道)。 如果你需要使用共享内存,你仍然可以直接共享状态,但在每个给定的实例中也是这样。 所以犯错误的机会就越少,代码的意图就越明确。
简单的回答我的理解方式
大多数线程模型使用“共享状态并发”,这意味着两个执行进程可以同时共享相同的内存。 如果一个线程不知道对方在做什么,它可以以另一个线程不期望的方式修改数据。 这会导致错误。
线程是“邪恶的”,因为你需要围绕n
线程同时在相同的内存上工作,以及与之相关的所有有趣的事情(死锁,赛车条件等)。
你可以阅读关于Clojure(不可变的数据结构)和Erlang(消息传递)的并发模型,以获得有关如何实现类似目标的替代想法。
什么使得线程变得“邪恶”是因为一旦你在你的程序中引入了多个执行流程,你就不能指望你的程序以确定性的方式工作。
也就是说:在给定相同的输入集合的情况下,单线程程序(在大多数情况下)总是执行相同的操作。
一个多线程程序,在给定相同的输入集合的情况下,每次运行时都可能做一些不同的事情,除非它被非常仔细地控制。 这是因为不同线程运行不同位代码的顺序是由操作系统的线程调度器和系统定时器共同决定的,这就给程序在运行时带来了很大的“随机性”。
结果是:调试一个多线程程序可能比调试单线程程序困难得多,因为如果你不知道你在做什么,可能会很容易导致竞争条件或死锁错误(似乎)随机出现一次或两次。 这个程序对你的QA部门看起来很好(因为他们没有一个月的时间来运行它),但是一旦在现场出现,你会听到客户说程序崩溃了,没有人能够重现崩溃。呃。
总而言之,线程并不是真正的“邪恶”,但是它们是强有力的,除非(a)你真的需要它们,并且(b)你知道你在做什么,否则不应该被使用。 如果你确实使用了它们,尽可能少地使用它们,尽可能使它们的行为变得愚蠢简单。 特别是在多线程的情况下,如果有什么可能出错,(迟早)会。
我会用另一种方式解释。 并不是说线程是邪恶的,而是在多线程的环境下(这种说法不那么吸引人),这是副作用 。
在这种情况下,副作用是影响多个线程共享的状态,无论是全局的还是共享的。 我最近写了一个Spring Batch的评论,其中一个代码片段是:
private static Map<Long, JobExecution> executionsById = TransactionAwareProxyFactory.createTransactionalMap(); private static long currentId = 0; public void saveJobExecution(JobExecution jobExecution) { Assert.isTrue(jobExecution.getId() == null); Long newId = currentId++; jobExecution.setId(newId); jobExecution.incrementVersion(); executionsById.put(newId, copy(jobExecution)); }
现在在这里少于10行的代码至少有三个严重的线程问题。 在这种情况下的一个副作用的例子是更新currentId静态变量。
函数式编程(Haskell,Scheme,Ocaml,Lisp等)倾向于支持“纯粹”的功能。 纯粹的功能是没有副作用的功能。 许多命令式语言(例如Java,C#)也鼓励使用不可变对象(一个不可变对象是一旦创建后状态就不能改变的对象)。
两者的原因(或至少是这两者的效果)基本相同:它们使得多线程代码更容易。 根据定义,纯函数是线程安全的。 根据定义,一个不可变对象是线程安全的。
优势过程是共享状态较少(一般情况下)。 在传统的UNIX C编程中,使用fork()来创建一个新的进程会导致共享进程状态,这被用作IPC(进程间通信)的手段,但是通常用exec()别的东西。
但是创建和销毁线程要便宜得多,而且占用的系统资源更少(事实上,操作本身可能没有线程概念,但仍然可以创建多线程程序)。 这些被称为绿色线程 。
你链接的文件似乎很好地解释自己。 你读过它吗?
请记住,线程可以引用编程语言结构(如在大多数过程或OOP语言中,您手动创建一个线程,并告诉它执行一个函数),也可以引用硬件结构(每个CPU内核一次执行一个线程)。
硬件级的线程显然是不可避免的,只是CPU的工作原理。 但是CPU不关心你的源代码是如何表达并发的。 例如,它不必由“beginthread”函数调用。 OS和CPU必须被告知应该执行哪个指令线程。
他的观点是,如果我们使用比C或Java更好的语言,并使用为并发而设计的编程模型,那么我们可以免费获得并发性。 如果我们使用了消息传递语言,或者没有副作用的函数式编译器,那么编译器就可以为我们并行化我们的代码。 它会工作。
线程不再比锤子或螺丝刀或任何其他工具更“邪恶” 他们只是需要技巧来利用。 解决办法不是避免它们; 这是教育自己,并提高你的技能。
对于任何需要长时间稳定和安全执行而没有失败或维护的应用程序来说,线程总是一个诱人的错误。 他们总是变得比他们的价值更麻烦。 他们产生了快速的结果和原型似乎正确执行,但几个星期或几个月后,你发现他们有严重的缺陷。
正如另一张海报所提到的,一旦你在你的程序中使用了一个单独的线程,你现在已经打开了一个非确定性的代码执行路径,在时间,内存共享和竞态条件下产生几乎无限的冲突。 解决这些问题的信心大多表现为学习了多线程编程原理但尚未解决的难题。
线程是邪恶的。 好的程序员避免在人类可能的地方。 在这里提供了分叉的替代方案,对于许多应用来说,这通常是一个很好的策略。 将代码分解成独立的执行流程(这种流程以某种形式的松散耦合的形式)的概念通常证明是支持它的平台上的优秀策略。 在单个程序中一起运行的线程不是解决方案。 通常是在您的设计中创建一个致命的建筑缺陷,只有通过重写整个程序才能真正弥补。
最近面向事件驱动的并发是一个很好的开发创新。 这些程序通常被证明在部署后具有很强的耐力。
我从来没见过一个不认为线程太好的年轻工程师。 我从来没有见过一个没有像瘟疫一样躲避他们的老工程师。
创建大量没有约束的线程确实是邪恶的。使用池化机制(threadpool)将缓解这个问题。
线程是“恶魔”的另一种方式是大多数框架代码不是为处理多个线程而设计的,所以您必须为这些数据结构管理自己的锁定机制。
线程是好的,但你必须考虑如何以及何时使用它们,并记住要衡量是否真的有性能优势。
线程有点像轻量级的过程。 把它看作是一个独立的应用程序执行路径。 线程在与应用程序相同的内存空间中运行,因此可以访问所有相同的资源,全局对象和全局变量。
关于他们的好处:你可以并行一个程序来提高性能。 一些例子,1)在图像编辑程序中,线程可以独立于GUI运行过滤器处理。 2)一些算法适合多线程。
他们有什么不好? 如果一个程序设计得不好,它们可能会导致死锁问题,在这两个线程正在彼此等待访问相同的资源。 其次,程序设计可以让我更复杂,因为这个。 而且,一些类库不支持线程。 例如c库函数“strtok”不是“线程安全的”。 换句话说,如果两个线程在同一时间使用它们,它们将会破坏其他的结果。 幸运的是,通常有线程安全的替代方案,例如增强库。
线程不是邪恶的,它们确实是非常有用的。
在Linux / Unix下,线程在过去得不到很好的支持,尽管我相信Linux现在有了Posix线程支持,而其他的unices支持现在通过库或本地线程。 即pthreads。
在Linux / Unix平台下线程的最常见替代方法是fork。 叉只是一个程序的副本,包括打开的文件句柄和全局变量。 fork()将子进程和进程标识返回给父进程。 在Linux / Unix下这是一种比较旧的方式,但仍然很好用。 线程使用的内存少于fork,启动速度更快。 而且,进程间通信比简单的线程更有效。
简单地说,您可以将线程看作当前进程中的另一个指令指针。 换句话说,它将另一个处理器的IP指向同一个可执行文件中的某些代码。 因此,代替有一个指令指针在代码中移动,同时有两个或多个IP执行来自同一个可执行文件和地址空间的指令。
记住可执行文件有自己的数据/堆栈等地址空间…所以,现在有两个或更多的指令同时执行,你可以想象当多个指令要读/写同一个内存地址时同一时间。
问题在于,线程在进程地址空间内运行,并且没有提供处理器的保护机制,即完整的进程 。 (在UNIX上分叉进程是标准做法,只是创建另一个进程。)
失控的线程可能会消耗CPU周期,咀嚼RAM,引起怀疑等等,唯一的办法就是告诉OS进程调度器强制终止线程,通过取消它的指令指针(即停止执行) 。 如果强行告诉CPU停止执行一系列指令,那么这些指令已经分配或正在操作的资源会发生什么情况? 他们是否处于一个稳定的状态? 他们是否被正确地释放了? 等等…
所以,是的,由于共享资源,线程比执行一个进程需要更多的思考和责任。
作为一个年长的工程师,我很同意德克萨斯奥术会的回答。
线程是非常邪恶的,因为它们造成难以解决的错误。 我花了几个月时间来解决零星的竞赛条件。 一个例子就是电车在路中间突然停下来一次,阻止交通,直到被拖走。 幸运的是我没有创建这个bug,但是我确实花了4个月的时间来解决它。
添加到这个线程是晚了一点,但我想提到一个非常有趣的线程替代:与协程序和事件循环的异步编程。 这得到越来越多的语言的支持,并没有像多线程那样的竞争条件问题。
在用于等待来自多个源的事件的情况下,它可以替代多线程,但是在多个CPU核上不需要并行执行计算的情况下,它可以取代多线程。