【问题标题】:What are multi-threading DOs and DONTs? [closed]什么是多线程 DO 和 DONT? [关闭]
【发布时间】:2009-08-24 10:36:05
【问题描述】:

我正在到处应用我新发现的线程知识并获得很多惊喜

示例:

我用线程在一个 大批。结果每次都不一样 时间。问题是我所有的 线程正在更新相同的 变量且未同步。

  • 有哪些已知线程问题?
  • 使用时应注意什么 线程?
  • 什么是好的多线程资源。
  • 请提供示例。

旁注:
(我将程序 thread_add.java 重命名为 thread_random_number_generator.java:-)

【问题讨论】:

  • 小事,任何人都可以在标题中添加缺少的空格吗? :-)
  • 这应该是一个维基。
  • 找个锁人。并在你做的时候锁上门。我的意思是线程。

标签: multithreading language-agnostic


【解决方案1】:

在多线程环境中,您必须注意同步,这样两个线程就不会因为同时执行修改而破坏状态。否则,您的代码中可能会出现竞争条件(例如,请参阅infamous Therac-25 accident。)您还必须调度线程来执行各种任务。然后,您必须确保您的同步和调度不会导致多个线程无限期地相互等待的死锁

同步

像增加计数器这样简单的事情需要同步:

counter += 1;

假设这一系列事件:

  • counter 初始化为 0
  • 线程A从内存中检索counter到cpu(0)
  • 上下文切换
  • 线程 B 从内存中检索counter 到 cpu (0)
  • 线程 B 在 cpu 上增加 counter
  • 线程 B 将 counter 从 cpu 写回内存 (1)
  • 上下文切换
  • 线程A在cpu上增加counter
  • 线程 A 将 counter 从 cpu 写回内存 (1)

此时counter 为1,但两个线程都尝试增加它。必须通过某种锁定机制来同步对计数器的访问:

lock (myLock) {
  counter += 1;
}

只允许一个线程执行锁定块内的代码。执行此代码的两个线程可能会导致以下事件序列:

  • 计数器初始化为 0
  • 线程A获取myLock
  • 上下文切换
  • 线程 B 尝试获取 myLock 但必须等待
  • 上下文切换
  • 线程A从内存中检索counter到cpu(0)
  • 线程A在cpu上增加counter
  • 线程 A 将 counter 从 cpu 写回内存 (1)
  • 线程 A 释放myLock
  • 上下文切换
  • 线程B获取myLock
  • 线程 B 从内存中检索counter 到 cpu (1)
  • 线程 B 在 cpu 上增加 counter
  • 线程 B 将 counter 从 cpu 写回内存 (2)
  • 线程 B 释放myLock

此时counter 为2。

调度

调度是同步的另一种形式,您必须使用线程同步机制(如事件、信号量、消息传递等)来启动和停止线程。这是 C# 中的一个简化示例:

AutoResetEvent taskEvent = new AutoResetEvent(false);

Task task;

// Called by the main thread.
public void StartTask(Task task) {
  this.task = task;
  // Signal the worker thread to perform the task.
  this.taskEvent.Set();
  // Return and let the task execute on another thread.
}

// Called by the worker thread.
void ThreadProc() {
  while (true) {
    // Wait for the event to become signaled.
    this.taskEvent.WaitOne();
    // Perform the task.
  }
}   

您会注意到对this.task 的访问可能没有正确同步,工作线程无法将结果返回给主线程,并且无法发出终止工作线程的信号。所有这些都可以在更详细的示例中得到纠正。

死锁

一个常见的死锁示例是当您有两个锁并且您不小心如何获取它们时。在某一时刻,您在lock2 之前获得了lock1

public void f() {
  lock (lock1) {
    lock (lock2) {
      // Do something
    }
  }
}

在另一点上,您在lock1 之前获得了lock2

public void g() {
  lock (lock2) {
    lock (lock1) {
      // Do something else
    }
  }
}

让我们看看这可能会如何死锁:

  • 线程A调用f
  • 线程A获取lock1
  • 上下文切换
  • 线程 B 调用g
  • 线程B获取lock2
  • 线程 B 尝试获取 lock1 但必须等待
  • 上下文切换
  • 线程 A 尝试获取 lock2 但必须等待
  • 上下文切换

此时线程 A 和 B 正在互相等待并死锁。

【讨论】:

  • 对我说你需要处理同步听起来像是从错误的结尾开始。你不妨说你应该确保你不需要同步,通过不共享可变状态和程序以更多的消息传递风格。
