并发危险:解决多线程代码中的 11 个常见的问题

合集下载

编程技术中常见的并发问题分析与解决方法

编程技术中常见的并发问题分析与解决方法

编程技术中常见的并发问题分析与解决方法在当今科技发展迅猛的时代,编程技术已经成为了各行各业都无法绕过的一项重要技能。

然而,在编写复杂的程序时,我们常常会遇到并发问题。

并发问题是指在多个线程或进程同时执行时可能出现的冲突和竞争条件。

本文将分析并发问题的常见类型,并讨论解决这些问题的方法。

首先,让我们来看看最常见的并发问题之一:竞态条件。

竞态条件是指多个线程或进程在访问共享资源时,由于执行顺序不确定而导致的结果不可预测的情况。

例如,当多个线程同时对一个变量进行自增操作时,由于线程执行的顺序不确定,最终的结果可能会出现错误。

为了解决这个问题,我们可以使用互斥锁来保证在任意时刻只有一个线程能够访问共享资源。

互斥锁可以通过加锁和解锁操作来实现,确保每个线程在访问共享资源之前都能够获得锁。

这样就能够避免多个线程同时访问共享资源而导致的竞态条件。

另一个常见的并发问题是死锁。

死锁是指多个线程或进程在等待对方释放资源的情况下无法继续执行的现象。

这种情况通常发生在多个线程同时持有某些资源,并且都在等待对方释放资源。

为了解决死锁问题,我们可以使用资源分级和有序申请的方法。

资源分级是指将资源按照优先级进行划分,每个线程在申请资源时必须按照一定的顺序进行,这样可以避免循环等待的情况。

有序申请是指每个线程在申请资源时必须按照一定的顺序进行,例如,线程A先申请资源X,然后再申请资源Y,而线程B必须按照相反的顺序申请资源Y和X。

这样可以避免多个线程同时持有不同的资源而导致的死锁。

除了竞态条件和死锁,还有一个常见的并发问题是活锁。

活锁是指多个线程或进程在尝试解决冲突时,由于执行顺序不当而导致无法取得进展的情况。

活锁通常发生在多个线程或进程都在尝试解决冲突,但是每次解决冲突后又会重新出现冲突的情况下。

为了解决活锁问题,我们可以使用随机等待和退避策略。

随机等待是指在解决冲突时引入一定的随机性,例如,在等待其他线程释放资源时,可以引入一个随机的等待时间,以避免多个线程同时尝试解决冲突而导致的活锁。

多线程编程的基本问题和解决方法

多线程编程的基本问题和解决方法

多线程编程的基本问题和解决方法多线程编程是现代计算机语言中的一个重要特性。

多线程编程允许多个线程同时执行,提高了计算机的资源利用率,提高了程序的并发性和性能。

但是,多线程编程也面临着一些基本问题,如共享内存、竞争条件、死锁和饥饿等问题。

为了解决这些问题,开发人员需要采用适当的方法和技术来保证多线程程序的正确性和性能。

共享内存问题当多个线程同时访问共享内存时,就会发生共享内存问题。

如果多个线程同时对共享内存进行读写操作,就会出现数据不一致的现象。

这是因为线程之间没有协调方案,同时访问同一个内存单元,产生竞争条件。

为了避免共享内存问题,可以采用同步机制,如互斥锁、条件变量等。

这些机制可以确保在一个时刻只能有一个线程能够访问共享内存,从而避免数据不一致的情况。

竞争条件问题竞争条件是多线程编程中的另一个重要问题。

当多个线程尝试同时访问同一个资源时,就会出现竞争条件。

这种竞争可能会导致程序出现错误或不一致。

竞争条件的解决方法是使用同步机制,如互斥锁、条件变量等。

这些机制可以确保在一个时刻只有一个线程能够访问资源,从而避免竞争条件的出现。

死锁问题死锁是多线程编程中的另一个常见问题。

当多个线程需要获取多个锁时,就可能会出现死锁。

死锁是一种状态,其中两个或更多的线程在等待对方释放必要的资源,无法继续执行。

