之前写过两篇关于线程同步问题的文章(一,二),这篇中将对相关话题进行总结,本文中也对.NET 4.0中新增的一些同步机制进行了介绍。

  首先需要说明的是为什么需要线程功能同步。MSDN中有这样一段话很好的解释了这个问题:

当多个线程可以调用单个对象的属性和方法时,对这些调用进行同步处理是非常重要的。否则,一个线程可能会中断另一个线程正在执行的任务,使该对象处于一种无效状态。

也就说在默认无同步的情况下,任何线程都可以随时访问任何方法或字段,但一次只能有一个线程访问这些对象。另外,MSDN中也给出定义,成员不受多线程调用中断影响的类即线程安全类。

CLI提供了几种可用来同步对实例和静态成员的访问的策略(前面两边文章介绍了这其中大部分机制):

  • 同步代码区域

可以使用Monitor类或(编译器支持的语法,如C#中的lock关键字)来同步需要安全的接受并发请求的代码段,这种方式比其他等效的同步方法有更好的性能。

lock语句通过Monitor的Enter和Exit方法实现代码段同步,使用try catch finally结构确保锁被释放。当线程执行该代码时,会尝试获取锁。如果该锁已由其他线程获取,则在锁变为可用状态之前,该线程一直处于阻塞状态。当线程退出同步代码块时,锁就会被释放,这与线程的退出方式无关。通常情况下同步一小代码块并且不跨越多个方法的最佳选择是lock 语句,Monitor类功能强大但使用不当容易出现孤立锁与死锁,而由于lock是通过Monitor的Enter和Exit实现的,因此在临界区中可以结合Monitor的其它方法一起使用。

另外可以通过[MethodImpl(MethodImplOptions.Synchronized)]特性标记一个方法是需要被同步的,方法可以是实例方法也可以是静态方法。最终实现的效果与使用lock关键字或Monitor相关方法相同。注意不要在此特性标记的方法内使用lock(this)/lock(typeof(this))(注意,单独使用lock时也不应用对象本身或类型作为锁(应为类型或实例可能被其它机制锁定,如被[MethodImpl(MethodImplOptions.Synchronized)]标记),对于实例方法与静态方法最好分别使用声明新的私有成员或静态私有成员作为锁,避免使用公有成员作为锁)。另外不能对字符串加锁。

  • 手动同步:

.NET Framework中提供一些类用于手动进行线程间的访问同步。这些类主要分为3大类别(但正如下文中会看到的这些类别划分并非绝对,某些同步机制在多个类别之间有交叉):

ü  锁定

ü  通知

ü  连锁操作

  1. 锁定

排他锁

独占锁

最常见的形式就是C#的lock语句,该语句控制对一个代码块的访问,这个代码块被称作临界区。详见前文xx中对lock的介绍。

Monitor类

Monitor类提供了许多附加功能,这些功能可以与lock关键字结合使用(在lock的临界区中调用Monitor类的方法)。更多细节见线程同步问题1方法二中的介绍。

Mutex类

Mutex的作用也是创建一个临界区以同步对其中对象的访问,方式类似Monitor类,但最大的不同是Mutex支持跨进程的同步。当然其效率也不如Monitor类,在同一进程内通信应首先考虑使用Monitor。Mutex的介绍详见线程同步问题2方法五中的介绍。

SpinLock类

.NET4.0中新增

当 Monitor 所需的开销会造成性能下降时,可以使用 SpinLock 类。当SpinLock请求进入临界区时,会反复地旋转(执行空循环),直至锁变为可用的。如果请求锁所需时间非常短,则空转可比阻塞提供更好的性能。但是,如果锁保留数十个周期以上,则SpinLock的表现会和Monitor一样,而且将使用更多的CPU周期,降低其他线程或进程的性能。

其它锁

有些时候锁不必独占,可以允许一定数目的线程并发访问某个资源。下面列举的锁即用于这个目的。

ReaderWriterLock类

允许多个线程同时读取一个资源,但在向该资源写入时要求线程等待以获得独占锁。更多细节见线程同步问题1方法三中的介绍。

Semaphore类

Semaphore类允许指定数目的线程访问某个资源。超过这个数目时,请求该资源的其他线程会一直阻塞,直到某个线程释放信号量。更多细节见线程同步问题2方法七中的介绍。

ReaderWriterLockSlim类

.NET4.0中新增

这个类的作用与ReaderWriterLock类完全一致,其拥有更好的性能,在新开发的程序中应当使用ReaderWriterLockSlim而不是ReaderWriterLock。ReaderWriterLockSlim 具有线程关联。

SemaphoreSlim类

.NET4.0中新增

SemaphoreSlim类是用于在单一进程边界内进行同步的轻量信号量。使用方式上与Semaphore一致。

  1. 通知

通知机制是等待另一个线程的信号的所有方法的统称。

Join方法

这是等待来自另一个线程信号最简单的方法,解释Join方法最好有一个场景,假如我们有ThreadA,ThreadB两个线程,假如我们在ThreadB执行的方法中调用ThreadA.Join()方法。这将阻塞B线程的执行直到A线程完成。场景中ThreadB可以是主线程也可以是其它子线程。其中也可以调用多个子线程的Join方法。这样ThreadB将阻塞并等待所有这些线程执行完毕后才继续执行。另外如果ThreadA的方法中调用了其它线程的Join方法,这将形成一个队列形式的线程调用,所有这些线程将一个个排队执行。

Join也具有两个接受时间间隔的重载,用于设置阻塞线程等待的最长时间。依然用上面的例子来说,我们在B线程方法中调用ThreadA.Join(5000),当在5秒钟内线程A执行完毕了,则Join方法会立刻返回true,ThreadB继续执行,如果5秒钟线程A未完成,则Join方法在5秒钟到时返回false,ThreadA与ThreadB进入并行交替执行状态。

等待句柄

等待句柄派生自WaitHandle类,后者又派生自 MarshalByRefObject。从而等待句柄可用于跨应用程序域边界的线程同步。WaitHandle类封装了Win32的同步句柄,用于表示所有允许多个等待操作的同步对象。

通过调用WaitOne实例方法或WaitAll、WaitAny及SignalAndWait中任一个静态方法方法,可以阻塞当前线程以等待WaitHandle发出信号。

WaitHandle的派生类具有不同的线程关联。事件等待句柄(EventWaitHandle、AutoResetEvent 和 ManualResetEvent)以及信号量没有线程关联。任何线程都可以发送事件等待句柄或信号量的信号。另一方面,mutex有线程关联。拥有mutex的线程必须将其释放;而如果在不拥有mutex的线程上调用ReleaseMutex方法,则将引发异常。

事件等待句柄

事件等待句柄包括EventWaitHandle类及其派生类AutoResetEvent和ManualResetEvent,这些类允许线程通过彼此发送信号和等待彼此的信号来同步活动。当通过调用Set方法或使用SignalAndWait方法通知事件等待句柄时,阻塞线程会从事件等待句柄中释放。

事件等待句柄要么自动重置自身(类似于每次得到信号时只允许一个线程通过的旋转门),要么必须手动重置(类似于一道门,在得到信号前一直关闭,得到信号打开后到其关闭前一直打开)。顾名思义,AutoResetEvent和ManualResetEvent分别表示前者和后者。

AutoResetEvent

派生自EventWaitHandle,表示自动重置的本地事件。详见线程同步问题2方法六的介绍。

ManualResetEvent

派生自EventWaitHandle,表示手动重置的本地事件。详见线程同步问题2方法六的介绍

ManualResetEventSlim

.NET4.0中新增

ManualResetEventSlim类提供了ManualResetEvent的简化版本。其模型与使用方式上与ManualResetEvent一致,主要用于同一进程内线程间的同步。

CountdownEvent

.NET4.0中新增

CountdownEvent的作用与Semaphore相反,Semaphore中设置了最大可用槽数,当计数为0时(即资源不够用时)则阻塞线程。而CountdownEvent用来统计其它线程结束工作的情况,当监听数变为0时,触发信号。本篇文章的最后部分我们详细介绍CountdownEvent类。

Mutex类/ Semaphore类

这两个类均派生自WaitHandle,所以它们均可与WaitHandle的静态方法一起使用。例如,线程可以使用WaitAll方法/WaitAny方法等待,而以下三个条件均可以使这个线程解除阻塞:EventWaitHandle接收到信号,Mutex被释放,Semaphore被释放。

Barrier类

.NET4.0中新增

利用 Barrier 类,可以对多个线程进行循环同步,以便它们都在同一个点上阻塞来等待其他线程完成。后文将对这个类进行详细介绍。

         
  1. 连锁操作

联锁操作是由 Interlocked 类的静态方法对某个内存位置执行的简单原子操作。这些原子操作包括添加、递增和递减、交换、依赖于比较的条件交换,以及 32 位平台上的 64 位值的读取操作。关于Interlocked类详见线程同步问题1方法一。

特别注意,原子性的保证仅限于单个操作;如果必须将多个操作作为一个单元执行,则必须使用更粗粒度的同步机制。

尽管这些操作中没有一个是锁或信号,但它们可用于构造锁和信号。因为它们是Windows操作系统固有的,因此联锁操作的执行速度非常快。如CountdownEvent的实现中就使用了Interlocked类。

最后注意,只要有一个线程避开同步机制直接访问需要同步访问的资源,这种同步机制就是无效的。

  • 同步上下文:

可以使用SynchronizationAttribute为ContextBoundObject对象(上下文绑定对象)启用简单的自动同步。介绍详见线程同步问题1方法四中的介绍。

  • 线程安全集合:

.NET4.0中新引入的命名空间System.Collections.Concurrent中提供的集合类内置对添加和移除操作的同步机制。多个线程可以在这些集合中安全高效地添加或移除项,而无需用户执行其他同步操作。在编写新代码时,如果遇到多个线程同时写入集合的情况,就应使用并发集合类。如果仅从集合进行(并发)读取,则可使用System.Collections.Generic命名空间中的类。

从.NET发展来看,.NET1.0中提供的集合类(Aarry,Hashtable)通过Synchronized属性支持同步,但不支持泛型,NET2.0种提供了泛型类的集合,但没有内置任何同步机制。.NET4.0开始提供的并发集合类把线程安全与类型安全集合起来。为了提高效率这些并发集合的一部分使用了.NET4.0新增的轻量同步机制,如SpinLock、SpinWait、SemaphoreSlim 和 CountdownEvent,另外ConcurrentQueue<T>和ConcurrentStack<T>类没有使用这些同步机制,而是依赖Interlocked操作来实现线程安全性。

这个新增的命名空间下包含如下类型:

类型

说明

BlockingCollection<T>

通过实现IProducerConsumerCollection<T>接口,实现了一个支持生产者消费者模型的数据结构。

ConcurrentDictionary<TKey, TValue>

键/值对字典的线程安全实现。

ConcurrentQueue<T>

线程安全的队列实现。

ConcurrentStack<T>

线程安全的堆栈实现。

ConcurrentBag<T>

无序的元素集合的线程安全实现。

IProducerConsumerCollection<T>

BlockingCollection实现的接口。

CLR中不同类别可以根据要求以不同的方式进行同步。下表显示了上面列出的几类同步策略为不同类别的字段和方法提供的同步支持。

类别

全局字段

静态字段

静态方法

实例字段

实例方法

特定代码块

无同步

不同步

不同步

不同步

不同步

不同步

不同步

同步上下文

不同步

不同步

不同步

可以同步

可以同步

不同步

同步代码区域

不同步

不同步

当标记时同步

不同步

当标记时同步

当标记时同步

手动同步

手动

手动

手动

手动

手动

手动

到这,可以发现.NET4.0添加了很多新的同步类(轻量类型),这些类尽可能避免依赖高开销的Win32内核对象(例如等待句柄)来提高性能。通常,当等待时间较短并且只有在尝试了原始同步类型并发现它们并不令人满意时,才应使用这些类型。另外,在需要跨进程通信的方案中不能使用轻量类型。

以下内容来源这篇文章:

CountdownEvent

CountdownEvent,前文中我们提及了CountdownEvent实现的同步效果。这里我们将给出一个CountdownEvent适用的场景及示例代码。如我们可以在主线程中模拟一个线程池,通过CountdownEvent使得主线程可以等待线程池中所有线程结束后才能继续执行(对所有子线程的执行顺序没有要求)。在给出代码之前先介绍一些CountdownEvent中一些主要的属性与方法:

重载的构造函数:CountdownEvent的构造函数接受一个整型值,表示事件句柄最初必须的信号数。

InitialCount属性:这个属性正是构造函数接收的参数所设置的值。

CurrentCount属性:事件解除阻塞所必需的剩余信号数。

AddCount方法:将CurrentCount属性的值加1。

Single方法:给出一个信号,这将是CurrentCount的值减1。

class Program

{

static void Main()

{

var customers = Enumerable.Range(1, 20);

using (var countdown = new CountdownEvent(customers.Count()))

{

foreach (var customer in customers)

{

int currentCustomer = customer;

ThreadPool.QueueUserWorkItem(delegate

{

BuySomeStuff(currentCustomer);

countdown.Signal();

//for test

Console.WriteLine(" CountdownEvent:" + countdown.CurrentCount);

});

}

countdown.Wait();

}

//主线程继续执行

Console.WriteLine("All Customers finished shopping...");

Console.ReadKey();

}

static void BuySomeStuff(int customer)

{

// Fake work

Thread.SpinWait(200000000);

Console.Write("Customer {0} finished", customer);

}

}

代码输出(每次运行子线程执行顺序可能不同):

Customer 1 finished CountdownEvent:19

Customer 2 finished CountdownEvent:18

Customer 3 finished CountdownEvent:17

Customer 4 finished CountdownEvent:16

Customer 5 finished CountdownEvent:15

Customer 6 finished CountdownEvent:14

Customer 7 finished CountdownEvent:13

Customer 8 finished CountdownEvent:12

Customer 9 finished CountdownEvent:11

Customer 10 finished CountdownEvent:10

Customer 11 finished CountdownEvent:9

Customer 12 finished CountdownEvent:8

Customer 13 finished CountdownEvent:7

Customer 14 finished CountdownEvent:6

Customer 15 finished CountdownEvent:5

Customer 16 finished CountdownEvent:4

Customer 17 finished CountdownEvent:3

Customer 18 finished CountdownEvent:2

Customer 20 finished CountdownEvent:1

Customer 19 finished CountdownEvent:0

All Customers finished shopping...

代码中主线程中调用Wait方法来等待子线程完成(即CountdownEvent的CurrentCount属性变为0)。

CountdownEvent内部通过ManualResetEventSlim与Interlocked实现,ManualResetEventSlim用于实现事件等待句柄,而Interlocked用于线程计数。

Barrier

这个类的作用很明确,使用很简单,首先介绍其中几个比较重要的属性与方法,之后直接进入示例:

构造函数:两个重载共同的参数是需要被同步的线程的数量,参数较多的一个重载第二个参数接收一个Action<Barrier>类型对象,表示所有线程达到同一阶段后执行的方法。

ParticipantCount属性:即构造函数中设置的需要被同步的线程的数量。

SignalAndWait方法:发出参与者已达到Barrier的信号,等待所有其他参与者也达到Barrier。

场景如下:Charlie、Mac、Dennis三个人相约在途中的加油站会合后一同前往西雅图。我们用Barrier来模拟这个场景,重要的是在加油站会和这一点进行同步。

代码:

class Program

{

static Barrier sync;

static CancellationToken token;

static void Main(string[] args)

{

var source = new CancellationTokenSource();

token = source.Token;

sync = new Barrier(3);

var charlie = new Thread(() => DriveToBoston("Charlie", TimeSpan.FromSeconds(1)));

charlie.Start();

var mac = new Thread(() => DriveToBoston("Mac", TimeSpan.FromSeconds(2)));

mac.Start();

var dennis = new Thread(() => DriveToBoston("Dennis", TimeSpan.FromSeconds(3)));

dennis.Start();

//source.Cancel();

charlie.Join();

mac.Join();

dennis.Join();

Console.ReadKey();

}

static void DriveToBoston(string name, TimeSpan timeToGasStation)

{

try

{

Console.WriteLine("[{0}] Leaving House", name);

// Perform some work

Thread.Sleep(timeToGasStation);

Console.WriteLine("[{0}] Arrived at Gas Station", name);

// Need to sync here

sync.SignalAndWait(token);

// Perform some more work

Console.WriteLine("[{0}] Leaving for Boston", name);

}

catch (OperationCanceledException)

{

Console.WriteLine("[{0}] Caravan was cancelled! Going home!", name);

}

}

}

执行结果(同样每次运行子线程执行顺序可能不同):

[Charlie] Leaving House

[Mac] Leaving House

[Dennis] Leaving House

[Charlie] Arrived at Gas Station

[Mac] Arrived at Gas Station

[Dennis] Arrived at Gas Station

[Dennis] Leaving for Boston

[Mac] Leaving for Boston

[Charlie] Leaving for Boston

另外可以取消代码中的注释,观察多线程取消的效果。

其它.NET4.0新增的线程类

SpinWait

从.NET Framework 4开始,当线程必须等待发生某个事件发出信号时或需要满足某个条件时,可以使用System.Threading.SpinWait结构,前提是实际等待时间预计会少于通过使用等待句柄或通过其他方式阻塞当前线程所需要的等待时间,否则SpinWait空转导致的CPU开销会影响其它进程。通过使用 SpinWait,可以指定在一个较短的时段内边等待边旋转,然后只有在相应的条件在指定时间内无法得到满足的情况下放弃旋转。

其它小话题:

Thread.Interrupt方法可用于使线程跳出阻塞状态(如等待访问同步代码区域)。Thread.Interrupt 还可用于使线程跳出 Thread.Sleep 等操作。

转载于:https://www.cnblogs.com/gjhjoy/p/3544361.html

多线程下的进程同步(线程同步问题总结篇)相关推荐

  1. 多线程下ArrayList类线程不安全的解决方法及原理

    多线程下ArrayList类线程不安全的解决方法及原理 参考文章: (1)多线程下ArrayList类线程不安全的解决方法及原理 (2)https://www.cnblogs.com/fangting ...

  2. c#.net多线程编程教学(3):线程同步

    随着对多线程学习的深入,你可能觉得需要了解一些有关线程共享资源的问题. .NET framework提供了很多的类和数据类型来控制对共享资源的访问. 考虑一种我们经常遇到的情况:有一些全局变量和共享的 ...

  3. python 线程锁_Python3多线程执行任务含线程同步锁

    Python启动多线程执行任务,用线程锁实现同步分配任务,最后等待所有线程执行完毕#python3多线程演示 import threading import random import time to ...

  4. IOS多线程系统学习之线程同步与线程通信

    多线程编程是有趣的事情,它很容易突然出现"错误情况",这是由于系统的线程调度具有一定的随机性造成的.不过,即使程序偶然出现"错误情况",这是由于系统的线程调度具 ...

  5. Java多线程详解(线程同步)

    嗨喽-小伙伴们我来了, 上一章,我们通过几个例子(点击跳转)介绍了线程安全问题,而说到线程安全就不得不提到线程同步,它是解决线程安全的一种重要方法.本章就来简单地介绍一下线程同步. 从上一章的学习我们 ...

  6. 对Java多线程编程的初步了解,实现多线程的三种方式以及多线程并发安全的线程同步机制

    什么叫进程?什么叫线程? 进程相当于一个应用程序,线程就是进程中的一个应用场景或者说是一个执行单元,一个进程可以启动多个线程,每个线程执行不同的任务,一个线程不能单独存在,他必须是进程的一部分,当进程 ...

  7. Linux系统编程38:多线程之什么是线程同步以及条件变量函数

    文章目录 (1):什么是线程的同步 (2):实现线程同步-条件变量函数 (1):什么是线程的同步 假如有一片临界资源,线程A和B都会修改它,为了保护资源所以要加锁,此时它们之间是互斥的关系.在我们的代 ...

  8. java 多线程(四)—— 线程同步/互斥=队列+锁

    同步.异步.互斥的区别 在很多文章中,直接把同步缩小为互斥,并称之为同步.下面也是这样的. 一.线程同步 = 队列 + 锁 同步(这里说的其实是互斥)就是多个线程同时访问一个资源. 那么如何实现? 队 ...

  9. Linux线程同步(一)---“初识篇”

    一 why 先给自己打个广告,本人的微信公众号:嵌入式Linux江湖,主要关注嵌入式软件开发,股票基金定投,足球等等,希望大家多多关注,有问题可以直接留言给我,一定尽心尽力回答大家的问题. 既然是探讨 ...

最新文章

  1. 怎样用cocos2d-x做一个基于地图块的游戏(Part One)
  2. 重读经典:完全解析特征学习大杀器ResNet
  3. 2020年安徽赛区智能车竞赛有关事项通知(草案)
  4. NopCommerce架构分析-依赖注入
  5. js作用域链以及全局变量和局部变量
  6. 如何把握好 transition 和 animation 的时序,创作描边按钮特效
  7. linux 监听数据包,linux下网络监听与发送数据包的方法(即libpcap、libnet两种类库的使用方法)...
  8. oracle归档日志太多(ORA-00257: archiver error. Connect internal only, until freed)错误的处理方法
  9. C语言网络编程实战之线上五子棋游戏(二)
  10. 微信公众号、小程序和企业微信申请流程
  11. 利用传输矩阵法求解布拉格光栅的透射谱
  12. python---酒鬼漫步
  13. GitLab 的安装及使用
  14. char类型与字符编码
  15. 中国计算机学会(CCF)推荐中文科技期刊目录(2019年)
  16. 一文详解基因组denovo组装原理和实战
  17. Quartz 是什么
  18. 《C++Primer》第二章-变量和基本类型-学习笔记(1)
  19. 服务器设置header返回信息,http服务器header返回时间问题
  20. linux微软雅黑乱码,CentOS安装微软雅黑,解决drawImage中文乱码相关问题

热门文章

  1. mybatis日期范围查询_15. Django 2.1.7 模型 条件查询、模糊查询、空查询、比较查询、范围查询、日期查询...
  2. python 命令行解析函数_python命令行解析之parse_known_args()函数和parse_args()使用区别介绍...
  3. h5自定义相机界面_有没有什么比较好用的H5小程序?
  4. html底部弹出选择,jQuery手机端底部弹出菜单列表特效代码
  5. oracle报错查询动态视图,oracle基表和动态性能视图
  6. 使用方法_防爆配电箱使用方法及使用条件
  7. 51单片机io位与 c语言,【51单片机】普通I/O口模拟SPI口C语言程序
  8. mysql workbench 数据备份_如何使用MySQL Workbench进行MySQL数据库备份?
  9. 看不到图层怎么办_图层管理工具及相关问题
  10. python以列表的形式输出_简单介绍python输出列表元素的所有排列形式