详解C#多线程之线程同步

2019-12-30 16:02:53王振洲

内核模式的所有对象的基类是WaitHandle。内核模式的所有类层次如下

WaitHandle

EventWaitHandle

AutoResetEvent

ManualResetEvent

Semaphore

Mutex

WaitHandle继承MarshalByRefObject,这个就是按引用封送了非托管对象。WaitHandle里面主要是各种Wait方法,调用了Wait方法在没有收到信号之前会被阻塞。WaitOne则是等待一个信号,WaitAny(WaitHandle[] waitHandles)则是收到任意一个waitHandles的信号,WaitAll(WaitHandle[] waitHandles)则是等待所有waitHandles的信号。这些方法都有一个版本允许设置一个超时时间。其他的内核模式构造都有类似的Wait方法。

EventWaitHandle的内部维护着一个布尔值,而Wait方法会在这个布尔值为false时线程就会被阻塞,直到该布尔值为true时线程才被释放。操纵这个布尔值的方法有Set()和Reset(),前者是把布尔值设成true;后者则设成false。这相当于一个开关,调用了Reset之后线程执行到Wait就暂停了,直到Set才恢复。它有两个子类,使用的方式类似,区别在于AutoResetEvent调用Set之后自动调用Reset,使得开关马上恢复关闭状态;而ManualResetEvent就需要手动调用Set让开关关闭。这样就达到一个效果一般情况下AutoResetEvent每次释放的时候能让一条线程通过;而ManualResetEvent在手动调用Reset之前有可能会让多条线程通过。

Semaphore的内部是维护着一个整形,当构造一个Semaphore对象时会指定最大的信号量与初始信号量值,每当调用一次WaitOne,信号量就会加1,当加到最大值时,线程就会被阻塞,当调用Release的时候就会释放一个或多个信号量,此时被阻塞掉的一个或多个线程就会被释放。这个就符合生产者与消费者问题了,当生产者不断往产品队列中加入产品时,他就会WaitOne,当队列满了,就相当于信号量满了,生成者就会被阻塞,当消费者消费掉一个商品时,就会Release释放掉产品队列中的一个空间,此时因没有空间存放产品的生产者又可以开始工作往产品队列中存放产品了。

Mutex的内部与规则相对前面两者稍微复杂一点,先说与前面相似的地方就是同样都会通过WaitOne来阻塞当前线程,通过ReleastMutex来释放对线程的阻塞。区别在于WaitOne的允许第一个调用的线程通过,其余后面的线程调用到WaitOne就会被阻塞,通过了WaitOne的线程可以重复调用WaitOne多次,但是必须调用同样次数的ReleaseMutex来释放,否则会因为次数不对等导致别的线程一直处于阻塞的状态。相比起之前的几个构造,这个构造会有线程所有权与递归这两个概念,这个是单纯靠前面的构造都无法实现的,额外封装除外。

混合构造

上面的基元构造是用了最简单的实现方式,用户 模式有用户模式的快,但是它会带来CPU时间的浪费;内核模式解决了这个问题,但是会带来性能上的损失,各有利弊,而混合构造则是集合了两者的利,它会在内部通过一定策略适当的时机使用用户模式,再另一种情况下又会使用内核模式。但是这些层层判断带来的是内存上的开销。在多线程同步中没有完美的构造,各个构造都有利弊,存在即有意义,结合具体的应用场景就会有最优的构造可供使用。只是在于我们能否按照具体的场景权衡利弊而已。