为了避免死锁,可以采用避免、预防和遥测措施。

避免死锁是一种保证程序的正确性的常见方法,可以通过避免循环等待等措施来实现。

饥饿问题饥饿是多线程编程中的另一个常见问题。

当一个或多个线程永远无法获取所需的资源时,就会出现饥饿问题。

这种情况可能会导致程序产生错误或失效。

为了避免饥饿问题,可以采用公平性和优先级算法。

公平性算法确保每个线程都有机会获得资源,而优先级算法可以确保优先级高的线程得到更好的资源。

结论多线程编程是现代计算机语言中的一个重要特性,但同时也面临着一些基本问题。

共享内存、竞争条件、死锁和饥饿等问题,需要通过适当的方法和技术来解决。

C多线程编程解析并发编程的挑战与解决方案

C多线程编程解析并发编程的挑战与解决方案

C多线程编程解析并发编程的挑战与解决方案多线程编程是一种并发编程的方式,可以同时执行多个线程,提高程序的执行效率。

然而,并发编程也带来了一些挑战,例如线程安全、死锁等问题。

本文将对C多线程编程中的并发编程挑战进行解析,并探讨一些解决方案。

一、线程安全在多线程编程中,多个线程共享同一个资源,可能会导致数据竞争的问题。

数据竞争是指多个线程同时对同一数据进行读写操作,导致数据不一致或者损坏。

为了确保线程安全,需要使用同步机制来保护共享资源。

常见的同步机制有互斥锁、条件变量、信号量等。

互斥锁是最常用的同步机制,可以保证同时只有一个线程能够访问共享资源。

通过在访问共享资源前加锁,在访问完成后释放锁,可以避免多个线程同时对同一资源进行写操作。

条件变量可以用于线程之间的通信,可以阻塞和唤醒线程。

信号量也可以用于线程之间的同步和互斥。

二、死锁死锁是指两个或多个线程互相等待对方释放资源,导致程序无法继续执行的情况。

死锁是并发编程中常见的问题之一。

为了避免死锁的发生,可以采取以下几种措施:1. 保持资源的有序性:按照相同的顺序请求资源,避免循环等待。

2. 设置超时机制:如果某个资源在一定时间内未获得,则释放已有资源,避免等待时间过长导致死锁。

3. 死锁检测与恢复:通过资源分配图等方式进行死锁检测,并主动释放资源解除死锁状态。

4. 避免使用过多的锁:减少锁的使用可以减少死锁的发生概率。

三、内存管理多线程编程中,每个线程拥有自己的堆栈,但共享同一地址空间,可能导致多个线程同时修改同一块内存区域,造成内存损坏或数据不一致。

为了解决内存管理的问题,可以采取以下措施:1. 使用局部变量:将需要共享的数据尽量定义为局部变量,避免多个线程同时访问同一块内存区域。

2. 使用原子操作:原子操作是不可中断的操作,可以保证多个线程同时访问同一块内存区域时的数据一致性。

3. 使用内存屏障:内存屏障可以保证多线程的操作按照指定的顺序进行,避免数据访问的乱序性。

简述并发操作可能带来的问题及解决方法

简述并发操作可能带来的问题及解决方法

如何解决并发操作可能带来的问题在计算机科学领域,当多个计算机程序同时访问和操作共享的资源时,可能会出现并发操作的问题。

这些问题包括但不限于数据竞争、死锁、饥饿等,对系统的性能和可靠性产生负面影响。

了解并发操作可能带来的问题,并掌握解决这些问题的方法,对于开发高质量的软件系统至关重要。

1. 数据竞争数据竞争是指当多个线程同时访问和修改共享的数据时,可能出现的不确定行为。

这种情况下,程序的输出结果可能会因为线程执行顺序的不确定性而产生变化。

为了解决数据竞争问题,我们可以采用以下方法:- 使用锁机制:通过对共享资源进行加锁和解锁操作,确保在任意时刻只有一个线程可以访问该资源,从而避免数据竞争的发生。

