【问题标题】:Why might threads be considered "evil"?为什么线程可能被认为是“邪恶的”?
【发布时间】:2009-07-28 01:49:00
【问题描述】:

我在阅读SQLite FAQ,偶然发现了这段话:

Threads are evil. 避开它们。

我不太明白“线程是邪恶的”这句话。如果是真的,那还有什么办法呢?

我对线程的肤浅理解是:

  • 线程使并发发生。否则,CPU 马力将被浪费,等待(例如)缓慢的 I/O。
  • 但不好的是,您必须同步您的逻辑以避免争用,并且您必须保护共享资源。

注意:由于我不熟悉 Windows 上的线程,我希望讨论仅限于 Linux/Unix 线程。

【问题讨论】:

  • 也许您应该阅读 pdf 文件? ;)
  • 投票以“不是一个真正的问题”结束,因为问题链接到答案。
  • 原来的问题,在被编辑之前,没有“我对线程的肤浅理解是:”这句话,而是像:“我对文章的肤浅理解是:“

标签: multithreading unix thread-safety


【解决方案1】:

当人们说“线程是邪恶的”时,通常是在说“进程是好的”的上下文中这样做的。线程隐式共享所有应用程序状态和句柄(并且线程本地是可选的)。这意味着在访问共享数据时有很多机会忘记同步(甚至不知道您需要同步!)。

进程具有独立的内存空间,它们之间的任何通信都是显式的。此外,用于进程间通信的原语通常根本不需要同步(例如管道)。如果需要,您仍然可以使用共享内存直接共享状态,但这在每个给定实例中也是明确的。所以出错的机会更少,代码的意图更明确。

【讨论】:

  • 这篇论文并没有真正讨论作为替代方案的流程
  • +1 - 有趣的一点是 Erlang 的线程,因为该语言是纯函数式的并且禁止副作用,所以像进程一样工作并通过消息传递共享数据。
【解决方案2】:

按照我理解的方式简单回答...

大多数线程模型使用“共享状态并发”,这意味着两个执行进程可以同时共享相同的内存。如果一个线程不知道另一个线程在做什么,它可以以另一个线程不期望的方式修改数据。这会导致错误。

线程是“邪恶的”,因为您需要围绕n 线程同时在同一内存上工作,以及随之而来的所有有趣的事情(死锁、赛车条件等)。

您可以阅读有关 Clojure(不可变数据结构)和 Erlang(消息传递)并发模型的信息,以了解有关如何实现类似目的的替代想法。

