解决多线程中11个常见问题

上传人:ni****g 文档编号:477620526 上传时间:2022-09-13 格式:DOCX 页数:12 大小:30.35KB
返回 下载 相关 举报
解决多线程中11个常见问题_第1页
第1页 / 共12页
解决多线程中11个常见问题_第2页
第2页 / 共12页
解决多线程中11个常见问题_第3页
第3页 / 共12页
解决多线程中11个常见问题_第4页
第4页 / 共12页
解决多线程中11个常见问题_第5页
第5页 / 共12页
点击查看更多>>
资源描述

《解决多线程中11个常见问题》由会员分享,可在线阅读,更多相关《解决多线程中11个常见问题(12页珍藏版)》请在金锄头文库上搜索。

1、并发危险解决多线程代码中的11个常见的问题Joe Duffy本文将介绍以下内容:本文使用了以下技术:基本并发概念多线程、.NET Framework并发问题和抑制措施实现安全性的模式横切概念目录数据争用忘记同步粒度错误读写撕裂无锁定重新排序重新进入死锁锁保护戳记两步舞曲优先级反转实现安全性的模式不变性纯度 隔离并发现象无处不在。服务器端程序长久以来都必须负责处理基本并发编程模型,而随着多核处理器 的日益普及,客户端程序也将需要执行一些任务。随着并发操作的不断增加,有关确保安全的问题也 浮现出来。也就是说,在面对大量逻辑并发操作和不断变化的物理硬件并行性程度时,程序必须继续 保持同样级别的稳定性

2、和可靠性。与对应的顺序代码相比,正确设计的并发代码还必须遵循一些额外的规则。对内存的读写以及对共享 资源的访问必须使用同步机制进行管制,以防发生冲突。另外,通常有必要对线程进行协调以协同完 成某项工作。这些附加要求所产生的直接结果是,可以从根本上确保线程始终保持一致并且保证其顺利向前推进。 同步和协调对时间的依赖性很强,这就导致了它们具有不确定性,难于进行预测和测试。这些属性之所以让人觉得有些困难,只是因为人们的思路还未转变过来。没有可供学习的专门API, 也没有可进行复制和粘贴的代码段。实际上的确有一组基础概念需要您学习和适应。很可能随着时间 的推移某些语言和库会隐藏一些概念,但如果您现在就

3、开始执行并发操作,则不会遇到这种情况。本 文将介绍需要注意的一些较为常见的挑战,并针对您在软件中如何运用它们给出一些建议。 首先我将讨论在并发程序中经常会出错的一类问题。我把它们称为“安全隐患”,因为它们很容易发现 并且后果通常比较严重。这些危险会导致您的程序因崩溃或内存问题而中断。当从多个线程并发访问数据时会发生数据争用(或竞争条件)。特别是,在一个或多个线程写入一段 数据的同时,如果有一个或多个线程也在读取这段数据,则会发生这种情况。之所以会出现这种问题, 是因为 Windows 程序(如 C+ 和 Microsoft .NET Framework 之类的程序)基本上都基于共享内存 概念,

4、进程中的所有线程均可访问驻留在同一虚拟地址空间中的数据。静态变量和堆分配可用于共享。 请考虑下面这个典型的例子:static class Counter internal static int s_curr = 0;internal static int GetNext() return s_curr+;Counter 的目标可能是想为 GetNext 的每个调用分发一个新的唯一数字。但是,如果程序中的两个线 程同时调用GetNext,则这两个线程可能被赋予相同的数字。原因是s_curr+编译包括三个独立的 步骤:1. 将当前值从共享的 s_curr 变量读入处理器寄存器。2. 递增该寄存器。

5、3. 将寄存器值重新写入共享 s_curr 变量。按照这种顺序执行的两个线程可能会在本地从 s_curr 读取了相同的值(比如 42)并将其递增到某个 值(比如 43),然后发布相同的结果值。这样一来, GetNext 将为这两个线程返回相同的数字,导 致算法中断。虽然简单语句 s_curr+ 看似不可分割,但实际却并非如此。忘记同步 这是最简单的一种数据争用情况:同步被完全遗忘。这种争用很少有良性的情况,也就是说虽然它们 是正确的,但大部分都是因为这种正确性的根基存在问题。这种问题通常不是很明显。例如,某个对象可能是某个大型复杂对象图表的一部分,而该图表恰好可 使用静态变量访问,或在创建新线

6、程或将工作排入线程池时通过将某个对象作为闭包的一部分进行传 递可变为共享图表。当对象(图表)从私有变为共享时,一定要多加注意。这称为发布,在后面的隔离上下文中会对此加 以讨论。反之称为私有化,即对象(图表)再次从共享变为私有。对这种问题的解决方案是添加正确的同步。在计数器示例中,我可以使用简单的联锁:static class Counter internal static volatile int s_curr = 0;internal static int GetNext() return Interlocked.Increment(ref s curr);它之所以起作用,是因为更新被限定在