- 使用原子操作:原子操作是不可分割的操作,可以保证多个线程对共享资源的并发访问不会导致数据竞争,常见的原子操作包括CAS (比较并交换)和原子类型等。

2. 死锁死锁是指多个线程因为相互等待对方持有的资源而陷入僵局的情况。

为了避免死锁的发生,我们可以采用以下方法:- 设置资源申请的超时机制:当线程申请资源的等待时间超过一定阈值时,自动放弃对资源的申请,避免长时间等待导致死锁的发生。

- 预防死锁:通过对资源的合理分配和加锁顺序的规定,避免不同线程持有资源的交叉等待现象,从而预防死锁的发生。

3. 饥饿饥饿是指某个线程因为种种原因无法获得所需的资源而无法继续执行的情况。

为了解决饥饿问题,我们可以采用以下方法:- 公平性调度:通过公平的调度算法来确保每个线程都有公平的机会获得所需的资源,避免某个线程长期处于无法获取资源的状态。

- 优先级调度:给予优先级较高的线程更高的执行机会,确保重要任务能够及时获得所需的资源。

了解并发操作可能带来的问题,并掌握解决这些问题的方法,对于开发高质量、可靠性的软件系统至关重要。

通过合理设计并发控制机制,可以有效地避免数据竞争、死锁和饥饿等问题的发生,提升系统的性能和可靠性。

个人观点:在处理并发操作可能带来的问题时,需要充分考虑系统设计和架构,采用合适的并发控制技术,并充分测试和验证系统的并发性能。

编程技术中常见的并发安全问题及其解决方案

编程技术中常见的并发安全问题及其解决方案

编程技术中常见的并发安全问题及其解决方案随着互联网的快速发展,编程技术在我们的生活中扮演着越来越重要的角色。

然而,并发安全问题是编程中常见的挑战之一。

在多线程或分布式系统中,当多个线程同时访问共享资源时,可能会导致数据不一致或竞态条件的问题。

本文将探讨一些常见的并发安全问题,并提供相应的解决方案。

1. 竞态条件竞态条件是指多个线程在访问和操作共享资源时,由于执行顺序的不确定性而导致的问题。

例如,当两个线程同时读取并增加一个计数器时,由于读取和写入操作不是原子性的,可能导致计数器的值不正确。

解决方案之一是使用互斥锁。

互斥锁可以确保在任意时刻只有一个线程可以访问共享资源。

通过在访问共享资源之前获取锁,并在访问完成后释放锁,可以避免竞态条件的发生。

2. 死锁死锁是指两个或多个线程相互等待对方释放资源而无法继续执行的情况。

当多个线程同时获取多个资源,并按照特定的顺序来获取资源时,可能会发生死锁。

解决死锁问题的一种方法是使用资源分级。

通过对资源进行排序,并要求线程按照相同的顺序获取资源,可以避免死锁的发生。

另外,还可以使用超时机制,在一定时间内无法获取到资源时,线程可以放弃当前的资源请求,避免死锁的发生。

3. 数据竞争数据竞争是指多个线程同时访问共享数据,并且至少有一个线程对共享数据进行了写操作。

在没有适当的同步机制的情况下,数据竞争可能导致未定义的行为和结果。

解决数据竞争问题的一种方法是使用原子操作。

原子操作是指不可中断的操作,可以确保在多线程环境下对共享数据的读写是原子性的。

通过使用原子操作,可以避免数据竞争的发生。

4. 内存一致性问题在分布式系统中,不同的计算节点可能拥有自己的本地缓存,这可能导致内存一致性问题。

当一个计算节点对共享数据进行修改时,其他计算节点可能无法立即看到这些修改。

解决内存一致性问题的一种方法是使用同步原语。

同步原语可以确保对共享数据的修改在所有计算节点之间是可见的。

例如,可以使用分布式锁来保证对共享数据的访问是串行化的,从而避免内存一致性问题。

多线程编程的常见问题和解决方法

多线程编程的常见问题和解决方法

多线程编程的常见问题和解决方法多线程编程是同时运行多个线程的编程模型,可以提高程序的并发性和响应性。

