我正在阅读SQLite FAQ,并发现了这段话:
Threads are evil.避免它们。
我不太明白“线程是邪恶的”这句话。如果这是真的,那么替代方案是什么?
我对线程的肤浅理解是:
注意:由于我不熟悉Windows上的线程,我希望讨论仅限于Linux / Unix线程。
答案 0 :(得分:14)
当人们说“线程是邪恶的”时,通常会在“流程很好”的背景下这样做。线程隐式共享所有应用程序状态和句柄(并且线程本地是选择加入)。这意味着在访问共享数据时,有很多机会忘记同步(或者甚至不了解您需要同步!)。
进程具有单独的内存空间,它们之间的任何通信都是显式的。此外,用于进程间通信的原语通常使得您根本不需要同步(例如管道)。如果需要,您仍然可以使用共享内存直接共享状态,但在每个给定的实例中也是如此。因此,犯错的机会较少,而且代码的意图更明确。
答案 1 :(得分:12)
简单回答我理解的方式......
大多数线程模型使用“共享状态并发”,这意味着两个执行进程可以同时共享同一个内存。如果一个线程不知道对方在做什么,它可以以另一个线程不期望的方式修改数据。这会导致错误。
线程是“邪恶的”,因为你需要在n
个线程周围同时处理同一个内存,以及随之而来的所有有趣的东西(死锁,竞争条件等) )。
您可能会阅读有关Clojure(不可变数据结构)和Erlang(消息传递)并发模型的内容,以获取有关如何实现类似目的的替代方法。
答案 2 :(得分:11)
线程“邪恶”的原因在于,一旦您在程序中引入了多个执行流,您就不能再指望您的程序以确定的方式运行。
也就是说:给定相同的输入集,单线程程序(在大多数情况下)总是会做同样的事情。
一个多线程程序,在给定相同的输入集的情况下,每次运行时都可以做一些不同的事情,除非它被非常小心地控制。这是因为不同线程运行不同代码位的顺序由OS的线程调度程序与系统计时器结合确定,这会在程序运行时引入大量“随机性”。
结果是:调试多线程程序比调试单线程程序要困难得多,因为如果你不知道自己在做什么,那么很容易就会遇到竞争条件或死锁只出现(看似)一个月一次或两次的错误。该程序看起来很适合你的QA部门(因为他们没有一个月的时间来运行它)但是一旦它出现在现场,你会听到客户说程序崩溃了,没有人可以重现崩溃。 .. bleah。
总而言之,线程并不是真正的“邪恶”,但它们是强大的juju并且不应该被使用,除非(a)你真的需要它们和(b)你知道你正在做什么。如果您确实使用它们,请尽可能少地使用它们,并尝试尽可能简单地使它们的行为变得简单。特别是对于多线程,如果出现任何问题,它(迟早会)。
答案 3 :(得分:7)
我会用另一种方式解释它。并不是线程是邪恶的,而是副作用在多线程环境中是邪恶的(这说起来不那么引人注目)。
此上下文中的副作用会影响由多个线程共享的状态,无论是全局还是仅共享。我最近写了一个review of 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())替换状态别的什么。
但是创建和销毁线程的成本要低得多,并且它们占用较少的系统资源(事实上,操作本身可能没有线程概念,但您仍然可以创建多线程程序)。这些被称为绿色线程。
答案 4 :(得分:4)
你联系的论文似乎很好地解释了自己。你看过了吗?
请记住,线程可以引用编程语言构造(如在大多数过程或OOP语言中,您手动创建线程,并告诉它执行函数),或者它们可以引用硬件构造(每个CPU核心一次执行一个线程。
硬件级线程显然是不可避免的,它只是CPU的工作方式。但CPU并不关心如何在源代码中表达并发性。例如,它不必通过“beginthread”函数调用。必须告诉操作系统和CPU应该执行哪些指令线程。
他的观点是,如果我们使用比C或Java更好的语言和一个为并发性设计的编程模型,我们可以基本上免费获得并发性。如果我们使用了消息传递语言或没有副作用的函数式语言,编译器就能够为我们并行化代码。它会起作用。
答案 5 :(得分:4)
线程不再比锤子或螺丝刀或任何其他工具“邪恶”;他们只需要技巧来利用。解决方案不是避免它们;这是教育自己和提高你的技能。
答案 6 :(得分:1)
对于任何需要长时间稳定安全执行且无故障或维护的应用程序,线程始终是一个诱人的错误。他们总是变得比他们的价值更麻烦。它们产生了快速的结果和原型,似乎表现正常,但经过几周或几个月的运行,你发现它们有严重的缺陷。
正如另一张海报所提到的,一旦你在程序中使用了一个单独的线程,你现在已经打开了一条非确定性的代码执行路径,它可以在时序,内存共享和竞争条件上产生几乎无限的冲突。大多数表达对解决这些问题的信心表达了那些已经学习了多线程编程原理但尚未遇到解决这些问题的困难的人。
线程是邪恶的。优秀的程序员尽可能地避免使用它们。这里提供了分叉的替代方案,对于许多应用来说,它通常是一个很好的策略。将代码分解为单独的执行进程的概念通常在支持它的平台上成为一种优秀的策略。在单个程序中一起运行的线程不是解决方案。通常在您的设计中创建一个致命的架构缺陷,只有通过重写整个程序才能真正解决。
最近向面向事件的并发性的转变是一项出色的开发创新。这些程序在部署后通常证明具有很强的耐久性。
我从未见过一位年轻的工程师,他认为线程并不好。我从未见过一位没有像瘟疫一样避开他们的老工程师。
答案 7 :(得分:1)
作为一名年长的工程师,我很同意德克萨斯奥术的答案。
线程非常邪恶,因为它们会导致极难解决的错误。我花了几个月的时间来解决零星的竞争条件。一个例子导致有轨电车在道路中间每月突然停止一次,并阻止交通直到被拖走。幸运的是我没有创建这个bug,但我确实花了4个月的全职时间来解决它......
添加到这个线程已经晚了一点,但我想提一个非常有趣的线程替代方案:使用协同例程和事件循环进行异步编程。这得到了越来越多语言的支持,并且没有多线程等竞争条件的问题。
在用于等待来自多个源的事件的情况下,它可以替换多线程,而不是在需要在多个CPU核心上并行执行计算的情况下。
答案 8 :(得分:0)
创建大量没有约束的线程确实是邪恶的......使用池化机制(threadpool)将缓解这个问题。
线程“邪恶”的另一种方式是大多数框架代码不是为处理多个线程而设计的,因此您必须为这些数据结构管理自己的锁定机制。
线程很好,但您必须考虑如何以及何时使用它们并记住测量是否确实存在性能优势。
答案 9 :(得分:0)
线程有点像轻量级过程。将其视为应用程序中的独立执行路径。该线程在与应用程序相同的内存空间中运行,因此可以访问所有相同的资源,全局对象和全局变量。
关于它们的好处:您可以并行化程序以提高性能。一些示例,1)在图像编辑程序中,线程可以独立于GUI运行过滤处理。 2)一些算法适用于多个线程。
他们有什么不好的?如果程序设计不当,它们可能会导致死锁问题,即两个线程都在相互等待以访问相同的资源。其次,由于这个原因,程序设计可能会更加复杂。此外,某些类库不支持线程。例如c库函数“strtok”不是“线程安全的”。换句话说,如果两个线程同时使用它们,它们会破坏彼此的结果。幸运的是,通常有线程安全的替代品......例如提升图书馆。
线程不是邪恶的,它们确实非常有用。
在Linux / Unix下,过去并没有很好地支持线程,虽然我认为Linux现在有Posix线程支持,而其他unices现在通过库或本机支持线程。即pthreads。
Linux / Unix平台下最常见的线程替代方法是fork。 Fork只是程序的一个副本,包括它的打开文件句柄和全局变量。 fork()向子进程返回0,向父进程返回进程id。这是一种在Linux / Unix下运行的旧方法,但仍然使用得很好。线程使用的内存少于fork,并且启动速度更快。此外,进程间通信比简单线程更多的工作。
答案 10 :(得分:0)
从简单的意义上讲,您可以将线程视为当前进程中的另一个指令指针。换句话说,它将另一个处理器的IP指向同一可执行文件中的某些代码。因此,不是让一个指令指针移过代码,而是有两个或多个IP同时从同一个可执行文件和地址空间执行指令。
记住可执行文件有自己的数据/堆栈等地址空间......所以现在两个或多个指令同时执行,你可以想象当多个指令想要读/写时会发生什么内存地址同时。
问题在于线程在进程地址空间内运行,并且没有为处理器提供完整的进程所提供的保护机制。 (在UNIX上分叉进程是标准做法,只是创建另一个进程。)
失控线程可能会消耗CPU周期,咀嚼RAM,导致异常等等。并且阻止它们的唯一方法是告诉OS进程调度程序通过使其指令指针无效来强制终止线程(即停止执行)。如果您强行告诉CPU停止执行一系列指令,那些指令已经分配或正在操作的资源会发生什么?他们是否处于稳定状态?它们是否被正确释放?等...
所以,是的,由于共享资源,线程需要比执行流程更多的思考和责任。