【解决方案2】:

有两种人不使用多线程。

1) 那些不理解这个概念并且不知道如何编程的人。 2) 那些完全理解这个概念并且知道把它做好是多么困难的人。

【讨论】:

  • 那些了解多线程有多困难并因此使用其他东西来实现并发的人:)(通信顺序进程、数据流变量、Erlang、Mozart Oz 等语言)
【解决方案3】:

我会做一个非常明目张胆的声明:

不要使用共享内存。

使用消息传递。

作为一般建议,请尝试限制共享状态的数量并更喜欢更多事件驱动的架构。

【讨论】:

    【解决方案4】:

    除了将您指向 Google 之外,我无法为您提供示例。搜索线程基础知识、线程同步,您将获得比您知道的更多的点击率。

    线程的基本问题是线程彼此不了解 - 所以它们会很高兴地踩到彼此的脚趾,就像两个人试图通过一扇门一样,有时他们会一个接一个地通过,但是有时他们会同时尝试通过并且会卡住。这很难重现,难以调试,并且有时会导致问题。如果您有线程并看到“随机”失败,这可能就是问题所在。

    因此需要注意共享资源。如果您和您的朋友想要一杯咖啡,但只有一个勺子,您不能同时使用它,那么你们中的一个将不得不等待另一个。用于“同步”对共享勺子的访问的技术是锁定。您确保在使用共享资源之前获得了锁定,然后再放开它。如果其他人有锁,你等到他们释放它。

    下一个问题与这些锁有关,有时您的程序可能很复杂,以至于您获得了锁,然后执行其他操作,然后访问另一个资源并尝试为此获得锁-但是其他一些线程具有第二个资源,所以你坐下来等待......但是如果第二个线程正在等待你为第一个资源持有的锁......它会坐下来等待。你的应用程序就在那里。这称为死锁,两个线程都在等待对方。

    这两个是绝大多数线程问题。答案通常是锁定时间越短越好,一次只持有 1 个锁定。

    【讨论】:

    • (+1) 用于出色的写作风格。你说的很清楚。
    • 如果您一次必须持有多个锁,请始终以相同的顺序获取锁。
    【解决方案5】:

    我注意到你正在用 java 编写,并且没有其他人提到书籍,所以 Java Concurrency In Practice 应该是你的多线程圣经。

    【讨论】:

    • 谢谢。但问题并不是特定于 java 的。我希望那本书中的实践普遍适用于用大多数(如果不是所有)编程语言编写的代码。
    【解决方案6】:

    -- 有哪些已知的线程问题? --

    -- 使用线程时应该注意什么? --

    在单处理器机器上使用多线程处理多个任务,其中每个任务花费大约相同的时间并不总是很有效。例如,您可能决定在程序中生成 10 个线程以处理 10 个单独的任务。如果每个任务大约需要 1 分钟的时间来处理,并且您使用 10 个线程来执行此处理,那么在整个 10 分钟内您将无法访问任何任务结果。相反,如果您只使用一个线程处理相同的任务,您将在 1 分钟内看到第一个结果,1 分钟后看到下一个结果,依此类推。如果您可以利用每个结果,而不必依赖同时准备好所有结果,那么单个 线程可能是实现程序的更好方式。

    如果您在一个进程中启动大量线程,线程内务管理和上下文切换的开销可能会变得很大。处理器将花费大量时间在线程之间切换,并且许多线程将无法取得进展。此外,具有大量线程的单个进程意味着其他进程中的线程的调度频率会降低,并且不会获得合理的处理器时间份额。

    如果多个线程必须共享许多相同的资源,您不太可能看到多线程应用程序带来的性能优势。许多开发人员将多线程视为某种能够自动带来性能优势的魔杖。不幸的是,多线程并不是它有时被认为是的魔杖。如果您出于性能原因使用多线程,您应该在几种不同的情况下非常密切地衡量您的应用程序的性能,而不是仅仅依靠一些不存在的魔法。

    协调线程对公共数据的访问可能是一个很大的性能杀手。使用粗锁计划时,使用多线程实现良好的性能并不容易,因为这会导致低并发和等待访问的线程。或者,除非您执行一些复杂的调整,否则细粒度的锁定策略会增加复杂性并且还会降低性能。

    使用多线程来利用具有多个处理器的机器在理论上听起来是个好主意,但在实践中你需要小心。要获得任何显着的性能优势,您可能需要掌握线程平衡。

    -- 请举例说明。 --

    例如,假设一个应用程序接收来自 网络,对这些信息进行聚合和排序,然后显示结果 在最终用户的屏幕上。

    对于双核机器,将任务分成三个线程是有意义的。第一个线程处理存储传入的价格信息,第二个线程处理价格,最后一个线程处理结果的显示。

    实施此解决方案后,假设您发现价格处理是迄今为止最长的阶段,因此您决定重写该线程的代码以将其性能提高三倍。不幸的是,单线程中的这种性能优势可能不会反映在整个应用程序中。这是因为其他两个线程可能无法跟上改进的线程。如果用户界面线程无法跟上更快的处理信息流,则其他线程现在必须等待系统中的新瓶颈。

    是的,这个例子直接来自我自己的经验:-)

    【讨论】:

      【解决方案7】:

      不要使用全局变量

      不要使用许多锁(最好不要使用 - 尽管实际上是不可能的)

      不要尝试成为英雄,实施复杂困难的 MT 协议

      使用简单的范例。即将一个数组的处理共享给 n 个相同大小的切片 - 其中 n 应该等于处理器的数量

      在不同的机器上测试你的代码(使用一个、两个、多个处理器)

      使用原子操作(如InterlockedIncrement()等)

      【讨论】:

      • 最后一个也应该是一个不喜欢第一个。联锁操作也不能很好地扩展(因为各种非常糟糕的缓存效果和其他 cpu 同步要求)。我仍然更喜欢锁而不是互锁操作,但是当分析显示锁有问题并且你不能做其他事情(比如减少共享)时,它们可能是最后的手段。
      • 你在考虑缓存和性能方面是对的。好点子。但是原子操作本质上是线程安全和无锁的。他们不会引入错误。
      【解决方案8】:

      YAGNI

      要记住的最重要的事情是:你真的需要多线程吗?

      【讨论】:

      • 他可能想学习个人成长的技巧。
      • @Gert 我同意他可能想研究个人成长的技术。在这种情况下,这个原则也成立。编写多线程代码应该有理由。
      • 在任何专业商店中,答案都可能是“是”。如果不是现在,那么在某个时候。不知道这个话题最终会阻碍你。
      【解决方案9】:

      到目前为止,我几乎同意所有答案。

      一个好的编码策略是尽可能减少或消除线程之间共享的数据量。你可以这样做:

      • 使用线程静态变量(尽管不要过分强调这一点,它会占用每个线程更多的内存,具体取决于您的操作系统)。
      • 将每个线程使用的所有状态打包到一个类中,然后保证每个线程都获得一个完全属于自己的状态类实例。将此视为“滚动您自己的静态线程”,但可以更好地控制流程。
      • 在线程之间按值编组数据,而不是共享相同的数据。要么使您的数据传输类不可变,要么保证所有跨线程调用都是同步的,或者两者兼而有之。

      尽量不要让多个线程竞争完全相同的 I/O“资源”,无论是磁盘文件、数据库表、Web 服务调用还是其他任何东西。当多个线程争夺同一资源时,这将导致争用。

      这是一个非常人为设计的 OTT 示例。在实际应用中,您可以限制线程数以减少调度开销:

      • 所有 UI - 一个线程。
      • 后台计算 - 一个线程。
      • 将错误记录到磁盘文件 - 一个线程。
      • 调用 Web 服务 - 每个唯一物理主机一个线程。
      • 查询数据库 - 每个独立组需要更新的表一个线程。

      与其猜测如何分配任务,不如分析您的应用程序并隔离那些 (a) 非常慢和 (b) 可以异步完成的部分。这些都是单独线程的好候选。

      以下是你应该避免的:

      • 计算、数据库命中、服务调用等 - 全部在一个线程中完成,但会多次启动以“提高性能”。

      【讨论】:

      • 或者使用类似 .net 的任务并行库这样的框架,创建很多小任务,让运行时系统决定并行执行哪些任务。 (在这种情况下,没有共享状态更重要)
      【解决方案10】:

      除非确实需要,否则不要启动新线程。启动线程并不便宜,对于短期运行的任务,启动线程实际上可能比执行任务本身花费更多的时间。如果您使用 .NET,请查看内置线程池,它在很多(但不是全部)情况下都很有用。通过重用线程,降低了启动线程的成本。

      编辑:关于创建线程与使用线程池(.NET 特定)的一些注意事项

      一般尽量使用线程池。例外:

      • 长时间运行的 CPU 密集型任务和阻塞任务不适合在线程池上运行,因为它们会强制池创建额外的线程。
      • 所有线程池线程都是后台线程,所以如果你需要你的线程是前台,你必须自己启动它。
      • 如果您需要不同优先级的线程。
      • 如果您的线程需要多于(或少于)标准的 1 MB 堆栈空间。
      • 如果您需要能够控制线程的生命周期。
      • 如果您需要与线程池提供的不同行为来创建线程(例如,池将限制新线程的创建,这可能是您想要的,也可能不是您想要的)。

      可能还有更多例外,我并不是说这是确定的答案。这正是我能想到的atm。

      【讨论】:

      • (+1)我怎么知道我是否真的需要一个线程来完成特定任务?当然,我会通过经验来了解这一点。但是,是否有一些技巧或经验法则?
      【解决方案11】:

      我正在应用我的发现的线程知识

      [强调添加]

      记住一点知识是危险的。了解您平台的线程 API 很容易。知道为什么当你需要使用同步是困难的部分。阅读“死锁”、“竞争条件”、“优先级反转”会让你开始理解为什么。

      何时使用同步的细节既简单(共享数据需要同步)又复杂(以正确方式使用的原子数据类型不需要同步,哪些数据是真正共享的):一生的学习和非常好的解决方案具体的。

      【讨论】:

        【解决方案12】:

        需要注意的重要事项(具有多个内核和 CPU)是cache coherency

        【讨论】:

        【解决方案13】:

        我很惊讶没有人指出 Herb Sutter 的 Effective Concurrency 专栏。在我看来,如果你想去线程附近的任何地方,这是必读的。

        【讨论】:

          【解决方案14】:

          a) 始终只让 1 个线程负责资源的生命周期。这样线程 A 就不会删除线程 B 需要的资源 - 如果 B 拥有资源的所有权

          b) 期待意外

          【讨论】:

            【解决方案15】:

            请考虑如何测试您的代码并为此留出大量时间。单元测试变得更加复杂。您可能无法手动测试您的代码 - 至少不可靠。

            请考虑线程生命周期以及线程如何退出。不要杀死线程。提供一种机制,使它们能够优雅地退出。

            请在您的代码中添加某种调试日志记录 - 这样您就可以在出现问题时看到您的线程在开发和生产中的行为都正确无误。

            请使用一个好的库来处理线程,而不是滚动您自己的解决方案(如果可以的话)。例如。 java.util.concurrency

            不要假设共享资源是线程安全的。

            不要这样做。例如。使用可以为您处理线程问题的应用程序容器。使用消息传递。

            【讨论】:

              【解决方案16】:

              在 .Net 中,当我开始尝试使用多线程时,让我感到惊讶的一件事是,除了创建 UI 控件的线程之外,您无法直接从任何线程更新 UI 控件。

              有一种方法可以解决这个问题,就是使用 Control.Invoke 方法来更新另一个线程上的控件,但第一次不是 100% 明显!

              【讨论】:

              • 不限于 .NET,很多 GUI 工具包都是“单线程”的。
              【解决方案17】:

              在您开始真正的项目之前,不要误以为您了解并发的困难。

              所有死锁、活锁、同步等的例子看起来都很简单,其实也很简单。但是它们会误导你,因为大家都在谈论的实现并发的“难点”是在实际项目中使用时,你无法控制一切。

              【讨论】:

                【解决方案18】:

                虽然正如一些受访者所指出的那样,您在数字总和上的初始差异可能是缺乏同步的结果,但如果您深入了解该主题,请注意,一般来说,您将无法准确地重现您在串行程序中获得的数字结果与来自同一程序的并行版本的结果。浮点算术不是严格可交换的、结合的或分配的;哎呀,它甚至没有关闭。

                我想与这里的多数意见有所不同。如果您正在为具有一个或多个多核 CPU 的桌面编写多线程程序,那么您正在使用共享内存计算机并且应该处理共享内存编程。 Java 具有执行此操作的所有功能。

                如果不知道更多关于您正在解决的问题类型,我会犹豫是否要写“你应该这样做”或“你不应该那样做”。

                【讨论】:

                • 偏离原来的问题:浮点运算不是封闭的?为什么不? (Inf 和 NaN 不是 IEEE-754 浮点数吗?)
                猜你喜欢
                • 2011-08-31
                • 2011-04-16
                • 2013-07-31
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2021-09-07
                相关资源
                最近更新 更多