然而,多线程编程也会带来一些常见问题,如竞态条件、死锁、活锁、饥饿等。

下面是一些常见的问题和解决方法。

1.竞态条件竞态条件是指多个线程对共享资源进行访问和修改时的不确定性结果。

解决竞态条件的方法有:-使用互斥锁(mutex):通过确保一次只有一个线程能够访问共享资源,来避免竞态条件。

-使用信号量(semaphore):通过限制同时访问共享资源的线程数量来避免竞态条件。

-使用条件变量(condition variable):通过让线程等待某个条件满足,再进行访问共享资源,来避免竞态条件。

2.死锁死锁是指多个线程互相等待对方释放资源,导致系统无法继续执行的状态。

解决死锁的方法有:-避免使用多个锁:尽可能减少锁的数量,或者使用更高级的同步机制如读写锁(read-write lock)。

-破坏循环等待条件:对资源进行排序,按序请求资源,避免循环等待。

-使用超时机制:在一定时间内等待资源,如果超时则丢弃请求,避免无限等待。

3.活锁活锁是指多个线程在不停地改变自己的状态,但无法向前推进。

解决活锁的方法有:-引入随机性:当多个线程同时请求资源时,引入随机性来打破死锁的循环。

-重试策略:如果发生活锁,暂停一段时间后重新尝试执行操作。

4.饥饿饥饿是指某个线程由于优先级或其他原因无法获得资源,导致无法继续执行。

解决饥饿的方法有:-使用公平锁:确保每个线程获得资源的机会是公平的,避免某个线程一直无法获得资源。

-调整线程优先级:提高饥饿线程的优先级,使其有机会获得资源。

5.数据竞争数据竞争是指多个线程同时对共享数据进行读写操作,导致不确定的结果。

解决数据竞争的方法有:-使用互斥锁:通过确保一次只有一个线程能够访问共享数据,来避免数据竞争。

-使用原子操作:使用原子操作来保证共享数据的原子性,避免数据竞争。

6.上下文切换开销多线程编程会引入上下文切换开销,导致性能下降。

Java并发编程中的常见问题与解决方案

Java并发编程中的常见问题与解决方案

Java并发编程中的常见问题与解决方案Java并发编程已经成为了Java开发者必备的技能之一。

然而,在实际的开发中,往往会遇到各种各样的问题,这些问题不仅会影响到程序的性能和可靠性,还可能引发严重的安全问题。

本文将介绍Java并发编程中的常见问题和解决方案,帮助开发者提高代码质量和效率。

一、线程安全问题在多线程程序中,线程安全问题是最普遍的问题。

当多个线程同时访问同一个共享资源时,就有可能发生竞态条件,也就是多个线程对同一个资源进行非原子操作,导致程序出现错误。

例如,在下面这段代码中:```javapublic class Counter {private int count;public synchronized void increment() {count ++;}public synchronized int getCount() {return count;}}public class Main {public static void main(String[] args) {Counter counter = new Counter();for (int i = 0; i < 10; i++) {new Thread(() -> {for (int j = 0; j < 1000; j++) {counter.increment();}}).start();}System.out.println("Count: " + counter.getCount());}}```这段代码用了一个计数器来统计所有线程对其进行increment()操作后的结果。

这里使用了synchronized关键字来保证increment()和getCount()方法的原子性。

但是,当计数器被多个线程访问时,即使使用了同步关键字,也可能会出现线程安全问题。

为了解决这个问题,可以使用Java的并发包中的AtomicInteger 类,它提供了一系列操作方法,保证了多线程间对整数变量的原子操作。

多线程编程的挑战和解决方案

多线程编程的挑战和解决方案

多线程编程的挑战和解决方案多线程编程是一种并发编程的方式,可以同时执行多个线程,提高程序的执行效率和资源利用率。

然而,多线程编程也会面临一些挑战,例如线程安全、调度和同步等问题。

为了解决这些挑战,我们可以采取一系列的解决方案。

首先,线程安全是多线程编程中最重要的挑战之一。

多个线程同时访问共享的数据很容易引发竞态条件,导致数据的不一致性。

