admin 管理员组

文章数量: 1086019


2024年3月13日发(作者:memorystream转filestream result)

并发危险

解决多线程代码中的 11 个常见的

问题

Joe Duffy

本文将介绍以下内容:

本文使用了以下技术:

基本并发概念

多线程、.NET Framework

并发问题和抑制措

实现安全性的模式

横切概念

目录

数据争用

忘记同步

粒度错误

读写撕裂

无锁定重新排序

重新进入

死锁

锁保护

戳记

两步舞曲

优先级反转

实现安全性的模式

不变性

纯度

隔离

并发现象无处不在。

服务器端程序长久以来都必须负责处理基本并发编程模型,而随着多核处理器

的日益普及,客户端程序也将需要执行一些任务。随着并发操作的不断增加,有关确保安全的问题也

浮现出来。也就是说,在面对大量逻辑并发操作和不断变化的物理硬件并行性程度时,程序必须继续

保持同样级别的稳定性和可靠性。

与对应的顺序代码相比,正确设计的并发代码还必须遵循一些额外的规则。对内存的读写以及对共享

资源的访问必须使用同步机制进行管制,以防发生冲突。另外,通常有必要对线程进行协调以协同完

成某项工作。

这些附加要求所产生的直接结果是,可以从根本上确保线程始终保持一致并且保证其顺利向前推进。

同步和协调对时间的依赖性很强,这就导致了它们具有不确定性,难于进行预测和测试。

这些属性之所以让人觉得有些困难,只是因为人们的思路还未转变过来。没有可供学习的专门 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++ 编译包括三个独立的

步骤:

1. 将当前值从共享的 s_curr 变量读入处理器寄存器。

2. 递增该寄存器。

3. 将寄存器值重新写入共享 s_curr 变量。

按照这种顺序执行的两个线程可能会在本地从 s_curr 读取了相同的值(比如 42)并将其递增到某个

值(比如 43),然后发布相同的结果值。这样一来,GetNext 将为这两个线程返回相同的数字,导

致算法中断。虽然简单语句 s_curr++ 看似不可分割,但实际却并非如此。

忘记同步

这是最简单的一种数据争用情况:同步被完全遗忘。这种争用很少有良性的情况,也就是说虽然它们

是正确的,但大部分都是因为这种正确性的根基存在问题。

这种问题通常不是很明显。例如,某个对象可能是某个大型复杂对象图表的一部分,而该图表恰好可

使用静态变量访问,或在创建新线程或将工作排入线程池时通过将某个对象作为闭包的一部分进行传

递可变为共享图表。

当对象(图表)从私有变为共享时,一定要多加注意。这称为发布,在后面的隔离上下文中会对此加

以讨论。反之称为私有化,即对象(图表)再次从共享变为私有。

对这种问题的解决方案是添加正确的同步。在计数器示例中,我可以使用简单的联锁:

static class Counter {

internal static volatile int s_curr = 0;

internal static int GetNext() {

return ent(ref s_curr);


本文标签: 并发 线程 进行