7、单一内存位置,还因为(这一点非常方便)存在硬件指令(LOCK INC),它相当于我尝试进行原子化操作的软件语句。或者,我可以使用成熟的锁定:static class Counter internal static int s_curr = 0;private static object s_currLock = new object();internal static int GetNext() lock (s_currLock) return s_curr+;lock语句可确保试图访问GetNext的所有线程彼此之间互斥,并且它使用CLRSystem.Threading.Monitor类。C

8、+程序使用CRITICAL_SECTION来实现相同目的。虽然对这个特 定的示例不必使用锁定,但当涉及多个操作时,几乎不可能将其并入单个互锁操作中。粒度错误即使使用正确的同步对共享状态进行访问,所产生的行为仍然可能是错误的。粒度必须足够大,才能 将必须视为原子的操作封装在此区域中。这将导致在正确性与缩小区域之间产生冲突,因为缩小区域 会减少其他线程等待同步进入的时间。例如,让我们看一看图1所示的银行帐户抽象。一切都很正常,对象的两个方法(Deposit和 Withdraw)看起来不会发生并发错误。一些银行业应用程序可能会使用它们,而且不担心余额会因为 并发访问而遭到损坏。图1银行帐户class

9、 BankAccount private decimal m_balance = 0.0M;private object m_balanceLock = new object();internal void Deposit(decimal delta) lock (m_balanceLock) m_balance += delta; internal void Withdraw(decimal delta) lock (m_balanceLock) if (m_balance delta)throw new Exception(Insufficient funds); m_balance -=

10、 delta;但是,如果您想添加一个 Transfer 方法该怎么办?一种天真的(也是不正确的)想法会认为由于Deposit和Withdraw是安全隔离的,因此很容易就可以合并它们:class BankAccount internal static void Transfer(BankAccount a, BankAccount b, decimal delta) Withdraw(a, delta);Deposit(b, delta);/ As before这是不正确的。实际上,在执行 Withdraw 与 Deposit 调用之间的一段时间内资金会完全丢失。 正确的做法是必须提前对a和b进

11、行锁定,然后再执行方法调用:class BankAccount internal static void Transfer(BankAccount a, BankAccount b, decimal delta) lock (a.m_balanceLock) lock (b.m_balanceLock) Withdraw(a, delta);Deposit(b, delta);/ As before事实证明,此方法可解决粒度问题,但却容易发生死锁。稍后,您会了解到如何修复它。读写撕裂如前所述,良性争用允许您在没有同步的情况下访问变量。对于那些对齐的、自然分割大小的字一例 如,用指针分割大小的内

12、容在32位处理器中是32位的(4字节),而在64位处理器中则是64位 的(8字节)读写操作是原子的。如果某个线程只读取其他线程将要写入的单个变量,而没有涉 及任何复杂的不变体,则在某些情况下您完全可以根据这一保证来略过同步。但要注意。如果试图在未对齐的内存位置或未采用自然分割大小的位置这样做,可能会遇到读写撕裂 现象。之所以发生撕裂现象,是因为此类位置的读或写实际上涉及多个物理内存操作。它们之间可能 会发生并行更新,并进而导致其结果可能是之前的值和之后的值通过某种形式的组合。例如,假设ThreadA处于循环中,现在需要仅将OxOL和OxaaaabbbbccccddddL写入64位变量s_x 中

13、。ThreadB在循环中读取它(参见图2)。图2将要发生的撕裂现象internal static volatile long s_x;void ThreadA() int i = 0;while (true) s_x = (i & 1) = 0 ? OxOL : OxaaaabbbbccccddddL;i+;void ThreadB() while (true) long x = s_x;Debug.Assert(x = 0x0L | x = OxaaaabbbbccccddddL);您可能会惊讶地发现ThreadB的声明可能会被触发。原因是ThreadA的写入操作包含两部分(高32 位和低3

14、2位),具体顺序取决于编译器。ThreadB的读取也是如此。因此ThreadB可以见证值 OxaaaabbbbOOOOOOOOL 或 OxOOOOOOOOaaaabbbbL。无锁定重新排序有时编写无锁定代码来实现更好的可伸缩性和可靠性是一种非常诱人的想法。这样做需要深入了解目 标平台的内存模型(有关详细信息,请参阅Vance Morrison的文章Memory Models:Understand the Impact of Low-Lock Techniques in Multithreaded Apps,网址为 错误。之所以发生这些错误,是因为编译器和处理器在处理或优化期间可自由重新排序内存

15、操作。 例如,假设s_x和s_y均被初始化为值0,如下所示:internalstaticvolatileintsx =0;internalstaticvolatileintsxa=0;internalstaticvolatileints_y =0;internalstaticvolatileints_ya=0;void ThreadA() s_x = 1;s_ya = s_y; void ThreadB() s_y = 1;s_xa = s_x;是否有可能在ThreadA和ThreadB均运行完成后,s_ya和s_xa都包含值0?看上去这个问题很 可笑。或者 s_x = 1 或者 s_y = 1 会首先发生,在这种情况下,其他线程会在开始处理其自身的更新 时见证这一

展开阅读全文
相关资源
相关搜索

当前位置:首页 > 学术论文 > 其它学术论文

电脑版 |金锄头文库版权所有
经营许可证:蜀ICP备13022795号 | 川公网安备 51140202000112号