解决多线程中11个常见问题
- 格式:docx
- 大小:50.76 KB
- 文档页数:12
解决多线程编程中的资源竞争问题多线程编程中的资源竞争问题是指多个线程同时对共享资源进行读写操作而产生的冲突。
资源竞争问题会导致数据不一致、死锁等严重后果,并且在多核处理器上,资源竞争问题还可能导致性能瓶颈。
为了解决多线程编程中的资源竞争问题,我们可以采取以下几种策略。
1.锁机制锁机制是最常用的解决资源竞争问题的方式之一。
通过在多个线程对共享资源进行读写操作时,加锁来保证同一时间只有一个线程可以访问共享资源,从而避免资源竞争问题的发生。
常见的锁机制包括互斥锁、读写锁、自旋锁等。
使用锁机制需要注意锁的粒度,过细的粒度可能导致性能问题,而过粗的粒度可能无法充分利用多线程的并发性能。
2.同步机制除了锁机制,还可以使用同步机制来解决资源竞争问题。
同步机制可以通过信号量、条件变量等方式来实现线程间的协作,以保证共享资源被安全地访问。
例如,可以使用条件变量来实现线程的等待和唤醒,以此来解决生产者-消费者模型中的资源竞争问题。
3.原子操作原子操作是不可中断的操作,能够确保多个线程对共享资源的操作是原子的。
在多线程编程中,可以使用原子操作来替代锁机制,从而避免显式地加锁和解锁的开销。
原子操作通常由处理器提供支持,使用原子操作可以有效地减少资源竞争问题的发生。
4.适当的数据结构选择在多线程编程中,选择合适的数据结构也可以减少资源竞争问题的发生。
例如,可以使用线程安全的队列、哈希表等数据结构,这些数据结构内部会使用锁、原子操作等方式来保证线程的安全访问。
5.数据复制在某些场景下,可以使用数据复制的方式来避免资源竞争问题。
即将共享资源的副本分别分配给每个线程,每个线程操作自己的副本而不影响其他线程的操作。
这种方式虽然会增加内存开销,但可以大大地减少资源竞争问题的发生,提高程序的并发性能。
6.异步编程异步编程是一种避免资源竞争问题的有效方式。
通过将任务切换为事件驱动的方式执行,可以避免多个线程对共享资源进行读写操作的竞争。
多线程注意事项范文多线程是指在一个程序中同时运行多个线程,每个线程独立执行不同的任务。
相比单线程,多线程可以提高程序的执行效率和资源利用率。
然而,多线程编程也存在一些注意事项,下面将详细介绍:1.线程安全问题:多个线程同时访问共享的数据,可能引发竞态条件或死锁等问题。
为避免这些问题,可以采用锁、信号量、互斥量等机制来保护共享数据的访问。
2.同步问题:当多个线程并发执行时,可能会出现对共享资源的不同步访问。
为解决这个问题,可以使用线程同步机制,如条件变量、读写锁等,来保证多个线程按照特定的顺序访问共享资源。
3.上下文切换开销:切换线程间的上下文需要保存和恢复线程的状态信息,这会带来一定的开销。
因此,在多线程编程时,应避免频繁的线程切换,合理调度线程的执行顺序,以降低上下文切换的开销。
4.线程间通信问题:多个线程之间可能需要进行通信,传递数据或控制信息。
为确保线程间的正确通信,可以使用消息队列、管道、共享内存等机制来实现线程间的数据交换。
5.线程优先级问题:多线程环境中,线程的调度是由操作系统决定的,因此无法确定线程的执行顺序。
这就导致线程的执行结果可能与预期不符。
为避免这个问题,可以设置线程的优先级,提高重要线程的执行优先级。
6.死锁问题:多个线程之间的循环等待资源的释放,导致所有线程都无法继续执行,称为死锁。
为避免死锁问题,应避免循环等待的发生,可以按照特定的顺序申请和释放资源。
7.线程创建和销毁开销:创建和销毁线程需要消耗系统资源,因此应合理控制线程的数量,避免频繁的线程创建和销毁操作。
8.线程安全方法和非线程安全方法:在多线程环境中,一些方法可能是线程安全的,即多个线程同时调用不会引发竞态条件等问题。
而一些方法可能是非线程安全的,多个线程同时调用可能导致不确定的结果。
在多线程编程时,应注意选择线程安全的方法。
9.CPU资源的合理利用:多线程程序可能会占用过多的CPU资源,导致其他程序无法正常工作。
线程不安全的例子线程不安全是指在多线程环境下,对共享资源的访问没有进行合理的同步,导致多个线程之间的操作相互干扰,最终导致程序出现错误或不确定的结果。
下面将列举10个线程不安全的例子,并对其进行详细描述。
1. 多线程同时对同一个变量进行写操作:假设有一个全局变量count,多个线程同时对其进行自增操作。
由于自增操作不是原子性的,可能会出现多个线程同时读取到同一个值,然后各自自增,导致最终结果不正确。
2. 多线程同时对同一个数组进行写操作:假设有一个全局数组arr,多个线程同时向其中添加元素。
由于数组的添加操作涉及到数组的扩容,可能会导致多个线程同时修改数组的长度,导致数组越界或数据丢失。
3. 多线程同时对同一个文件进行写操作:假设有多个线程同时向同一个文件写入数据。
由于文件写入操作是磁盘IO操作,可能会导致多个线程同时写入同一个位置,导致文件数据错乱或丢失。
4. 多线程同时对同一个数据库进行写操作:假设有多个线程同时向同一个数据库插入数据。
由于数据库插入操作涉及到磁盘IO操作和事务的管理,可能会导致多个线程同时插入相同的数据,导致数据冗余或主键冲突。
5. 多线程同时对同一个缓存进行写操作:假设有多个线程同时向同一个缓存中存储数据。
由于缓存的写操作是内存操作,可能会导致多个线程同时写入同一个位置,导致数据覆盖或丢失。
6. 多线程同时对同一个队列进行写操作:假设有多个线程同时向同一个队列中添加元素。
由于队列的添加操作涉及到指针的移动,可能会导致多个线程同时修改指针的位置,导致队列数据错乱或丢失。
7. 多线程同时对同一个缓存区进行写操作:假设有多个线程同时向同一个缓存区写入数据。
由于缓存区的写操作是内存操作,可能会导致多个线程同时写入同一个位置,导致数据覆盖或丢失。
8. 多线程同时对同一个共享变量进行读写操作:假设有多个线程同时读取和修改同一个共享变量。
由于读写操作的执行顺序不确定,可能会导致读取到的数据不一致或逻辑错误。
线程池注意事项和常见问题线程池是一种常用的多线程编程技术,它可以提高程序的性能和稳定性。
但是,在使用线程池时,我们也需要注意一些问题和常见错误。
本文将介绍线程池注意事项和常见问题,帮助读者更好地使用线程池。
一、线程池注意事项1.线程池大小的选择线程池大小的选择需要根据实际情况进行调整。
如果线程池过小,可能会导致任务无法及时处理,从而影响程序的性能;如果线程池过大,可能会导致系统资源的浪费,从而影响程序的稳定性。
2.任务队列的选择任务队列的选择也需要根据实际情况进行调整。
如果任务队列过小,可能会导致任务无法及时处理,从而影响程序的性能;如果任务队列过大,可能会导致系统资源的浪费,从而影响程序的稳定性。
3.线程池的关闭线程池的关闭需要注意线程池中的任务是否已经全部完成。
如果线程池中还有未完成的任务,直接关闭线程池可能会导致任务丢失或者程序异常。
因此,在关闭线程池之前,需要等待所有任务都已经完成。
二、线程池常见问题1.线程池中的任务出现异常线程池中的任务出现异常可能会导致整个线程池崩溃。
因此,在编写任务时,需要注意异常处理,避免出现未处理的异常。
2.线程池中的任务阻塞线程池中的任务阻塞可能会导致线程池无法及时处理其他任务,从而影响程序的性能。
因此,在编写任务时,需要注意任务的执行时间,避免出现长时间阻塞的情况。
3.线程池中的任务过多线程池中的任务过多可能会导致系统资源的浪费,从而影响程序的稳定性。
因此,在使用线程池时,需要根据实际情况进行调整,避免出现任务过多的情况。
4.线程池中的线程过多线程池中的线程过多可能会导致系统资源的浪费,从而影响程序的稳定性。
因此,在使用线程池时,需要根据实际情况进行调整,避免出现线程过多的情况。
总之,线程池是一种常用的多线程编程技术,但是,在使用线程池时,我们也需要注意一些问题和常见错误。
只有正确地使用线程池,才能提高程序的性能和稳定性。
软件开发常见问题解决方案集锦软件开发是一个复杂而又充满挑战的过程。
在开发过程中,开发人员经常会遇到各种各样的问题。
这些问题可能涉及到编码、测试、部署等方面。
为了帮助开发人员更好地解决这些问题,本文将介绍一些常见问题的解决方案。
1. 内存泄漏内存泄漏是软件开发中常见的问题之一。
当程序分配内存后没有及时释放,导致内存资源无法再次被使用时,就会出现内存泄漏。
解决内存泄漏问题的一个常用方法是使用垃圾回收机制。
垃圾回收机制可以自动检测和释放不再使用的内存,从而避免内存泄漏的问题。
2. 并发问题在多线程编程中,经常会遇到并发问题。
例如,多个线程同时访问共享资源可能会导致数据不一致的情况。
为了解决并发问题,可以使用锁机制来控制对共享资源的访问。
锁机制可以确保同一时间只有一个线程可以访问共享资源,从而避免数据不一致的问题。
3. 性能问题性能问题是软件开发中常见的挑战之一。
当软件运行速度较慢或者消耗过多的系统资源时,就会出现性能问题。
解决性能问题的一个方法是进行性能优化。
性能优化可以通过对代码进行优化,减少不必要的计算和IO操作,提高代码的执行效率。
此外,还可以使用缓存技术来减少对数据库等资源的访问,从而提高系统的响应速度。
4. 安全问题安全问题是软件开发中必须重视的问题之一。
在开发过程中,需要考虑如何保护用户的隐私和数据安全。
为了解决安全问题,可以采用一些安全措施,例如加密算法、访问控制和身份验证等。
此外,还可以进行安全测试,发现并修复潜在的安全漏洞,提高系统的安全性。
5. 兼容性问题兼容性问题是软件开发中常见的问题之一。
当软件在不同的操作系统、浏览器或设备上运行时,可能会出现兼容性问题。
为了解决兼容性问题,可以进行兼容性测试,发现并修复不同平台上的兼容性问题。
此外,还可以使用一些跨平台开发工具和框架,简化兼容性测试和开发工作。
总结:软件开发中常见的问题有内存泄漏、并发问题、性能问题、安全问题和兼容性问题等。
为了解决这些问题,开发人员可以采用一些常用的解决方案,例如使用垃圾回收机制解决内存泄漏问题,使用锁机制解决并发问题,进行性能优化解决性能问题,采用安全措施解决安全问题,进行兼容性测试解决兼容性问题等。
解决线程安全问题的三种方法线程安全问题是多线程编程中常见的难题,常常出现在多线程访问共享资源时。
下面列举了三种解决线程安全问题的方法:1. 同步代码块同步代码块可以控制多线程对共享资源的访问,同步代码块要求多个线程使用同一个锁对象,每次只能有一个线程执行同步代码块中的代码。
当一个线程进入同步代码块后,其他线程只能等待锁释放后才能进入同步代码块。
同步代码块的格式如下:```synchronized(锁对象){// 需同步的代码块}```也可以使用这样的方式,将整个方法加锁:```public synchronized void method(){// 需同步的方法体}```2. 同步方法同步方法是指使用synchronized修饰的方法,其效果相当于使用同步代码块的方式。
当一个线程进入同步方法后,其他线程只能等待该线程执行完毕后才能进入该方法。
同步方法的格式如下:```public synchronized void method(){// 需同步的方法体}```3. 使用线程安全的类Java中提供了线程安全的类,例如Vector、Hashtable、ConcurrentHashMap等。
这些类在实现时,考虑了多线程并发访问的问题,并且采用了不同的方式进行线程安全控制。
在开发时,尽量使用这些线程安全的类,可以有效避免线程安全问题。
总结线程安全问题是多线程编程中常见的问题,可以采用同步代码块、同步方法、使用线程安全的类等方式来解决。
在使用这些方法时,需要根据实际情况选择最适合的方法,避免死锁问题和性能问题。
此外,还应该加强对多线程编程原理和相关知识的了解和学习,提高代码水平和应对线程安全问题的能力。
多线程编程中的同步和并发问题解析在多线程编程中,同步和并发是两个关键的概念,主要涉及到多个线程之间的协同工作和共享资源的管理。
了解和解决同步和并发问题是保证多线程程序正确执行的关键。
一、同步问题同步问题是指多个线程之间的协作和按照一定的顺序执行。
在多线程编程中,可能会遇到以下几种同步问题:1.竞态条件(Race Condition):竞态条件是指多个线程竞争共享资源导致的问题。
当多个线程对同一共享资源进行读写操作时,可能会出现不可预期的结果。
例如,一个线程在读取共享资源的同时,另一个线程可能在修改这个资源,导致读取的结果不正确。
解决竞态条件的常见方法是使用互斥锁(Mutex)来保证对共享资源的排他访问,确保同一时间只有一个线程能够对共享资源进行操作。
2.死锁(Deadlock):死锁是指多个线程互相等待对方释放资源导致的无法继续执行的情况。
当多个线程都在等待对方释放资源时,将无法继续执行下去,形成死锁。
解决死锁问题的方法可以使用资源分级策略,即按照一定的顺序请求资源,释放资源也按照相反的顺序进行。
这样能够避免多个线程同时请求相同的资源,从而降低死锁的可能性。
3.饥饿(Starvation):饥饿是指某个线程由于资源被其他优先级高的线程占用而无法获得所需的资源,无法继续执行的情况。
解决饥饿问题的方法可以使用公平调度策略,即按照请求的先后顺序分配资源,避免某个线程长时间无法获得资源的情况。
二、并发问题并发问题是指多个线程同时执行,可能会导致不可预期的结果。
在多线程编程中,可能会遇到以下几种并发问题:1.数据竞争(Data Race):数据竞争是指多个线程同时读写共享数据导致的问题。
当多个线程对同一数据进行读写操作时,可能会出现不一致的结果。
例如,一个线程正在写入数据,同时另一个线程正在读取这个数据,导致读取的结果不正确。
解决数据竞争问题的常见方法是使用原子操作(Atomic Operation)或者互斥锁来保证对共享数据的原子性操作,确保多个线程对数据的访问不会出现冲突。
线程死锁的四个必要条件在多线程编程中,线程死锁是一种常见的问题。
它指的是两个或多个线程互相等待对方释放资源而陷入的一种僵局。
线程死锁的出现会导致程序无法继续执行,造成严重的影响。
为了避免线程死锁的出现,我们需要了解它的四个必要条件。
1. 互斥条件互斥条件指的是线程在执行时所需要的资源必须是排他性的,即不能同时被多个线程占用。
如果多个线程同时占用了同一个资源,那么就会出现资源竞争的问题,从而导致死锁的出现。
解决方法:可以通过使用锁来实现资源的互斥访问,使得同一时间只有一个线程能够访问该资源。
2. 请求与保持条件请求与保持条件指的是线程在执行时会请求一些其他线程所占用的资源,并且保持自己持有的资源不释放。
如果多个线程同时持有自己的资源并请求其他线程的资源,那么就会出现死锁的情况。
解决方法:可以通过一次性获取所有需要的资源来避免请求与保持条件的出现,或者在获取资源之前先释放已有的资源。
3. 不剥夺条件不剥夺条件指的是线程在执行时所持有的资源不能被其他线程剥夺,只能由持有该资源的线程自行释放。
如果一个线程持有了某个资源而不释放,其他线程无法剥夺该资源,就会出现死锁的情况。
解决方法:可以通过设置优先级或者时间限制等方式来避免不剥夺条件的出现。
4. 循环等待条件循环等待条件指的是多个线程之间形成了一个循环等待的环路,每个线程都在等待下一个线程所持有的资源。
如果该环路中的所有线程都不释放自己所持有的资源,那么就会出现死锁的情况。
解决方法:可以通过破坏环路来避免循环等待条件的出现,比如按照资源的编号来获取资源,或者按照一定的顺序获取资源。
线程死锁的出现需要满足以上四个条件,只要破坏其中任意一个条件就可以避免死锁的出现。
在进行多线程编程时,需要注意线程之间的资源访问问题,避免出现死锁的情况。
多线程注意事项多线程是指在一个程序中同时运行多个线程,每个线程独立执行不同的任务。
多线程的使用可以提高程序的性能和响应速度,但同时也需要注意一些问题和注意事项。
1. 线程安全性:在多线程编程中,线程与线程之间共享同一块内存空间,因此需要关注线程安全性。
如果多个线程同时访问和修改同一份数据,可能会导致数据不一致或出现竞态条件。
为了确保线程安全,可以使用同步机制,如互斥锁(mutex)、条件变量、信号量等来控制对共享数据的访问。
2. 线程同步:线程同步是保证多个线程按照一定的顺序协同工作的一种机制。
例如,如果一个线程需要依赖另一个线程的结果,则需要使用同步机制来等待另一个线程完成任务并获取结果。
常见的线程同步机制包括互斥锁、条件变量、信号量等。
3. 死锁:当多个线程相互等待对方释放资源时,可能会导致死锁。
死锁是指所有的线程都无法继续执行,程序陷入僵局。
为了避免死锁,需要合理设计线程间资源的请求和释放顺序,避免循环等待。
4. 线程优先级:线程在操作系统中会分配一个优先级,优先级高的线程会获得更多的系统资源。
但在实际开发中,不建议过分依赖线程优先级来控制线程的执行顺序,因为不同操作系统和硬件平台对线程优先级的实现方式不同。
5. 线程创建和销毁的开销:创建线程和销毁线程都需要一定的系统资源。
频繁创建和销毁线程会带来开销,所以需要根据实际需求和系统资源的限制,合理选择线程的创建和销毁时机。
6. 上下文切换开销:当一个处理器从一个线程切换到另一个线程时,需要保存当前线程的上下文状态以及加载新线程的上下文状态,这个过程称为上下文切换。
上下文切换会带来一定的开销,特别是当线程数量较多时。
因此,合理控制线程数量,避免不必要的线程切换,可以提高程序的性能。
7. 资源管理:多线程需要共享系统资源,如内存、文件、网络连接等。
因此,需要合理地管理和分配这些资源,避免出现资源争用的情况。
特别是当多个线程同时访问和修改同一份数据时,需要确保对资源的访问和修改都是线程安全的。
软件开发中最常见的24种错误类型及其解决方案在软件开发中,无论是新手还是经验丰富的开发人员,都难免会遇到各种各样的错误和挑战。
这些错误可能来自不同的层面,比如编码、测试、部署和维护等。
为了帮助开发人员更好地解决这些问题,本文总结了软件开发中最常见的24种错误类型,并提供了相应的解决方案。
1. 死锁错误死锁是一种多线程执行过程中常见的错误类型。
当多个线程都在等待某个资源的时候,就有可能出现死锁。
这种错误通常会导致程序停止响应,无法正常执行。
解决方案:通过合理规划线程代码顺序,减少出现死锁的概率。
对于已经出现死锁的情况,可以通过进程管理工具来手动结束进程。
2. 内存泄漏错误内存泄漏是指程序在运行时分配的内存空间没有被释放,导致程序在长时间运行后出现崩溃或者异常。
这种错误通常会难以定位,因为它不会立即导致程序崩溃。
解决方案:通过代码审查和内存泄漏检测工具找出问题代码,并在代码中添加适当的释放内存的语句。
3. 缓存不一致错误在分布式系统中,缓存是一种常见的技术,用于提高系统性能。
然而,由于缓存的更新机制存在一定的滞后性,当多个系统同时访问某个缓存时,就容易出现缓存不一致的情况,导致数据不准确或者出现异常。
解决方案:利用分布式缓存系统或者锁机制,实现缓存的同步更新,避免不一致的情况。
4. 空指针错误空指针错误是指程序中使用了空指针变量,导致程序崩溃或者出现异常。
这种错误通常由于变量没有被初始化或者被误删除导致。
解决方案:在程序中对变量进行合适的初始化,并添加空指针判断,确保变量不是空指针。
5. 栈溢出错误栈溢出是指程序在执行中使用了过多的栈空间,导致程序崩溃或者异常。
这种错误通常由于递归调用、过深的函数调用链等因素引起。
解决方案:对程序进行优化和重构,减少递归调用的次数和深度,并分离长函数实现。
6. 逻辑错误逻辑错误是指程序在实现业务逻辑时出现的错误,导致程序无法正确执行。
这种错误通常由于实现逻辑不完整或者存在逻辑漏洞引起。
并发危险解决多线程代码中的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 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 的所有线程彼此之间互斥,并且它使用CLRSystem.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",网址为/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?看上去这个问题很可笑。