《CLR via C#》读书笔记-线程同步(三)

目录

  • 内核模式的特点
  • 内核模式中相关知识
    • Event构造
    • Semaphore构造
    • Mutex构造
  • 内核模式小节

一、内核模式的特点

  1. 缺点
    保证线程同步可以通过用户模式和内核模式两种方式实现,内核模式与用户模式相比,构造时间要慢得多。主要的原因有两个:1、其需要操作系统的协作。2、在内核对象上调用的方法会按照:托管代码–>本地用户模式代码–>本地内核模式代码的路径转换
  2. 优点
    1. 当发生线程同步竞争时,window会阻塞其中一个线程,但并不会像用户模式占用CPU而“自旋”,避免无谓的浪费
    2. 可实现本地线程与托管线程同步
    3. 可同步在同一电脑上不同进程中运行的线程
    4. 可应用安全性设置,阻止未授权的用户访问

二、内核模式的相关知识
在内核模式的构造中最基本的两个构造就是event构造和semaphore构造,这两个构造的情况会在下面说明。
在整个内核模式中有一个最基本的基类,就是WaitHandle(其在system.threading命名空间内)。其唯一的作用就是封装一个内核对象句柄。在WaitHandle基类中有一属性SafeWaitHandle,可通过这个属性值获取或设定句柄。这个属性值并不是通过外界的赋值而初始化,而是在构造一个WaitHandle派生类时就会初始化(派生类的构造器内部调用了Win32的方法,Win32方法返回了句柄,并将句柄存放在本属性中)。MSDN上属性截图如下所示(也解释了为什么叫SafeWaitHandle,而不是Handle、WaitHandle了):
MSDN上属性的描述

WaitHandle中常用的方法有:

//调用Win32的CloseHandle
public virtual void Close();
public void Dispose();

//调用Win32的WaitForSingleObjectEx
public virtual bool WaitOne();
public virtual bool WaitOne(int millisecondsTimeout);

//调用Win32的WaitForMultipleObjectEx
public static int WaitAny(WaitHandle[] waitHandle);
public static int WaitAny(WaitHandle[] waitHandle,int millisecondsTimeout);

//调用Win32的WaitForMultipleObjectEx
public static bool WaitAll(WaitHandle[] waitHandle);
public static bool WaitAll(WaitHandle[] waitHandle,int millisecondsTimeout);

//调用Win32的SignalObjectAndWait
public static bool SignalAndWait(WaitHandle toSignal,WaitHandle toWaitOn);
public static bool SignalAndWait(WaitHandle toSignal,WaitHandle toWaitOn,int millisecondsTimeout,bool exitContext);

//保存句柄的属性
public SafeWaitHandle SafeWaitHandle{ get; set;}

public const int WaitTimeout=0x102;

上面的方法/属性中,特别是属性SafeWaitHandle,其类型也是SafeWaitHandle,MSDN上的解释:

Represents a wrapper class for a wait handle.

其就是一个句柄的包装类。它的构造方法如下:

public SafeWaitHandle(
    IntPtr existingHandle,
    bool ownsHandle
)

因此,SafeWaitHandle类就是一个句柄封装类。
下面详细的逐一说明WaitHandle中的各个方法
1、WaitOne
waitone方法让调用线程等待内核对象收到信号。如果内核对象收到信息,则返回true;若发生超时,则返回false
2、waitany()
waitany方法会让调用线程等待数组内的内核对象收到信号。返回值是int类,是数组中收到信号的内核对象的索引。若在等待期间未有对象收到信号,则返回WaitTimeout
3、waitall()
waitall方法会让调用线程等待数据内的所有的内核对象收到信息。若均收到信号,则返回true;若发生超时,则返回false
4、SignalAndWait
signalandwait方法其会自动向一个内核对象发送信号,并等待另外一个内核对象收到信号。

三、内核对象的具体构造

waithandle基类下会有几个派生类,具体如下

  • waithandle
    • eventwaithandle
      • autoresetevent
      • manualresetevent
  • semaphore
  • mutex

派生类都继承了waithandle类中的方法。除了上面的方法之外,派生类还引入了自己的方法,具体如下:
1、派生类的构造器都在内部调用了Win32的CreateEvent、CreateSemaphore、CreateMutex方法,从这个方法返回的句柄,就保存在一开始说的SafeWaitHandle属性中。
2、三个派生类(EventWaitHandle、Semaphore、Mutex)都提供了一个静态的OpenExisting方法。方法的签名如下

