目录程序员
数据争用
忘记同步
粒度错误
读写撕裂
无锁定从新排序
从新进入
死锁
锁保护
戳记
两步舞曲
优先级反转
实现安全性的模式
不变性
纯度
隔离
算法
并发现象无处不在。服 务器端程序长久以来都必须负责处理基本并发编程模型,而随着多核处理器的日益普及,客户端程序也将须要执行一些任务。随着并发操做的不断增长,有关确保安 全的问题也浮现出来。也就是说,在面对大量逻辑并发操做和不断变化的物理硬件并行性程度时,程序必须继续保持一样级别的稳定性和可靠性。数据库
与对应的顺序代码相比,正确设计的并发代码还必须遵循一些额外的规则。对内存的读写以及对共享资源的访问必须使用同步机制进行管制,以防发生冲突。另外,一般有必要对线程进行协调以协同完成某项工做。编程
这些附加要求所产生的直接结果是,能够从根本上确保线程始终保持一致而且保证其顺利向前推动。同步和协调对时间的依赖性很强,这就致使了它们具备不肯定性,难于进行预测和测试。设计模式
这 些属性之因此让人以为有些困难,只是由于人们的思路还未转变过来。没有可供学习的专门 API,也没有可进行复制和粘贴的代码段。实际上的确有一组基础概念须要您学习和适应。极可能随着时间的推移某些语言和库会隐藏一些概念,但若是您如今就 开始执行并发操做,则不会遇到这种状况。本文将介绍须要注意的一些较为常见的挑战,并针对您在软件中如何运用它们给出一些建议。缓存
首先我将讨论在并发程序中常常会出错的一类问题。我把它们称为“安全隐患”,由于它们很容易发现而且后果一般比较严重。这些危险会致使您的程序因崩溃或内存问题而中断。安全
当 从多个线程并发访问数据时会发生数据争用(或竞争条件)。特别是,在一个或多个线程写入一段数据的同时,若是有一个或多个线程也在读取这段数据,则会发生 这种状况。之因此会出现这种问题,是由于 Windows 程序(如 C++ 和 Microsoft .NET Framework 之类的程序)基本上都基于共享内存概念,进程中的全部线程都可访问驻留在同一虚拟地址空间中的数据。静态变量和堆分配可用于共享。服务器
请考虑下面这个典型的例子:数据结构
static class Counter { internal static int s_curr = 0; internal static int GetNext() { return s_curr++; } }
Counter 的目标多是想为 GetNext 的每一个调用分发一个新的惟一数字。可是,若是程序中的两个线程同时调用 GetNext,则这两个线程可能被赋予相同的数字。缘由是 s_curr++ 编译包括三个独立的步骤:多线程
将当前值从共享的 s_curr 变量读入处理器寄存器。
递增该寄存器。
将寄存器值从新写入共享 s_curr 变量。
按 照这种顺序执行的两个线程可能会在本地从 s_curr 读取了相同的值(好比 42)并将其递增到某个值(好比 43),而后发布相同的结果值。这样一来,GetNext 将为这两个线程返回相同的数字,致使算法中断。虽然简单语句 s_curr++ 看似不可分割,但实际却并不是如此。
忘记同步
这是最简单的一种数据争用状况:同步被彻底遗忘。这种争用不多有良性的状况,也就是说虽然它们是正确的,但大部分都是由于这种正确性的根基存在问题。
这种问题一般不是很明显。例如,某个对象多是某个大型复杂对象图表的一部分,而该图表刚好可以使用静态变量访问,或在建立新线程或将工做排入线程池时经过将某个对象做为闭包的一部分进行传递可变为共享图表。
当对象(图表)从私有变为共享时,必定要多加注意。这称为发布,在后面的隔离上下文中会对此加以讨论。反之称为私有化,即对象(图表)再次从共享变为私有。
对这种问题的解决方案是添加正确的同步。在计数器示例中,我可使用简单的联锁:
static class Counter { internal static volatile int s_curr = 0; internal static int GetNext() { return Interlocked.Increment(ref s_curr); } }
它之因此起做用,是由于更新被限定在单一内存位置,还由于(这一点很是方便)存在硬件指令 (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 的全部线程彼此之间互斥,而且它使用 CLR System.Threading.Monitor 类。C++ 程序使用 CRITICAL_SECTION 来实现相同目的。虽然对这个特定的示例没必要使用锁定,但当涉及多个操做时,几乎不可能将其并入单个互锁操做中。
粒度错误
即便使用正确的同步对共享状态进行访问,所产生的行为仍然多是错误的。粒度必须足够大,才能将必须视为原子的操做封装在此区域中。这将致使在正确性与缩小区域之间产生冲突,由于缩小区域会减小其余线程等待同步进入的时间。
例如,让咱们看一看图 1 所示的银行账户抽象。一切都很正常,对象的两个方法(Deposit 和 Withdraw)看起来不会发生并发错误。一些银行业应用程序可能会使用它们,并且不担忧余额会由于并发访问而遭到损坏。
图 1 银行账户
class 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 -= 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 进行锁定,而后再执行方法调用:
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 }
事实证实,此方法可解决粒度问题,但却容易发生死锁。稍后,您会了解到如何修复它。
读写撕裂
如 前所述,良性争用容许您在没有同步的状况下访问变量。对于那些对齐的、天然分割大小的字 — 例如,用指针分割大小的内容在 32 位处理器中是 32 位的(4 字节),而在 64 位处理器中则是 64 位的(8 字节)— 读写操做是原子的。若是某个线程只读取其余线程将要写入的单个变量,而没有涉及任何复杂的不变体,则在某些状况下您彻底能够根据这一保证来略过同步。
但要注意。若是试图在未对齐的内存位置或未采用天然分割大小的位置这样作,可能会遇到读写撕裂现象。之因此发生撕裂现象,是由于此类位置的读或写实际上涉及多个物理内存操做。它们之间可能会发生并行更新,并进而致使其结果多是以前的值和以后的值经过某种形式的组合。
例如,假设 ThreadA 处于循环中,如今须要仅将 0x0L 和 0xaaaabbbbccccddddL 写入 64 位变量 s_x 中。ThreadB 在循环中读取它(参见图 2)。
图 2 将要发生的撕裂现象
internal static volatile long s_x; void ThreadA() { int i = 0; while (true) { s_x = (i & 1) == 0 ? 0x0L : 0xaaaabbbbccccddddL; i++; } } void ThreadB() { while (true) { long x = s_x; Debug.Assert(x == 0x0L || x == 0xaaaabbbbccccddddL); } }
您 可能会惊讶地发现 ThreadB 的声明可能会被触发。缘由是 ThreadA 的写入操做包含两部分(高 32 位和低 32 位),具体顺序取决于编译器。ThreadB 的读取也是如此。所以 ThreadB 能够见证值 0xaaaabbbb00000000L 或 0x00000000aaaabbbbL。
无锁定从新排序
有 时编写无锁定代码来实现更好的可伸缩性和可靠性是一种很是诱人的想法。这样作须要深刻了解目标平台的内存模型(有关详细信息,请参阅 Vance Morrison 的文章 "Memory Models:Understand the Impact of Low-Lock Techniques in Multithreaded Apps",网址为 msdn.microsoft.com/magazine/cc163715)。若是不了解或不注意这些规则可能会致使内存从新排序错误。之因此发生这些错误,是由于编译器和处理器在处理或优化期间可自由从新排序内存操做。
例如,假设 s_x 和 s_y 均被初始化为值 0,以下所示:
internal static volatile int s_x = 0; internal static volatile int s_xa = 0; internal static volatile int s_y = 0; internal static volatile int s_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 会首先发生,在这种状况下,其余线程会在开始处理其自身的更新时见证这一更新。至少理论上如此。
遗憾的是,处理器随时均可能从新排序此代码,以使在写入以前加载操做更有效。您能够借助一个显式内存屏障来避免此问题:
void ThreadA() { s_x = 1; Thread.MemoryBarrier(); s_ya = s_y; }
.NET Framework 为此提供了一个特定 API,C++ 提供了 _MemoryBarrier 和相似的宏。但这个示例并非想说明您应该在各处都插入内存屏障。它要说明的是在彻底弄清内存模型以前,应避免使用无锁定代码,并且即便在彻底弄清以后也 应谨慎行事。
在 Windows(包括 Win32 和 .NET Framework)中,大多数锁定都支持递归得到。这只是意味着,即便当前线程已持有锁但当它试图再次得到时,其要求仍会获得知足。这使得经过较小的原 子操做构成较大的原子操做变得更加容易。实际上,以前给出的 BankAccount 示例依靠的就是递归得到:Transfer 对 Withdraw 和 Deposit 都进行了调用,其中每一个都重复得到了 Transfer 已得到的锁定。
但 是,若是最终发生了递归得到操做而您实际上并不但愿如此,则这可能就是问题的根源。这多是由于从新进入而致使的,而发生从新进入的缘由多是因为对动态 代码(如虚拟方法和委托)的显式调用或因为隐式从新输入的代码(如 STA 消息提取和异步过程调用)。所以,最好不要从锁定区域对动态方法进行调用。
例如,设想某个方法暂时破坏了不变体,而后又调用委托:
class C { private int m_x = 0; private object m_xLock = new object(); private Action m_action = ...; internal void M() { lock (m_xLock) { m_x++; try { m_action(); } finally { Debug.Assert(m_x == 1); m_x--; } } } }
C 的方法 M 可确保 m_x 不发生改变。但会有很短的一段时间,m_x 会先递增 1,而后再从新递减。对 m_action 的调用看起来没有任何问题。遗憾的是,若是它是从 C 类用户接受的委托,则表示任何代码均可以执行它所请求的操做。这包括回调到同一实例的 M 方法。若是发生了这种状况,finally 中的声明可能会被触发;同一堆栈中可能存在多个针对 M 的活动的调用(即便您未直接执行此操做),这必然会致使 m_x 包含的值大于 1。
当多个线程遇到死锁时,系统会直接中止响应。多篇《MSDN 杂志》文章都介绍了死锁的发生缘由以及使死锁变得可以接受的一些方法,其中包括我本身的文章 "No More Hangs:Advanced Techniques to Avoid and Detect Deadlocks in .NET Apps"(网址为 msdn.microsoft.com/magazine/cc163618)以及 Stephen Toub 的 2007 年 10 月 .NET 相关问题专栏(网址为 msdn.microsoft.com/magazine/cc163352), 所以这里只作简单的讨论。总而言之,只要出现了循环等待链 — 例如,ThreadA 正在等待 ThreadB 持有的资源,而 ThreadB 反过来也在等待 ThreadA 持有的资源(也许是间接等待第三个 ThreadC 或其余资源)— 则全部向前的推动工做均可能会停下来。
此 问题的常见根源是互斥锁。实际上,以前所示的 BankAccount 示例遇到的就是这个问题。若是 ThreadA 试图将 $500 从账户 #1234 转移到账户 #5678,与此同时 ThreadB 试图将 $500 从 #5678 转移到 #1234,则代码可能发生死锁。
使用一致的得到顺序可避免死锁,如图 3 所示。此逻辑可归纳为“同步锁得到”之类的名称,经过此操做可依照各个锁之间的某种顺序动态排序多个可锁定的对象,从而使得在以一致的顺序得到两个锁的同时必须维持两个锁的位置。另外一个方案称为“锁矫正”,可用于拒绝被认定以不一致的顺序完成的锁得到。
图 3 一致的得到顺序
class BankAccount { private int m_id; // Unique bank account ID. internal static void Transfer( BankAccount a, BankAccount b, decimal delta) { if (a.m_id < b.m_id) { Monitor.Enter(a.m_balanceLock); // A first Monitor.Enter(b.m_balanceLock); // ...and then B } else { Monitor.Enter(b.m_balanceLock); // B first Monitor.Enter(a.m_balanceLock); // ...and then A } try { Withdraw(a, delta); Deposit(b, delta); } finally { Monitor.Exit(a.m_balanceLock); Monitor.Exit(b.m_balanceLock); } } // As before ... }
但 锁并非致使死锁的惟一根源。唤醒丢失是另外一种现象,此时某个事件被遗漏,致使线程永远休眠。在 Win32 自动重置和手动重置事件、CONDITION_VARIABLE、CLR Monitor.Wait、Pulse 以及 PulseAll 调用等同步事件中常常会发生这种状况。唤醒丢失一般是一种迹象,表示同步不正确,没法重置等待条件或在 wake-all(WakeAllConditionVariable 或 Monitor.PulseAll)更为适用的状况下使用了 wake-single 基元(WakeConditionVariable 或 Monitor.Pulse)。
此问题的另外一个常见根源是自动重置事件和手动重置事件信号丢失。因为此类事件只能处于一个状态(有信号或无信号),所以用于设置此事件的冗余调用实际上将被忽略不计。若是代码认定要设置的两个调用始终须要转换为两个唤醒的线程,则结果可能就是唤醒丢失。
锁保护
当某个锁的到达率与其锁得到率相比始终居高不下时,可能会产生锁保护。在极端的状况下,等待某个锁的线程超过了其承受力,就会致使灾难性后果。对于服务器端的程序而言,若是客户端所需的某些受锁保护的数据结构需求量大增,则常常会发生这种状况。
例如,请设想如下状况:平均来讲,每 100 毫秒会到达 8 个请求。咱们将八个线程用于服务请求(由于咱们使用的是 8-CPU 计算机)。这八个线程中的每个都必须得到一个锁并保持 20 毫秒,而后才能展开实质的工做。
遗 憾的是,对这个锁的访问须要进行序列化处理,所以,所有八个线程须要 160 毫秒才能进入并离开锁。第一个退出后,须要通过 140 毫秒第九个线程才能访问该锁。此方案本质上没法进行调整,所以备份的请求会不断增加。随着时间的推移,若是到达率不下降,客户端请求就会开始超时,进而发 生灾难性后果。
众 所周知,在锁中是经过公平性对锁进行保护的。缘由在于在锁原本已经可用的时间段内,锁被人为封闭,使获得达的线程必须等待,直到所选锁的拥有者线程可以唤 醒、切换上下文以及得到和释放该锁为止。为解决这种问题,Windows 已逐渐将全部内部锁都改成不公平锁,并且 CLR 监视器也是不公平的。
对于这种有关保护的基本问题,惟一的有效解决方案是减小锁持有时间并分解系统以尽量减小热锁(若是有的话)。虽说起来容易作起来难,但这对于可伸缩性来讲仍是很是重要的。
“蜂拥”是指大量线程被唤醒,使得它们所有同时从 Windows 线程计划程序争夺关注点。例如,若是在单个手动设置事件中有 100 个阻塞的线程,而您设置该事件…嗯,算了吧,您极可能会把事情弄得一团糟,特别是当其中的大部分线程都必须再次等待时。
实 现阻塞队列的一种途径是使用手动设置事件,当队列为空时变为无信号而在队列非空时变为有信号。遗憾的是,若是从零个元素过渡到一个元素时存在大量正在等待 的线程,则可能会发生蜂拥。这是由于只有一个线程会获得此单一元素,此过程会使队列变空,从而必须重置该事件。若是有 100 个线程在等待,那么其中的 99 个将被唤醒、切换上下文(致使全部缓存丢失),全部这些换来的只是不得再也不次等待。
两步舞曲
有时您须要在持有锁的状况下通知一个事件。若是唤醒的线程须要得到被持有的锁,则这可能会很不凑巧,由于在它被唤醒后只是发现了它必须再次等待。这样作很是浪费资源,并且会增长上下文切换的总数。此状况称为两步舞曲,若是涉及到许多锁和事件,可能会远远超出两步的范畴。
Win32 和 CLR 的条件变量支持在本质上都会遇到两步舞曲问题。它一般是不可避免的,或者很难解决。
两 步舞曲问题在单处理器计算机上状况更糟。在涉及到事件时,内核会将优先级提高应用到唤醒的线程。这几乎能够保证抢先占用线程,使其可以在有机会释放锁以前 设置事件。这是在极端状况下的两步舞曲,其中设置 ThreadA 已切换出上下文,使得唤醒的 ThreadB 能够尝试得到锁;固然它没法作到,所以它将进行上下文切换以使 ThreadA 可再次运行;最终,ThreadA 将释放锁,这将再次提高 ThreadB 的优先级,使其优先于 ThreadA,以便它可以运行。如您所见,这涉及了屡次无用的上下文切换。
优先级反转
修改线程优先级经常是自找苦吃。当不一样优先级的许多线程共享对一样的锁和资源的访问权时,可能会发生优先级反转,即较低优先级的线程实际无限期地阻止较高优先级线程的进度。这个示例所要说明的道理就是尽量避免更改线程优先级。
下 面是一个优先级反转的极端示例。假设低优先级的 ThreadA 得到某个锁 L。随后高优先级的 ThreadB 介入。它尝试得到 L,但因为 ThreadA 占用使得它没法得到。下面就是“反转”部分:好像 ThreadA 被人为临时赋予了一个高于 ThreadB 的优先级,这一切只是由于它持有 ThreadB 所需的锁。
当 ThreadA 释放了锁后,此状况最终会自行解决。遗憾的是,若是涉及到中等优先级的 ThreadC,设想一下会发生什么状况。虽然 ThreadC 不须要锁 L,但它的存在可能会从根本上阻止 ThreadA 运行,这将间接地阻止高优先级 ThreadB 的运行。
最 终,Windows Balance Set Manager 线程会注意到这一状况。即便 ThreadC 保持永远可运行状态,ThreadA 最终(四秒钟后)也将接收到操做系统发出的临时优先级提高指令。希望这足以使其运行完毕并释放锁。但这里的延迟(四秒钟)至关巨大,若是涉及到任何用户界 面,则应用程序用户确定会注意到这一问题。
实现安全性的模式
现 在我已经找出了一个又一个的问题,好消息是我这里还有几种设计模式,您能够遵循它们来下降上述问题(尤为是正确性危险)的发生频率。大多数问题的关键是由 于状态在多个线程之间共享。更糟的是,此状态可被随意控制,可从一致状态转换为不一致状态,而后(希望)又从新转换回来,具备使人惊讶的规律性。
当开发人员针对单线程程序编写代码时,全部这些都很是有用。在您向最终的正确目标迈进的过程当中,极可能会使用共享内存做为一种暂存器。多年来 C 语言风格的命令式编程语言一直使用这种方式工做。
但随着并发现象愈来愈多,您须要对这些习惯密切加以关注。您能够按照 Haskell、LISP、Scheme、ML 甚至 F#(一种符合 .NET 的新语言)等函数式编程语言行事,即采用不变性、纯度和隔离做为一类设计概念。
不变性
具备不变性的数据结构是指在构建后不会发生改变的结构。这是并发程序的一种奇妙属性,由于若是数据不改变,则即便许多线程同时访问它也不会存在任何冲突风险。这意味着同步并非一个须要考虑的因素。
不 变性在 C++ 中经过 const 提供支持,在 C# 中经过只读修饰符支持。例如,仅具备只读字段的 .NET 类型是浅层不变的。默认状况下,F# 会建立固定不变的类型,除非您使用可变修饰符。再进一步,若是这些字段中的每一个字段自己都指向字段均为只读(并仅指向深层不可变类型)的另外一种类型,则该 类型是深层不可变的。这将产生一个保证不会改变的完整对象图表,它会很是有用。
所 有这一切都说明不变性是一个静态属性。按照惯例,对象也能够是固定不变的,即在某种程度上能够保证状态在某个时间段不会改变。这是一种动态属性。 Windows Presentation Foundation (WPF) 的可冻结功能刚好可实现这一点,它还容许在不一样步的状况下进行并行访问(可是没法以处理静态支持的方式对其进行检查)。对于在整个生存期内须要在固定不变 和可变之间进行转换的对象来讲,动态不变性一般很是有用。
不变性也存在一些弊端。只要有内容须要改变,就必须生成原始对象的副本并在此过程当中应用更改。另外,在对象图表中一般没法进行循环(除动态不变性外)。
例如,假设您有一个 ImmutableStack<T>,如图 4 所示。您须要从包含已应用更改的对象中返回新的 ImmutableStack<T> 对象,而不是一组变化的 Push 和 Pop 方法。在某些状况下,能够灵活使用一些技巧(与堆栈同样)在各实例之间共享内存。
图 4 使用 ImmutableStack
public class ImmutableStack<T> { private readonly T m_value; private readonly ImmutableStack<T> m_next; private readonly bool m_empty; public ImmutableStack() { m_empty = true; } internal ImmutableStack(T value, Node next) { m_value = value; m_next = next; m_empty = false; } public ImmutableStack<T> Push(T value) { return new ImmutableStack(value, this); } public ImmutableStack<T> Pop(out T value) { if (m_empty) throw new Exception("Empty."); return m_next; } }
节点被推入时,必须为每一个节点分配一个新对象。在堆栈的标准连接列表实现中,必须执行此操做。可是要注意,当您从堆栈中弹出元素时,可使用现有的对象。这是由于堆栈中的每一个节点是固定不变的。
固定不变的类型无处不在。CLR 的 System.String 类是固定不变的,还有一个设计指导原则,即全部新值类型都应是固定不变的。此处给出的指导原则是在可行和合适的状况下使用不变性并抵抗执行变化的诱惑,而最新一代的语言会使其变得很是方便。
纯度
即 使是使用固定不变的数据类型,程序所执行的大部分操做还是方法调用。方法调用可能存在一些反作用,它们在并发代码中会引起问题,由于反作用意味着某种形式 的变化。一般这只是表示写入共享内存,但它也多是实际变化的操做,如数据库事务、Web 服务调用或文件系统操做。在许多状况下,我但愿可以调用某种方法,而又没必要担忧它会致使并发危险。有关这一方面的一些很好示例就是 GetHashCode 和 ToString on System.Object 等简单的方法。不少人都不但愿它们带来反作用。
纯方法始终均可以在并发设置中运行,而无需添加同步。尽管纯度没有任何常见语言支持,但您能够很是简单地定义纯方法:
它只从共享内存读取,而且只读取不变状态或常态。
它必须可以写入局部变量。
它能够只调用其余纯方法。
因 此,纯方法能够实现的功能很是有限。但当与不变类型结合使用时,纯度就会成为可能并且很是方便。一些函数式语言默认状况下都采用纯度,特别是 Haskell,它的全部内容都是纯的。任何须要执行反作用的内容都必须封装到一个被称为 monad 的特殊内容中。可是咱们中的多数人都不使用 Haskell,所以咱们必须遵守纯度约定。
隔离
前面咱们只是简单说起了发布和私有化,但它们却击中了一个很是重要的问题的核心。因为状态一般在多个线程之间共享,所以同步是必不可少的(不变性和纯度也颇有趣味)。但若是状态被限制在单个线程内,则无需进行同步。这会致使软件在本质上更具伸缩性。
实 际上,若是状态是隔离的,则能够自由变化。这很是方便,由于变化是大部分 C 风格语言的基本内置功能。程序员已习惯了这一点。这须要进行训练以便可以在编程时以函数式风格为主,对大多数开发人员来讲这都至关困难。尝试一下,但不要 自欺欺人地认为世界会在一晚上之间改成使用函数式风格编程。
所 有权是一件很难跟踪的事情。对象是什么时候变为共享的?在初始化时,这是由单线程完成的,对象自己还不能从其余线程访问。将对某个对象的引用存储在静态变量 中、存储在已在线程建立或排列队列时共享的某个位置或存储在可从其中的某个位置传递性访问的对象字段中以后,该对象就变为共享对象。开发人员必须特别关注 私有与共享之间的这些转换,并当心处理全部共享状态。
Joe Duffy 在 Microsoft 是 .NET 并行扩展方面的开发主管。他的大部分时间都在攻击代码、监督库的设计以及管理梦幻开发团队。他的最新著做是《Concurrent Programming on Windows》。