什么是多线程DO和DONT?

时间:2009-08-24 10:36:05

标签: multithreading language-agnostic

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

实施例:

  

我使用线程来添加数字   阵列。结果各不相同   时间。问题是我的全部   线程正在更新相同   变量并且没有同步。

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

旁注:
(我将我的计划thread_add.java重命名为thread_random_number_generator.java: - )

18 个答案:

答案 0 :(得分:28)

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

<强>同步

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

counter += 1;

假设这一系列事件:

  • counter初始化为0
  • 线程A从内存中检索counter到cpu(0)
  • 上下文切换
  • 线程B从内存中检索counter到cpu(0)
  • 线程B在cpu上增加counter
  • 线程B将counter从cpu写回到memory(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的访问可能没有正确同步,工作者线程无法将结果返回给主线程,并且没有办法向工作线程发出信号终止。所有这些都可以通过更详细的例子来纠正。

<强>死锁

死锁的一个常见例子是当你有两个锁并且你不小心如何获得它们。您曾在lock1之前获得lock2

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

另一方面,您会在lock2之前获得lock1

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

让我们看看这可能会陷入僵局:

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

此时线程A和B正在等待并且已经死锁。

答案 1 :(得分:16)

有两种人不使用多线程。

1)那些不理解这个概念并且不知道如何编程的人。 2)那些完全理解这个概念并且知道如何正确理解它的人。

答案 2 :(得分:13)

我会发表一个非常明确的声明:

不要使用共享内存。

DO 使用消息传递。

作为一般建议,请尝试限制共享状态的数量,并选择更多事件驱动的体系结构。

答案 3 :(得分:9)

除了指向Google之外,我不能给你一些例子。搜索线程基础知识,线程同步,你会得到比你知道更多的命中。

线程的基本问题是线程彼此不了解 - 所以他们会愉快地踩到彼此的脚趾,就像2个人试图通过1个门,有时他们会一个接一个地通过,但是有时他们会同时试图通过并且会卡住。这很难重现,难以调试,有时会引起问题。如果你有线程并看到“随机”失败,这可能就是问题所在。

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

下一个问题来自那些锁,有时候你可以拥有一个复杂的程序,以至于你得到一个锁,做一些其他的东西然后访问另一个资源并试图获得一个锁 - 但是其他一些线程有这个第二个资源,所以你坐等等......但是如果那个第二个线程正在等待你为第一个资源保持的锁定......那么它将等待。你的应用就坐在那里。这称为死锁,2个线程互相等待。

这两个是绝大多数线程问题。答案通常是锁定尽可能短的时间,并且一次只能锁定1次。

答案 4 :(得分:7)

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

答案 5 :(得分:5)

不要使用全局变量

不要使用很多锁(最多没有锁 - 虽然几乎不可能)

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

DO 使用简单的范例。即将数组处理为相同大小的n个切片 - 其中n应等于处理器数

DO 在不同的计算机上测试您的代码(使用一个,两个,多个处理器)

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

答案 6 :(得分:5)

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

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

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

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

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

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

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

- 请提供示例。 -

例如,想象一下从中接收传入价格信息的应用程序 网络,聚合和排序该信息,然后显示结果 在最终用户的屏幕上。

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

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

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

答案 7 :(得分:3)

YAGNI

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

答案 8 :(得分:3)

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

良好的编码策略是尽可能减少或消除线程之间共享的数据量。您可以通过以下方式执行此操作:

  • 使用线程静态变量(尽管不要过度使用它,每个线程会占用更多内存,具体取决于你的操作系统)。
  • 将每个线程使用的所有状态打包成一个类,然后保证每个线程只向自己获取一个状态类实例。将此视为“滚动自己的线程静态”,但可以更好地控制该过程。
  • 通过线程之间的值对数据进行编组,而不是共享相同的数据。要么使数据传输类不可变,要么保证所有跨线程调用都是同步的,或两者兼而有之。

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

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

  • 所有用户界面 - 一个帖子。
  • 背景计算 - 一个主题。
  • 将错误记录到磁盘文件 - 一个线程。
  • 调用Web服务 - 每个唯一的物理主机一个线程。
  • 查询数据库 - 每个需要更新的表的一个线程。

不是猜测如何对任务进行分配,而是分析您的应用并隔离那些(a)非常慢的位,以及(b)可以异步完成。这些是单独线程的好候选人。

这是你应该避免的:

  • Calcs,数据库命中,服务调用等 - 所有这些都在一个线程中,但多次旋转“以提高性能”。

答案 9 :(得分:2)

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

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

一般尝试使用线程池。例外情况:

  • 长时间运行的CPU绑定任务和阻塞任务在线程池上运行并不理想,因为它们会强制池创建其他线程。
  • 所有线程池线程都是后台线程,因此如果您需要将线程作为前台,则必须自己启动它。
  • 如果您需要具有不同优先级的线程。
  • 如果您的线程需要比标准1 MB堆栈空间更多(或更少)。
  • 如果您需要能够控制线程的使用寿命。
  • 如果你需要不同的行为来创建线程而不是线程池提供的线程(例如,池将限制新线程的创建,这可能是你想要的,也可能不是你想要的。)

可能有更多例外,我并不是说这是最终的答案。这正是我能想到的。

答案 10 :(得分:1)

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

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

请在代码中添加某种调试日志记录 - 以便在事情发生时,您可以看到您的线程在开发和生产中都表现正常。

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

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

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

答案 11 :(得分:1)

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

b)期待意外

答案 12 :(得分:1)

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

答案 13 :(得分:1)

  

我正在应用找到的线程无处不在的知识

[强调补充]

DO 请记住,一点点知识是危险的。了解平台的线程API很容易。在需要使用同步时知道的原因是困难的部分。阅读“死锁”,“竞争条件”,“优先倒置”将使您了解原因。

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

答案 14 :(得分:1)

需要注意的重要事项(使用多个内核和CPU)cache coherency

答案 15 :(得分:0)

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

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

在不了解你正在处理的问题类型的情况下,我会毫不犹豫地写下“你应该这样做”或“你不应该这样做”。

答案 16 :(得分:0)

在你将头脑分成一个真实的项目之前,不要误以为你理解并发的困难。

所有死锁,活锁,同步等的例子看似简单,而且它们都是。但是他们会误导你,因为实现每个人都在谈论的并发性的“困难”是它在一个真实的项目中使用,而你却无法控制一切。

答案 17 :(得分:0)

在.Net中,当我开始尝试进入多线程时,让我感到惊讶的一件事是,你不能直接从除创建UI控件的线程之外的任何线程更新UI控件。

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