public static Semaphore OpenExisting(string name)

本方法的作用就是判断是否存在名称为name的内核对象。若存在,则返回对象;若不存在,则报异常。
另外,多次调用此方法使用相同的值 name 不一定返回相同 Semaphore 对象。
3、内核模式的应用场景
内核模式最常用的构造就是创建在任何时刻只有一个实例运行的应用程序。这个是由window内核保证的
3.1 event构造
event构造本质上是内核维护的一个bool变量。当事件为false时,在事件上等待的线程就阻塞;当为true时,就解除阻塞。event构造分为:auto和manual。auto代表的是:在解除一个线程的阻塞后(false变为true),event会自动的置为false。而manual代表:当事件置为true之后,不会自动的设为false,因此阻塞的线程会全部的解锁,除非手动的置为true。
EventWaitHandle类代表常用的方法:

public class EventWaitHandle:WaitHandle
{
    public bool Set();//将事件置为true,解除阻塞,总是返回true
    public bool Reset(); //将事件置为false,使线程阻塞,总是返回true
}

//AutoResetEvent与ManualResetEvent的具体定义如下:
public class AutoResetEvent:EventWaitHandle
{
    public AutoResetEvent(bool initialState);
}

public class ManualResetEvent:EventWaitHandle
{
    public ManualResetEvent(bool initialState);
}

在这儿就可以通过内核模式的autoresetevent来构建一个与之前的simplespinlock相似的线程同步锁。

internal class SimpleWaitLock:IDisposable{

    pivate AutoResetEvent m_ResourceFree = new AutoResetEvent(true);

    public void Enter()
    {
        m_ResourceFree.WaitOne();
        //这儿以及SimpleSpinLock类中的Enter方法要想理解Enter的作用及实现原理
        //Enter方法的本质就是形成一种阻塞,是形成一种对其他(没有获取权限/令牌/资源)线程形成的一种阻塞
    }

    public void Leave()
    {
        m_ResourceFree.Set();
    }

    public void Disposable()
    {
        m_ResourceFree.Disposable();
    }
}

enter方法内部的实现,就是一个阻塞线程的具体逻辑。内核模式的阻塞是通过window内核的内部方法实现。
3.2 Semaphore构造
信号量本质内核维护的一个int变量。信号量为0时,在信号量上等待的线程就会阻塞。信号量大于0时,就解除阻塞。在信号量内部有两个变量:资源剩余量、可处理的最大量。例如:一个教室最多有40个座位,当有同学进入教室时,位于教室门口处的“剩余座位数”就会减一。当有40个同学进入教室时,“剩余座位数”就显示为0,想进入教室的同学只能等待了。基本原理就是这样。
Semaphore类的相关定义如下:

public sealed class Semaphore:WaitHandle
{
    public Semaphore(int initialCount,int maxCount);
    public int Release(); //相当于Release(1)
    public int Release(int releaseCount);
}

Semaphore有一个特点,不会对调用WaitOne或Release方法的线程进行登记。因此会造成一个线程多次调用Release方法,导致“资源剩余量”大于最大处理量,此时就会抛出SemaphoreFullException的异常。举例说明:假设Semaphore最大的处理量为2,此时线程A和线程B均进入(enter)Semaphore,若线程B在释放资源时,调用了Release方法两次。当线程A释放资源,调用Release方法时,Semaphore就会抛出SemaphoreFullException异常。
3.3 Mutex构造
正是因为Semaphore不“认证/登记”线程,存在导致SemaphoreFullException异常的可能,为了解决这个问题,就提出了Mutex(结构体)。Mutex最主要的特性有两个:1、依次允许线程获取资源,但每次只允许一个线程获取资源。2、对线程进行“认证/登记”。Mutex的基本信息如下:

public sealed class Mutex:WaitHandle
{
    public Mutex();
    public void ReleaseMutex();
}

