第二十九章 基元线程同步构造

目录

29.1 类库和线程安全

29.2 基元用户模式和内核模式构造

29.3 用户模式构造

29.4 内核模式构造

线程同步:多个线程同时访问共享数据时,线程同步能防止数据损坏。

繁琐:在代码中,必须标识出所有可能由多个线程同时访问的数据。然后,必须用额外的代码将这些代码包围起来,并获取和释放一个线程同步锁。锁的作用是确保一次只有一个线程访问资源,只要有一个代码块忘记用锁包围,数据就会损坏。锁:会损坏性能,获取和释放锁是需要时间的,因为要调用一些额外的方法,而且不同的CPU必须进行协调,以决定哪个线程先取得锁。一次只允许一个线程访问资源。

29.1 类库和线程安全

FCL保证所有静态方法都是线程安全的。

使自己的所有静态方法都线程安全,使所有实例方法都非线程安全。

29.2 基元用户模式和内核模式构造

 基元是指可以在代码中使用的最简单的构造。两种基元构造:用户模式和内核模式。

用户模式使用特殊CPU指令来协调线程。意味着协调是在硬件中发生的。也意味着Microsoft Windows操作系统永远检测不到一个线程在基元用户模式的构造上阻塞了。由于用户模式的基元构造上阻塞的线程永远不认为已阻塞,所以线程池不会创建新线程来替换这种临时阻塞的线程。

内核模式的构造是由Windows操作系统自身提供的。所以,它们要求在应用程序的线程中调用由操作系统内核实现的函数。将线程从用户模式切换为内核模式(或相反)会招致巨大的性能损失。优点:线程通过内核模式的构造获取其他线程拥有的资源时,Windows会阻塞线程以避免它浪费CPU时间。当资源变得可用时,Windows会恢复线程,允许它访问资源。

对于在一个构造上等待的线程 ,如果拥有这个构造的线程一直不释放它,前者就可能一直阻塞。如果是用户模式的构造,线程将一直在一个CPU上运行,我们称为“活锁”。如果是内核模式的构造,线程将一直阻塞,我们称为“死锁”。理想中的构造:在没有竞争的情况下,这个构造应该快而且不会阻塞(用户模式)。但如果存在对构造的竞争,它被操作系统内核阻塞。——混合构造

CLR的许多线程同步构造实际只是“Win32线程同步构造”的一些面向对象的包装器。CLR线程就是Windows线程,意味着要由Windows调度线程和控制线程同步。

29.3 用户模式构造

 原子性:一次性。非原子性:一次torn read。

两种基元用户模式线程同步构造:

易变构造:在特定的时间,它在包含一个简单数据类型的变量上执行原子性的读或写造作。

互锁构造:在特定的时间,它在包含一个简单数据类型的变量上执行原子性的读或写操作。

29.3.1 易变构造

高级语言常规构造:if/else,swithc/case,各种循环,局部变量,实参,虚方法调用,操作符重载等。最终,高级语言的编译器必须将高级构造转换成低级构造,使计算机能真正做你想做的事情。

C#编译器将你的C#构造转换成中间语言(IL)。然后JIT将IL转换成本机CPU指令,然后由CPU亲自处理这些指令。此外,C#编译器,JIT编译器,甚至CPU本身都可能优化你的代码。

Volatile.Writer 方法强迫location中的值在调用时写入。此外,按照编码那顺序,之前的加载和存储操作必须在调用Volatile.Write之前发生。

Volatile.Read 方法强迫location中的值在调用时读取。此外,按照编码顺序,之后的加载和存储操作必须在调用Volatile.Read之后发生。

当线程通过共享内存相互通信时,调用Volation.Write来写入最后一个值,调用Volatile.Read来读取第一个值。

29.3.2 互锁构造

Interlocked类中的每个方法都执行一次原子读取以及写入操作。此外,Interlicked的所有方法都建立了完整的内存栅栏。调用某个Interlocked方法之前的任何变量写入都在这个Interlocked方法调用之前执行;而这个调用之后的任何变量读取都在这个调用之后读取。 

23.3.3 实现简单的自旋锁

 如果需要原子性地操作类对象中的一组字段,这种情况下需要采取一个办法阻止所有线程,只允许一个进入对字段进行操作的代码区域。可以使用Interlocked的方法构造一个线程同步块。

23.3.4 Interlocked Anything模式

 该模式类似于在修改数据库记录时使用的乐观并发模式。

29.4 内核模式构造

Windows提供了几个内核模式的构造来同步线程。内核模式的构造比用户模式的构造慢得多,一个原因是它们要求Windows操作系统自身配合,另一个原因是在内核对象上调用的每个方法都造成调用线程从托管代码转换为本机用户模式代码,再转换为本机内核模式代码。然后,还要朝相反的方向一路返回。这些在转换需要大量CPU时间:

优点:

