C#多线程——线程同步

一、为什么要线程同步?

多个线程同时使用共享对象会造成很多问题,同步这些线程使得对共享对象的操作能够以正确的顺序执行是非常重要的。

二、实现线程同步的方法:

• 使用Mutex类
• 使用SemaphoreSlim类
• 使用AutoResetEvent类
• 使用ManualResetEventSlim类
• 使用CountDownEvent类
• 使用Barrier类
• 使用ReaderWriterLockSlim类
• 使用SpinWait类

三、具体实现:

1. 使用Mutex类

i.简介:一种原始的同步方式,只对一个线程授予对共享资源的独占访问。

ii.Code实现:

C#多线程——线程同步_第1张图片

iii.原理:定义一个指定名称的互斥量,设置initialOwner标志为false。这就意味着如果互斥量已被创建,则允许程序获取互斥量。如果没有获得互斥量,程序则显示Runing,等待直到按下任何键,然后释放互斥量并退出。

iv. 注意:
1)如果再运行同样的程序,则会在5秒内尝试获取互斥量。此时第一个程序如果按下了任意键,第二个程序就会开始运行,如果没有,则第二个程序将无法获得该互斥量。
2) 具名的互斥量是全局的操作系统对象,请务必正确关闭互斥量,最好用using代码块包裹互斥对象。
3) 该方式可用于不同程序中同步线程,可被推广到大量的使用场景中。

2. 使用SemaphoreSlim类
i.简介:作为Semaphore的轻量级版本,限制同时访问同一个资源的线程数量。

ii.Code实现:
C#多线程——线程同步_第2张图片

iii.原理:当主程序启动时,创建了SemaphoreSlim的一个实例,并在其构造函数中指定允许的并发线程数量。然后启动了6个不同名称和不同初始运行时间的线程。每个线程都尝试获取数据库的访问,但是我们借助于信号系统限制了访问数据库的并发数为4个线程。当有4个线程获取了数据库的访问后,其他两个线程需要等待,直到之前线程中的某一个完成工作并调用_ semaphore.Release 方法来发出信号。

iv.拓展:这里我们使用了混合模式,其允许我们在等待时间很短的情况下无需使用上下文切换。然而,有一个叫作Semaphore的SemaphoreSlim类的老版本。该版本使用纯粹的内核时间( kernel-time)方式。一般没必要使用它,除非是非常重要的场景。我们可以创建一一个具名的semaphore,就像一个具 名的mutex一样, 从而在不同的程序中同步线程。SemaphoreSlim 并不使用Windows内核信号量,而且也不支持进程间同步。所以在跨程序同步的场景下可以使用Semaphore。

3. 使用AutoResetEvent类
i.简介:从一个线程向另一个线程发送通知,AutoResetEvent可以通知等待线程有某件事发生。

ii.Code实现:
C#多线程——线程同步_第3张图片
iii.原理:当主程序启动时,定义了两个AutoResetEvent实例。其中一个是从子线程向主线程发信号,另一个实例是从主线程向子线程发信号。我们向AutoResetlEvent构造方法传入false,定义了这两个实例的初始状态为unsignaled.这意味着任何线程调用这两个对象中的任何一个的WaitOne方法将会被阻塞,直到我们调用了Set方法。如果初始事件状态为true,那么AutoResetEvent实例的状态为signaled,如果线程调用WaitOne方法则会被立即处理。然后事件状态自动变为unsignaled,所以需要再对该实例调用一次Set方法,以便让其他的线程对该实例调用WaitOne方法从而继续执行。

然后我们创建了第二个线程,其会执行第一个操作 10秒钟,然后等待从第二个线程发出的信号。该信号意味着第一个操作已经完成。现在第二个线程在等待主线程的信号。我们对主线程做了一些附加工作,并通过调用mainEvent.Set 方法发送了一个信号。然后等待从第二个线程发出的另一个信号。

AutoResetEvent类采用的是内核时间模式,所以等待时间不能太长。使用2.6节中的ManualResetEventslim类更好,因为它使用的是混合模式。

4. 使用ManualResetEventSlim类
i.简介:ManualResetEventSlim类以更加灵活的方式在线程间传递信号。
ii.Code实现:
C#多线程——线程同步_第4张图片
iii.原理:
当主制停能动时,古先创建了Mneersin类的一个实例。然后启动了三个线程,等待事件信号通知它们继续执行。

ManuReseEvetsSim的整个工作方式有点像人群通过大门。2.5 节中讨论过的AutoResetEvent事件像一一个旋转门, 一次只允许一人 通过。ManualResetEventSlim 是Manual-ResclEvent的混合版本,一直保持大门散开直到手动调用Reset方法。当调用mainEventSet时,相当于打开了大门从而允许准备好的线程接收信号并继续工作。然而线程3还处于睡眠状态,没有赶上时间。当调用mainEvent.Reset 相当于关闭了大门。最后一个线程已经准备好执行,但是不得不等待下一个信号,即要等待好几秒钟。