Mutex能够保证释放资源的线程就是当初获取资源的线程,但是在线程资源的过程中,因某原因线程被终结了(例如通过任务管理器等),则此时的Mutex的状态就是“遗弃状态”,Mutex会被置为“可用”状态(即,Mutex可被线程占用),此时被阻塞的线程就会获取Mutex内核对象的owership(所有权)。在.NET2.0之前是不会抛出异常的,但是之后就会抛出一个AbandonedMutexException异常。
不管任何原因,一个线程被终结,则系统不能保证在占用Mutex的过程中的数据是完整的,因此一般而言,数据的完整性就被破坏了。但若下一个获得Mutex的线程能够保证数据的完整性,则程序也可以继续进行
在上面的Mutex基本信息介绍中,只是介绍了Mutex的最简单的构造,其还有其他的集中构造方式,如下图所示:
Mutex其他的构造器
Mutex分为两类:未命名的本地Mutex,命名的系统Mutex。两者之间的关系就是电脑属性设定里的全局环境变量和局部环境变量之间的关系。本地的Mutex可以通过Mutex的实例在整个应用程序进程中使用。而系统的Mutex则可以同步不同的进程。
另外,Mutex可以实现递归的调用(这也是拜其本身的特性所赐,能够登记当前调用资源的线程ID),在其内部维护这一个递归计数,用于记录拥有Mutex线程调用/拥有了它多少次。例如:

internal class SomeClass:IDisposable
{
    private readonly Mutex m_lock = new Mutex(); //声明一个local Mutex

    public void Method1()
    {
        m_lock.WaitOne();
        Method2();
        m_lock.ReleaseMutex();
    }

    public void Method2()
    {
        m_lock.WaitOne();
        //Do something
        m_lock.ReleaseMutex();
    }

    public void IDispose(){m_lock.Dispose(); }
}

上面的这个例子中就是一个Mutex使用递归的例子。Mutex可以使用递归更深层的原因就是:每一个本地Mutex都是相互独立。在Method1中占用了资源,并不代表占有method2的资源,因此必须在method2上添加获取及释放方法。另外,Mutex只允许一个线程获得它,并且又提供了提供了递归计数,因此就完美的解决在递归过程中存在的问题。
3.4 内核模式中的回调方法
线程如何知道内核对象收到了信号?一种方式就是阻塞式同步等待,另外一个方式就是非阻塞异步方法。后者就是经常提到的回调方法(callback method)。而在Thread.ThreadPool中也提供了一个方法RegisterWaitForSingleObject,方法的具体定义如下:

public static RegisteredWaitHandle RegisterWaitForSingleObject(
    WaitHandle waitObject,
    WaitOrTimerCallback callBack,
    object state,
    int millisecondsTimeOutInterval,
    bool executeOnlyOnce
)

方法的参数如下:

WaitHandle waitObject

代表要监听的内核对象。例如:autosetevent、Semaphore、Mutex等

WaitOrTimerCallback callBack

回调方法。回调方法要符合WaitOrTimerCallback委托。WaitOrTimerCallback委托的定义如下:

public delegate void WaitOrTimerCallback(
    object state,
    bool timedOut
)

object state

回调方法的传入参数。回调方法有两个参数,其中第二个参数是指回调方法是因为等到内核对象时间到了还是内核对象自己发出的。为false,代表回调方法之所以被调用,是因为内核对象发出信号。若为true,则代表是因为等待时间到时了。可以通过这个参数,判断是何种情况,参见最下方的例子

int millisecondsTimeOutInterval

代表回调方法的等待时间,一般是-1

bool executeOnlyOnce

是否只执行一次。
针对本方法,举例如下:

internal class RegisteredWaitHandleDemo
{
    public static void Main()
    {
        AutoResetEvent are = new AutoResetEvent(true);

        RegisteredWaitHandle rwh= ThreadPool.RegisterWaitForSingleObject(are,eventOperation,null,5000,false);

        Char opeartion = (Char) 0;
        while(opeartion!='Q')
        {
            Console.WriteLine("S=Signal,Q=Quit?");
            opeartion=Char.ToUpper(Console.ReadKey(true).KeyChar);
            if(opeartion=='S') are.Set(); 
        }

        rwh.Unregister(null); //注销的方法
    }

    private void eventOperation(object state,bool timeout)
    {
        Console.WriteLine(timeout?"TimeOut":"Event became true");
    }
}

四、内核模式小节
内核模式是最纠结的一部分,一是当时就没怎么整明白,要写出来必须自己得明白;二是临近过年,事情比较多。不过好在在春节前完成内核模式部分的读书感,也算没白读。
在读内核模式这部分时,作者给我的最大的感受就是:这个地方写的不是很好,若想了解详细内容,请查看我的《window 核心编程》,撩的我都很想买。要忍住!防止买书如山倒,读书如抽丝的事情。
现在2017-1-22 23:08:33,腊月二十五,以此纪念

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值