内核模式的构造检测到一个资源上的竞争时,Windows会阻塞输掉的线程,使它不着一个CPU“自旋“,无所谓地浪费处理器资源。

内核模式的构造可实现本机和托管线程相互之间的同步。

内核模式的构造可同步在用一台机器的不同进程中的运行的线程。

内核模式的构造可应用安全性能设置,防止未授权的账户访问它们。

线程可一直阻塞,直到集合中的所有内核模式构造都可用,或直到集合中的任何内核模式构造可用。

在内核模式的构造上阻塞的线程可指定超时值;指定时间内访问不到希望的资源,线程就可以解除阻塞并执行其他任务。

事件和信号时两种基元内核模式线程同步构造。

WaitHandle基类内部有一个 SafeWaitHandle字段,它容纳一个Win32内核对象句柄。这个字段时在构造一个具体WaitHandle派生类时初始化的。除此之外,WaitHandle类公开了由所有派生类继承的方法。在衣蛾内核模式的构造上调用的每个方法都代表一个完整的内栅栏。

public abstract class WaitHandle : MarshalByRefObject, IDisposable{
     public virtual Boolean WaitOne(); //内部调用 Win32WaitForSingleObjectEx函数  
     pubilc virtual Boolean WaitOne(Int32 millisecondsTimeOut); 
     public virtual Boolean WaitOne(TimeSpan timeout); 

     public static Boolean WaitAll(WaitHandle[] WaitHandles); //内部调用Win32 WaitForMultipleObjectEx函数  
     public static Boolean WaitAll(WaitHandle[] WaitHandles, Int32 millisecondsTimeOut); 
     public static Boolean WaitAll(WaitHandle[] WaitHandles, TimeSpan timeout);

     public static Int32 WaitAny(WaitHandle[] WaitHandles); //内部调用Win32 WaitForMultipleObjectEx函数  
     public static Int32 WaitAny(WaitHandle[] WaitHandles, Int32 millisecondsTimeOut); 
     public static Int32 WaitAny(WaitHandle[] WaitHandles, TimeSpan timeout);

     public const Int32 WaitTimeout = 258; //超时就从WaitAny返回这个

     public void Dispse(); // 内部调用Win32 CloseHandle函数 - 自己不要调用
    
}    

注意:

可以调用WaitHandle的WaitOne方法让调用线程等待底层内核对象受到信号。

可以调用WaitHandle的静态WaitAll方法,让调用线程等待WaitHandle[]中指定的所有内核对象都受到信号。

可以调用WaitHandle的静态WaitAny方法,让调用线程等待WaitHandle[]中指定的所有内核对象都受到信号。

在传给WaitAny和WaitAll方法的数组中,包含元素不能超过64个,否则方法会抛出一个System.NotSupportedException。

可以调用WatiHandle的Dispose方法来关闭底层内核对象句柄。

不接受超时参数的那些版本的WaitOne和WaitAll方法应返回void而不是Boolean。

内核模式构造的一个常见用途就是创建在任何时刻只允许它的一个实例运行的应用程序。

每个进程都有自己的线程,两个线程都尝试创建具有相同字符串名称的一个Seamphore。Windows内核确保只有一个线程实际地创建具有指定名称的内核对象;创建对象的线程会将它createdNew变量设为true。对于第二个线程,Windows发现具有指定名称的内核对象已经存在了。因此,不允许第二个线程创建另一个同名的内核对象。如果这个线程继续运行的话,它能访问和第一个进程的线程所访问的一样的内核对象。不同进程中的线程就是这样通过内核对象来通信的。

29.4.1 Event构造

  事件其实只是由内核维护的Boolean变量。事件为false,在事件上等待的线程就阻塞;事件为true,就解除阻塞。有两种事件,即自动重置事件和手动重置事件。当一个自动重置事件为true时,它只唤醒一个阻塞的线程,因为在解除第一个线程,因为在解除第一线程的阻塞后,内核将事件自动重置回false,造成其余线程继续阻塞。而当一个手动重置事件为true时,它解除正在等待它的所有线程的阻塞,因为内核不将事件自动重置回false;

29.4.2 Semaphore构造

 信号量(semaphore)其实就是由内核维护的Int32变量。信号量为0时,在信号量上等待的线程会阻塞;信号量大于0时解除阻塞。在信号量上等待的线程解除阻塞时,内核自动从信号量的计数中减1.信号量海关联了一个最大Int32值,当前计数决不允许超过最大计数。

多个线程在一个自动重置事件上等待时,设置事件只导致一个线程被解除阻塞。

多个线程在一个手动重置事件上等待时,设置事件导致所有线程被解除阻塞。

多个线程在一个信号量上等待时,释放信号量导致releaseCount个线程被解除阻塞

29.4.3 Mutex构造

互斥体代表一个互斥的锁。

 

posted @ 2019-07-26 22:48  郭大大大  阅读(162)  评论(0编辑  收藏  举报