为了解决线程安全问题,我们可以采取以下几种方案:1.使用锁机制:通过使用互斥锁(mutex)或读写锁(read-write lock),可以保证同一时间只有一个线程访问共享数据,从而避免竞态条件。

2.使用原子操作:原子操作是不可中断的操作,可以保证在执行期间不会有其他线程对共享数据进行修改。

在不需要复杂的同步逻辑的情况下,原子操作可以提高程序的执行效率。

3.使用线程局部存储(thread-local storage):线程局部存储可以保证每个线程都有自己独立的变量副本,从而避免多个线程之间的竞争。

除了线程安全,多线程编程还面临调度和同步的挑战。

调度问题涉及到多个线程之间的优先级和调度顺序,同步问题则涉及到线程之间的协作和通信。

1.设置线程优先级:在多线程编程中,可以为每个线程设置不同的优先级,优先级高的线程会被调度器优先执行。

通过合理设置线程优先级,可以调整程序的执行顺序。

2.使用条件变量(condition variable):条件变量是一种用于线程间同步和通信的机制。

通过条件变量,可以让线程在某个条件满足时等待,直到其他线程满足条件后唤醒它们继续执行。

3.使用信号量(semaphore):信号量是一种计数器,可用于控制对共享资源的访问。

通过使用信号量,可以限制同时访问共享资源的线程数量,从而避免资源争用问题。

此外,在多线程编程中,还可以使用线程池(thread pool)来管理线程的创建和销毁,避免频繁的线程创建和销毁操作带来的开销。

线程池通过维护一个线程队列,可以重用已创建的线程,从而降低线程创建和销毁的开销。

  1. 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
  2. 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
  3. 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。

并发危险:解决多线程代码中的11 个常见的问题并发危险解决多线程代码中的11 个常见的问题Joe Duffy目录数据争用忘记同步粒度错误读写撕裂无锁定重新排序重新进入死锁锁保护戳记两步舞曲优先级反转实现安全性的模式不变性纯度隔离并发现象无处不在。

服务器端程序长久以来都必须负责处理基本并发编程模型,而随着多核处理器的日益普及,客户端程序也将需要执行一些任务。

随着并发操作的不断增加,有关确保安全的问题也浮现出来。

也就是说,在面对大量逻辑并发操作和不断变化的物理硬件并行性程度时,程序必须继续保持同样级别的稳定性和可靠性。

与对应的顺序代码相比,正确设计的并发代码还必须遵循一些额外的规则。

对内存的读写以及对共享资源的访问必须使用同步机制进行管制,以防发生冲突。

另外,通常有必要对线程进行协调以协同完成某项工作。

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

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

这些属性之所以让人觉得有些困难,只是因为人们的思路还未转变过来。

没有可供学习的专门API,也没有可进行复制和粘贴的代码段。

实际上的确有一组基础概念需要您学习和适应。

很可能随着时间的推移某些语言和库会隐藏一些概念,但如果您现在就开始执行并发操作,则不会遇到这种情况。

本文将介绍需要注意的一些较为常见的挑战,并针对您在软件中如何运用它们给出一些建议。

首先我将讨论在并发程序中经常会出错的一类问题。

我把它们称为&#8220;安全隐患&#8221;,因为它们很容易发现并且后果通常比较严重。

这些危险会导致您的程序因崩溃或内存问题而中断。

当从多个线程并发访问数据时会发生数据争用(或竞争条件)。

特别是,在一个或多个线程写入一段数据的同时,如果有一个或多个线程也在读取这段数据,则会发生这种情况。

之所以会出现这种问题,是因为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 &lt; 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 &amp; 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。

无锁定重新排序有时编写无锁定代码来实现更好的可伸缩性和可靠性是一种非常诱人的想法。

这样做需要深入了解目标平台的内存模型(有关详细信息,请参阅VanceMorrison 的文章"Memory Models:Understand the Impact of Low-LockTechniques in Multithreaded Apps",网址为/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?看上去这个问题很可笑。

相关文档
最新文档