https://blog.csdn.net/qq_42537006/article/details/104949841?spm=1001.2101.3001.6650.1&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7Edefault-1.no_search_link&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7Edefault-1.no_search_link
文章目录
一、前言
二、线程锁的类型
1. volatile关键字
2. Lock锁
3. System.Threading.Interlocked
4. Monitor
5. Mutex
6. ReaderWriterLock
7. 线程同步事件AutoResetEvent和ManualResetEvent
三、实例代码测试
1. Lock锁
Lock的测试总结:
2. Monitor用法
3. System.Threading.Interlocked
3. Mutex用法
4. ReaderWriterLock用法
5. 7, 同步事件AutoResetEvent和ManualResetEvent用法
一、前言
线程不是一个计算机硬件的功能,而是操作系统提供的一种逻辑功能,线程本质上是进程中一段并发运行的代码,所以线程需要操作系统投入CPU资源来运行和调度。
线程,有时被称为轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元。一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组成。
在讲述线程锁之前,我们先了解一下什么是线程同步?
线程同步----在多线程程序中,会出现多个线程抢占一个资源的情况,这时间有可能会造成冲突,也就是一个线程可能还没来得及将更改的 资源保存,另一个线程的更改就开始了。可能造成数据不一致。因此引入多线程同步,也就是说多个线程只能一个对共享的资源进行更改,其他线程不能对数据进行修改。
为了解决多个线程会占用一个资源的问题,我们会使用线程锁来解决。
二、线程锁的类型
常用的线程锁分为一下七种:volatile关键字、Lock锁、System.Threading.Interlocked原子级别操作、Monitor、Metux、ReaderWriterLock、EventWaitHandle同步事件。下面我们对这六种做详细的介绍:
1. volatile关键字
volatile 并没有实现真正的线程同步,操作级别停留在变量级别并非原子级别,对于单系统处理器中,变量存储在主内存中,没有机会被别人修改。但是如果是多处理器,可能就会有问题,因为每个处理器都有单独的data cash,数据更新不一定立刻被写回到主存,可能会造成不同步
2. Lock锁
Lock锁为操作的代码块添加互斥对象,如果A线程正在访问,对象没有到达临界区,则B线程不会访问。不推荐使用Lock(this)的方式最为Lock锁,因为你不确定别是是否重新实例了含有Lock的对象。
对于Lock锁有以下建议
如果一个类的实例是public的,最好不要lock(this)。因为使用你的类的人也许不知道你用了lock,如果他new了一个实例,并且对这个实例上锁,就很容易造成死锁
如果MyType是public的,不要lock(typeof(MyType))
不要Lock一个字符串
3. System.Threading.Interlocked
Interlocked类即互锁操作,是对某个内存位置的原子操作,在大多数计算机上,增加变量的操作都不是原子操作,需要以下三步完成:
将实例变量中的值加载到寄存器
在寄存器中加减该值
将加减后的值保存到实体变量中
线程可能在执行完前两步时,被夺走了CPU的时间,另一个线程继续对同一个变量操作,造成第一个线程继续执行加减操作时,操作结果不准确。这是更微观的表现,
Interlocked类则提供了4种方法进行原子级别的变量操作。Increment , Decrement , Exchange 和CompareExchange 。使用Increment 和Decrement 可以保证对一个整数的加减为一个原子操作。Exchange 方法自动交换指定变量的值。
CompareExchange 方法组合了两个操作:比较两个值以及根据比较的结果将第三个值存储在其中一个变量中。比较和交换操作也是按原子操作执行的。
Interlocked.CompareExchange(ref a, b, c); 原子操作,a参数和c参数比较, 相等b替换a,不相等不替换。
4. Monitor
Monitor和Lock的方法类似,但是Monitor可以更好的保护功能块,通过Monitor.Enter可以占有对Obiect的使用权限,使用Monitor.Exit可以释放此权限,Monitor类同时提供了TryEnter(Object o,[int])的一个重载方法,该方法尝试获取o对象的独占权, ,当获取独占权失败时,将返回false。
Lock可以当成Monitor封装后的方法,使用起来更简单方便,而且使用Monitor如果占有资源后报错,没有使用Finally调用Monitor.Exit释放资源,会导致其它线程锁死,所以使用Lock更为简单方便,Monitor还提供了三个静态方法Monitor.Pulse(Object o),Monitor.PulseAll(Object o)和Monitor.Wait(Object o ) ,用来实现一种唤醒机制的同步。
5. Mutex
Mutex和Monitor很接近,但是没有Monitor.Pulse,Wait,PulseAll的唤醒功能,他的优点是可以跨进程,可以在同一台机器甚至远程机器人的不同进程间共用一个互斥体,当然也可以用于线程同步,不过因为它是win32封装的,所以他需要互操作转换,会消耗更多的资源。
6. ReaderWriterLock
如果我们仅仅是获取某个资源,并不会很频繁的对资源进行修改,那么占有获取权限则很浪费时间。那么net中的ReaderWriteLock提供了一种方法,当没有获得写的权限时,可以获得多个读的权限,当已经在执行写的权限时,则无法获得读取的权限,当写入完成之后,才能继续读取。
7. 线程同步事件AutoResetEvent和ManualResetEvent
同:
都是将布尔变量传递给构造函数控制初始状态,True则非堵塞,False则为堵塞
异:
虽然他们都可以使用Set()解除堵塞,但是AutoResetEvent每执行一次会自动Reset(),Autoevent.WaitOne()每次只允许一个线程。
ManualResetEvent则需要手动需要手动Reset(),所以Manualevent则可以唤醒多个线程,如果Manualevent.Set,信号被触发后,此事件则一直通路,直到手动Reset()才会继续堵塞
三、实例代码测试
下面对以上几种方法中,比较常用的进行测试,并对比。
1. Lock锁
Lock主要针对Object对象的几种定义进行测试,分为一下几种情况
单实例化的多线程调用Lock
Locker LockTest1 = new Locker("LockTest1");
Thread GainMsg1 = new Thread(new ThreadStart(LockTest1.Work));
GainMsg1.Name = "GainMsg1";
Thread GainMsg2 = new Thread(new ThreadStart(LockTest1.Work));
GainMsg2.Name = "GainMsg2";
GainMsg1.Start();
GainMsg2.Start();
全局Public 私有Private
静态 Public static Object = new object() Private static Object = new object()
非静态 public Object obj = new object() Private Object = new object()
测试结果:
全局Public 私有Private
静态 OK OK
非静态 OK OK
多实例化的Lock
Locker LockTest1 = new Locker("LockTest1");
Locker LockTest2 = new Locker("LockTest2");
Thread GainMsg1 = new Thread(new ThreadStart(LockTest1.Work));
GainMsg1.Name = "GainMsg1";
Thread GainMsg2 = new Thread(new ThreadStart(LockTest2.Work));
GainMsg2.Name = "GainMsg2";
GainMsg1.Start();
GainMsg2.Start();
全局Public 私有Private
静态 Public static Object = new object() Private static Object = new object()
非静态 public Object obj = new object() Private Object = new object()
测试结果:
全局Public 私有Private
静态 OK OK
非静态 NG NG
Lock的测试总结:
单实例的情况下,因为只有一个Object对象,所以不论是否静态,是否全局都可以单一访问,实现线程锁
多实例的情况下,因为静态的Obejct只有一个,所以不受实例化的影响,能够完成单一访问,实现线程锁
但是对于非静态的Object,由于实例化了两个对象,如果对同一对象单一访问可以实现线程锁,如果分别访问两个对象,则不能实现线程锁的功能。
2. Monitor用法
System.Threading.Monitor.Enter(obj); //加锁
1
System.Threading.Monitor.Exit(obj); //解锁,释放资源
1
System.Threading.Monitor.Enter(obj);
num = 0;
for (int i = 0; i < 10; i++)
{
num += 1;
msg = string.Format("线程 [{0}],实例[{1}]中num的值是[{2}]", Thread.CurrentThread.Name, this.Name, num);
Console.WriteLine(msg);
Thread.Sleep(1000);
}
Console.WriteLine("===线程[{0}] 执行完毕===", Thread.CurrentThread.Name);
System.Threading.Monitor.Exit(obj);
3. System.Threading.Interlocked
Increment:增量 +1
Decrement :增量 -1
Exchange :自动交换指定变量的值
CompareExchange :比较两个值以及根据比较的结果将第三个值存储在其中一个变量中
3. Mutex用法
线程使用Mutex.WaitOne()方法等待C# Mutex对象被释放,如果它等待的C# Mutex对象被释放了,它就自动拥有这个对象,直到它调用Mutex.ReleaseMutex()方法释放这个对象,而在此期间,其他想要获取这个C# Mutex对象的线程都只有等待。
public void Work()
{
mutex.WaitOne();
num = 0;
for (int i = 0; i < 10; i++)
{
num += 1;
msg = string.Format("线程 [{0}],实例[{1}]中num的值是[{2}]", Thread.CurrentThread.Name, this.Name, num);
Console.WriteLine(msg);
Thread.Sleep(1000);
}
Console.WriteLine("===线程[{0}] 执行完毕===", Thread.CurrentThread.Name);
mutex.ReleaseMutex();
}
4. ReaderWriterLock用法
m_readerWriterLock.AcquireReaderLock(-1);//获取读取的权限,-1代表无限时等待,每次获取完毕后,记得释放.此权限可以被多线程同时获取,也就是“多读”。******如果不是释放,写入的权限将无法被获取**********
m_readerWriterLock.ReleaseReaderLock();//释放读取权限资源
m_readerWriterLock.AcquireWriterLock(-1);//获取写入的权限,-1代表无限时等待,每次写入完毕后,记得释放******如果不是放,其它线程将无法读取**********
public void Read()
{
for (int i = 0; i < 10; i++)
{
Console.WriteLine("ThreadName " + Thread.CurrentThread.Name + " AcquireReaderLock");
m_readerWriterLock.AcquireReaderLock(-1);
num += 1;
msg = string.Format("线程 [{0}],实例[{1}]中num的值是[{2}]", Thread.CurrentThread.Name, this.Name, num);
Console.WriteLine(msg);
Thread.Sleep(1000);
m_readerWriterLock.ReleaseReaderLock();
}
Console.WriteLine("===线程[{0}] 执行完毕===", Thread.CurrentThread.Name);
}
public void Writer()
{
Console.WriteLine("ThreadName " + Thread.CurrentThread.Name + " AcquireWriterLock");
m_readerWriterLock.AcquireWriterLock(-1);
num = 1000;
msg = string.Format("线程 [{0}],实例[{1}]中num的值是[{2}]", Thread.CurrentThread.Name, this.Name, num);
Console.WriteLine(msg);
Thread.Sleep(3000);
Console.WriteLine("===线程[{0}] 执行完毕===", Thread.CurrentThread.Name);
m_readerWriterLock.ReleaseWriterLock();
}
测试效果
从打印信息可以看出。执行获取写入权限时,读取的权限无法被获取,会堵塞读取功能。反之如果读取的权限没有被释放,写入的权限也无法被获取。所以一定要养成用完即弃的习惯,避免死锁发生
5. 7, 同步事件AutoResetEvent和ManualResetEvent用法
声明Auto和Manual事件时的起始布尔量设置为True,表示信号为通。设置为False信息为堵塞
public static EventWaitHandle WaitAutoTest = new AutoResetEvent(false);//线程
public static EventWaitHandle WaitManualTest = new ManualResetEvent(false);// 运动指令的堵塞
测试AutoResetEvent建立了两个单独的线程,方法分别是AutoTest1()、AutoTest2(),线程启动后,他们都会被Form1.WaitAutoTest.WaitOne();指令堵塞。
点击AutoResetEvent按钮后会执行 WaitAutoTest.Set();将信号置为通
private void Btn_Auto_Click(object sender, EventArgs e)
{
WaitAutoTest.Set();
}
此时只有一个线程会获得通路,然后会再次将信号置为False。当通路的线程运行完毕后会再次 执行WaitAutoTest.Set(),另一个线程则会通路。
public void AutoTest1()
{
Form1.WaitAutoTest.WaitOne();
int num = 0;
for (int i = 0; i < 10; i++)
{
num += 1;
msg = string.Format("线程 [{0}],实例[{1}]中num的值是[{2}]", Thread.CurrentThread.Name, this.Name, num);
if (dataReceive != null) dataReceive(msg);
//Console.WriteLine(msg);
Thread.Sleep(1000);
}
dataReceive("");
dataReceive(string.Format("===线程[{0}] 执行完毕===", Thread.CurrentThread.Name));
dataReceive("");
Form1.WaitAutoTest.Set();
}
public void AutoTest2()
{
Form1.WaitAutoTest.WaitOne();
int num = 0;
for (int i = 0; i < 10; i++)
{
num += 1;
msg = string.Format("线程 [{0}],实例[{1}]中num的值是[{2}]", Thread.CurrentThread.Name, this.Name, num);
if (dataReceive != null) dataReceive(msg);
Thread.Sleep(1000);
}
dataReceive("");
dataReceive(string.Format("===线程[{0}] 执行完毕===", Thread.CurrentThread.Name));
dataReceive("");
Form1.WaitAutoTest.Set();
}
AutoResetEvent按钮测试结果如图
可以看出执行一次WaitAutoTest.Set();只有一个线程获得了通路的机会,另个一个线程仍然处于堵塞状态。
测试ManualResetEvent同样建立了两个单独的线程,方法分别是ManualTest1()、ManualTest2(),线程启动后,他们都会被Form1.WaitManualTest.WaitOne();指令堵塞。
private void Btn_Manual_Click(object sender, EventArgs e)
{
WaitManualTest.Set();
}
点击ManualResetEvent按钮之后,会触发 WaitManualTest.Set();通线程唤醒。唤醒之后除非手动执行Reset().否则就会一直处于通路状态
public void ManualTest1()
{
Form1.WaitManualTest.WaitOne();
int num = 0;
for (int i = 0; i < 10; i++)
{
num += 1;
msg = string.Format("线程 [{0}],实例[{1}]中num的值是[{2}]", Thread.CurrentThread.Name, this.Name, num);
if (dataReceive != null) dataReceive(msg);
//Console.WriteLine(msg);
Thread.Sleep(1000);
}
dataReceive("");
dataReceive(string.Format("===线程[{0}] 执行完毕===", Thread.CurrentThread.Name));
dataReceive("");
Form1.WaitManualTest.Set();
}
public void ManualTest2()
{
Form1.WaitManualTest.WaitOne();
int num = 0;
for (int i = 0; i < 10; i++)
{
num += 1;
msg = string.Format("线程 [{0}],实例[{1}]中num的值是[{2}]", Thread.CurrentThread.Name, this.Name, num);
if (dataReceive != null) dataReceive(msg);
Thread.Sleep(1000);
}
dataReceive("");
dataReceive(string.Format("===线程[{0}] 执行完毕===", Thread.CurrentThread.Name));
dataReceive("");
Form1.WaitManualTest.Set();
}
ManualResetEvent按钮测试结果如图
可以看出执行一次WaitManualTest.Set();两个线程同时解除了堵塞,同时开始运行,所以ManualTest事件可以同时唤醒多个线程的作用便是如此。
测试使用的代码见链接:
链接: 资源包含内容如下图所示.
————————————————
版权声明:本文为CSDN博主「顶着太阳去烧烤」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_42537006/article/details/104949841