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

2024-01-28 14:40

本文主要是介绍《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,解除阻塞,总是返回truepublic 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 Mutexpublic void Method1(){m_lock.WaitOne();Method2();m_lock.ReleaseMutex();}public void Method2(){m_lock.WaitOne();//Do somethingm_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,腊月二十五,以此纪念

这篇关于《CLR via C#》读书笔记-线程同步(三)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/653934

相关文章

C#特性(Attributes)和反射(Reflection)详解

《C#特性(Attributes)和反射(Reflection)详解》:本文主要介绍C#特性(Attributes)和反射(Reflection),具有很好的参考价值,希望对大家有所帮助,如有错误... 目录特性特性的定义概念目的反射定义概念目的反射的主要功能包括使用反射的基本步骤特性和反射的关系总结特性

C#实现查找并删除PDF中的空白页面

《C#实现查找并删除PDF中的空白页面》PDF文件中的空白页并不少见,因为它们有可能是作者有意留下的,也有可能是在处理文档时不小心添加的,下面我们来看看如何使用Spire.PDFfor.NET通过C#... 目录安装 Spire.PDF for .NETC# 查找并删除 PDF 文档中的空白页C# 添加与删

通过C#获取Excel单元格的数据类型的方法详解

《通过C#获取Excel单元格的数据类型的方法详解》在处理Excel文件时,了解单元格的数据类型有助于我们正确地解析和处理数据,本文将详细介绍如何使用FreeSpire.XLS来获取Excel单元格的... 目录引言环境配置6种常见数据类型C# 读取单元格数据类型引言在处理 Excel 文件时,了解单元格

C#实现高性能Excel百万数据导出优化实战指南

《C#实现高性能Excel百万数据导出优化实战指南》在日常工作中,Excel数据导出是一个常见的需求,然而,当数据量较大时,性能和内存问题往往会成为限制导出效率的瓶颈,下面我们看看C#如何结合EPPl... 目录一、技术方案核心对比二、各方案选型建议三、性能对比数据四、核心代码实现1. MiniExcel

MySQL主从同步延迟问题的全面解决方案

《MySQL主从同步延迟问题的全面解决方案》MySQL主从同步延迟是分布式数据库系统中的常见问题,会导致从库读取到过期数据,影响业务一致性,下面我将深入分析延迟原因并提供多层次的解决方案,需要的朋友可... 目录一、同步延迟原因深度分析1.1 主从复制原理回顾1.2 延迟产生的关键环节二、实时监控与诊断方案

在.NET平台使用C#为PDF添加各种类型的表单域的方法

《在.NET平台使用C#为PDF添加各种类型的表单域的方法》在日常办公系统开发中,涉及PDF处理相关的开发时,生成可填写的PDF表单是一种常见需求,与静态PDF不同,带有**表单域的文档支持用户直接在... 目录引言使用 PdfTextBoxField 添加文本输入域使用 PdfComboBoxField

C#如何调用C++库

《C#如何调用C++库》:本文主要介绍C#如何调用C++库方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录方法一:使用P/Invoke1. 导出C++函数2. 定义P/Invoke签名3. 调用C++函数方法二:使用C++/CLI作为桥接1. 创建C++/CL

C#使用StackExchange.Redis实现分布式锁的两种方式介绍

《C#使用StackExchange.Redis实现分布式锁的两种方式介绍》分布式锁在集群的架构中发挥着重要的作用,:本文主要介绍C#使用StackExchange.Redis实现分布式锁的... 目录自定义分布式锁获取锁释放锁自动续期StackExchange.Redis分布式锁获取锁释放锁自动续期分布式

JAVA保证HashMap线程安全的几种方式

《JAVA保证HashMap线程安全的几种方式》HashMap是线程不安全的,这意味着如果多个线程并发地访问和修改同一个HashMap实例,可能会导致数据不一致和其他线程安全问题,本文主要介绍了JAV... 目录1. 使用 Collections.synchronizedMap2. 使用 Concurren

C# foreach 循环中获取索引的实现方式

《C#foreach循环中获取索引的实现方式》:本文主要介绍C#foreach循环中获取索引的实现方式,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧... 目录一、手动维护索引变量二、LINQ Select + 元组解构三、扩展方法封装索引四、使用 for 循环替代