【讨论】:

    【解决方案3】:

    使线程“邪恶”的原因在于,一旦您在程序中引入了多个执行流,您就不能再指望您的程序以一种确定的方式运行。

    也就是说:给定相同的输入集,单线程程序将(在大多数情况下)总是做同样的事情。

    一个多线程程序,给定相同的输入集,每次运行时可能会做不同的事情,除非它受到非常仔细的控制。这是因为不同线程运行不同位代码的顺序是由操作系统的线程调度程序结合系统计时器确定的,这会在程序运行时引入大量“随机性”。

    结果是:调试多线程程序可能比调试单线程程序困难得多,因为如果您不知道自己在做什么,很容易导致竞争条件或死锁每月仅随机出现(看似)一次或两次的错误。该程序对您的 QA 部门来说看起来不错(因为他们没有一个月的时间来运行它),但是一旦它投入使用,您就会从客户那里听到程序崩溃的消息,并且没有人可以重现崩溃。 .. 呜呜。

    总而言之,线程并不是真正的“邪恶”,但它们是强大的 juju,不应该使用,除非 (a) 你真的需要它们,并且 (b) 你知道你自己在做什么。如果您确实使用它们,请尽可能少地使用它们,并尝试使它们的行为尽可能简单。尤其是多线程,如果有任何问题,它(迟早)会出现。

    【讨论】:

      【解决方案4】:

      我会用另一种方式解释它。并不是说 线程 是邪恶的,而是 副作用 在多线程上下文中是邪恶的(说起来不那么吸引人)。

      这种情况下的副作用是影响多个线程共享的状态,无论是全局的还是共享的。我最近写了一个review of Spring Batch,其中一个sn-ps使用的代码是:

      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())别的东西。

      但是线程的创建和销毁成本很多,并且它们占用的系统资源更少(实际上,操作本身可能没有线程的概念,但您仍然可以创建多线程程序)。这些被称为绿色线程

      【讨论】:

      • 在 Linux 上,线程并不比运行相同可执行文件并共享大部分数据 (COW) 的不同进程便宜多少。 Linux 并没有真正区分线程和进程,只是不同线程的 PID 相同。
      【解决方案5】:

      您链接到的论文似乎很好地解释了自己。你读了吗?

      请记住,线程可以引用编程语言构造(在大多数过程或 OOP 语言中,您手动创建线程,并告诉它执行函数),或者它们可以引用硬件构造(每个 CPU 内核一次执行一个线程)。

      硬件级线程显然是无法避免的,CPU就是这样工作的。但是 CPU 并不关心并发在你的源代码中是如何表达的。例如,它不必通过“beginthread”函数调用。只需告诉操作系统和 CPU 应该执行哪些指令线程。

      他的观点是,如果我们使用比 C 或 Java 更好的语言以及为并发设计的编程模型,我们基本上可以免费获得并发。如果我们使用消息传递语言,或者没有副作用的函数式语言,编译器将能够为我们并行化我们的代码。它会起作用的。

      【讨论】:

        【解决方案6】:

        螺纹并不比锤子、螺丝刀或任何其他工具更“邪恶”;他们只需要使用技能。解决方案不是避免它们;这是为了教育你自己并提高你的技能。

        【讨论】:

        • 同意。在我看来,作者根本不知道如何正确使用线程。无知不会抹黑线程的价值。此外,我没有在论文中看到明确的替代方案。
        【解决方案7】:

        在没有约束的情况下创建大量线程确实是邪恶的。使用池机制(线程池)将缓解这个问题。

        线程“邪恶”的另一种方式是大多数框架代码并非旨在处理多个线程,因此您必须为这些数据结构管理自己的锁定机制。

        线程很好,但您必须考虑如何以及何时使用它们,并记住衡量是否真的有性能优势。

        【讨论】:

          【解决方案8】:

          线程有点像轻量级进程。将其视为应用程序中的独立执行路径。线程与应用程序在相同的内存空间中运行,因此可以访问所有相同的资源、全局对象和全局变量。

          它们的好处是:您可以并行化程序以提高性能。一些示例, 1) 在图像编辑程序中,线程可以独立于 GUI 运行过滤处理。 2) 一些算法适合多线程。

          他们有什么不好?如果程序设计不当,它们可能会导致死锁问题,即两个线程都在等待对方访问相同的资源。其次,程序设计可以因此变得更复杂。此外,一些类库不支持线程。例如c 库函数“strtok”不是“线程安全的”。换句话说,如果两个线程同时使用它,它们会破坏彼此的结果。幸运的是,通常有线程安全的替代方案......例如提升库。

          线程并不邪恶,它们确实非常有用。

          在 Linux/Unix 下,线程在过去没有得到很好的支持,尽管我相信 Linux 现在支持 Posix 线程,而其他 unice 现在通过库或本机支持线程。即 pthreads。

          Linux/Unix 平台下最常见的线程替代方案是 fork。 Fork 只是一个程序的副本,包括它的打开文件句柄和全局变量。 fork() 向子进程返回 0,向父进程返回进程 ID。这是一种在 Linux/Unix 下做事的旧方式,但仍然很好用。线程使用的内存比 fork 少,而且启动速度更快。此外,进程间通信比简单的线程需要更多的工作。

          【讨论】:

            【解决方案9】:

            在简单的意义上你可以把一个线程看作是当前进程中的另一个指令指针。换句话说,它将另一个处理器的 IP 指向同一可执行文件中的某些代码。因此,不是让一个指令指针在代码中移动,而是两个或多个 IP 同时执行来自同一可执行文件和地址空间的指令

            请记住,可执行文件有自己的地址空间和数据/堆栈等...所以现在两条或多条指令正在同时执行,您可以想象当多个指令想要读取/写入相同的指令时会发生什么内存地址。

            问题在于线程在进程地址空间内运行,并且没有像完全成熟的进程那样受到处理器的保护机制。 (在 UNIX 上分叉一个进程是标准做法,只是创建另一个进程。)

            失控的线程可能会消耗 CPU 周期、占用 RAM、导致异常等等,而停止它们的唯一方法是告诉 OS 进程调度程序通过取消线程的指令指针来强制终止线程(即停止执行)。如果您强行告诉 CPU 停止执行一系列指令,那么这些指令已分配或正在操作的资源会发生什么情况?他们是否处于稳定状态?他们是否得到适当的释放?等等……

            所以,是的,由于共享资源,线程需要比执行进程更多的思考和责任。

            【讨论】:

              【解决方案10】:

              对于任何需要长时间稳定和安全执行而没有故障或维护的应用程序,线程总是一个诱人的错误。他们总是被证明是比他们的价值更多的麻烦。它们产生快速的结果和原型,似乎运行正常,但运行几周或几个月后,您会发现它们存在严重缺陷。

              正如另一张海报所提到的,一旦您在程序中使用了一个线程,您现在就打开了一条不确定的代码执行路径,该路径可能会在时间、内存共享和竞争条件方面产生几乎无限的冲突。大多数对解决这些问题充满信心的表达方式是那些已经学习了多线程编程原理但还没有经历过解决这些问题的困难的人。

              线程是邪恶的。优秀的程序员会尽可能避免使用它们。这里提供了分叉的替代方案,它通常是许多应用程序的好策略。将代码分解为以某种松散耦合形式运行的单独执行进程的概念通常在支持它的平台上被证明是一种极好的策略。在单个程序中一起运行的线程不是解决方案。这通常是在您的设计中创建了一个致命的架构缺陷,只有通过重写整个程序才能真正补救。

              最近转向面向事件的并发是一项出色的开发创新。这类程序在部署后通常证明具有很强的持久性。

              我从来没有遇到过一个认为线程不好的年轻工程师。我从来没有遇到过像瘟疫一样避开他们的年长工程师。

              【讨论】:

                【解决方案11】:

                作为一名老工程师,我非常同意answer by Texas Arcane

                线程是非常邪恶的,因为它们会导致极难解决的错误。我确实花了几个月的时间来解决零星的比赛条件。一个例子导致有轨电车大约每月突然停在路中间一次,阻塞交通直到被拖走。幸运的是我没有创建错误,但我确实花了 4 个月的全职时间来解决它......

                添加到这个线程有点晚了,但我想提一个非常有趣的线程替代方案:使用协程和事件循环的异步编程。这正被越来越多的语言所支持,并且不会像多线程那样存在竞争条件问题。

                在用于等待来自多个来源的事件的情况下,它可以替代多线程,但不适用于需要在多个 CPU 内核上并行执行计算的情况。

                【讨论】:

                  猜你喜欢
                  • 2012-10-10
                  • 2011-10-24
                  • 2011-02-03
                  • 2010-09-16
                  • 2010-10-01
                  • 1970-01-01
                  • 2010-11-22
                  • 2011-02-04
                  相关资源
                  最近更新 更多