iv.拓展:如果我们需要全局事件,则可以使用EventWaitHandle类,其是AutoResetEvent和ManualResetEvent类的基类。

5. 使用CountDownEvent类
i.简介:CountDownEvent信号类,等待直到一定数量的操作完成。

ii. Code实现:
C#多线程——线程同步_第5张图片
iii.原理:当主程序启动时,创建了一个CountdownEvent实例,在其构造函数中指定了当两个操作完成时会发出信号。然后我们启动了两个线程,当它们执行完成后会发出信号。一 旦第二个线程完成,主线程会从等待CountdownEvent的状态中返回并继续执行。针对需要等待多个异步操作完成的情形,使用该方式是非常便利的。

然而这有一个重大的缺点。如果调用countdown.Signal( 没达到指定的次数,那么_coundown.WaitO将直等待。 请确保使用CoundownEvent时,所有线程完成后都要调用Signal方法。

6. 使用Barrier类
i.简介:Barrier类用于组织多个线程及时在某个时刻碰面,其提供了一个回调函数,每次线程调用了SignalAndWait方法后该回调函数会被执行。

ii. Code实现:

iii.原理:
我们创建了Barrier类,指定了我们想要同步两个线程。在两个线程中的任何一个调用了barrier.SignalAndWait 方法后,会执行一个回调函数来打印出阶段。

每个线程将向Barrier发送两次信号,所以会有两个阶段。每次这两个线程调用Signal-AndWait方法时,Barrier 将执行回调函数。这在多线程迭代运算中非常有用,可以在每个迭代结束前执行一些计算。 当最后一个线程调用SignalAndWait方法时可以在迭代结束时进行交互。

7. 使用ReaderWriterLockSlim类
i.简介:使用ReaderWriterLockSlim类创建一个线程安全的机制,在线程中对一个集合进行读写操作。ReaderWriterLockSlim代表一个管理资源访问的锁,允许多个线程同时读写,以及独占写。

ii. Code实现:
C#多线程——线程同步_第6张图片
C#多线程——线程同步_第7张图片
iii.原理:当主程序启动时,同时运行了三个线程来从字典中读取数据,还有另外两个线程向该字典中写人数据。我们使用ReaderWriterLockSim类来实现线程安全,该类专为这样的场景而设计。

这里使用两种锁:读锁允许多线程读取数据,写锁在被释放前会阻塞了其他线程的所有操作。获取读锁时还有一个有意思的场景,即从集合中读取数据时,根据当前数据而决定是否获取一个写锁并修改该集合。一旦得到写锁,会阻止阅读者读取数据,从而浪费大量的时间, 因此获取写锁后集合会处于阻塞状态。为了最小化阻塞浪费的时间,可以使用EnterUpgradeableReadLock和ExitUpgradeableReadLock方法。先获取读锁后读取数据。如果发现必须修改底层集合,只需使用EnterWriteLock方法升级锁,然后快速执行一次写操作,最后使用ExitWriteLock释放写锁。

在本例中,我们先生成一个随机数。 然后获取读锁并 检查该数是否存在于字典的键集合中。如果不存在,将读锁更新为写锁然后将该新键加人到字典中。始终使用tyrfinaly代码块来确保在捕获锁后一定会释放锁,这是一项好的实践。

所有的线程都被创建为后台线程。主线程在所有后台线程完成后会等待30秒。

8. 使用SpinWait类
i. 简介:混合同步构造,被设计为使用用户模式等待一段时间,然后切换到内核模式以节省CPU时间。

ii. Code实现:
C#多线程——线程同步_第8张图片
iii.原理:当主程序启动时,定义了一个线程,将执行一一个无止境的循环,直到20毫秒后主线程设置isCompleted 变量为true。我们可以试验运行该周期为20 ~30秒,通过Windows 任务管理器测量CPU的负载情况。取决于CPU内核数量,任务管理器将显示一个显著的处理时间。

我们使用volatile 关键字来声明isCompleted 静态字段。Volatile 关键字指出一个字段可能会被同时执行的多个线程修改。声明为volatile的字段不会被编译器和处理器优化为只能被单个线程访问。这确保了该字段总是最新的值。

然后我们使用了SpinWait版本,用于在每个选代打印一个特殊标志位来显示线程是否切换为阻塞状态。运行该线程5毫秒来在看结果。刚开始,SpinWait 尝试使用用户模式,在9个迭代后,开始切换线程为阻塞状态。如果尝试测量该版本的CPU负载,在Windows任务管理器将不会看到任何CPU的使用。

四、示例代码下载

ThreadSynchronization.zip

你可能感兴趣的:(C#多线程——线程同步)