上一篇文章主要带领大家认识了线程,也了解到了线程的基本用法和状态,接下来就让我们一起学习下什么是线程同步。
线程中异常的处理
在线程中始终使用try/catch代码块是非常重要的,因为不可能在线程代码之外来捕获到异常。
可以阅读下面的代码,这块是做的验证,证明在线程之外捕获异常是错误的选择,应该在线程中时时刻刻都使用异常处理机制。
static void Main(string[] args) { Thread twoThread = new Thread(TwoMethod); twoThread.Start(); twoThread.Join(); try { Thread oneThread = new Thread(OneMethod); oneThread.Start(); } catch (Exception ex) { Console.WriteLine("外部捕获线程one的异常:"+ex.Message); } Console.ReadKey(); } static void OneMethod() { Console.WriteLine("Start OneMethod"); Thread.Sleep(TimeSpan.FromSeconds(2)); throw new Exception("异常01"); } static void TwoMethod() { try { Console.WriteLine("Start TwoMethod"); Thread.Sleep(TimeSpan.FromSeconds(1)); throw new Exception("异常02"); } catch (Exception ex) { Console.WriteLine(ex.Message); } }
下面图片是输出报错的结果,可以看到OneThread的异常没有被外部的try/catch捕获到,导致直接在线程内部提示错误,导致程序崩溃。
看到这个情况,那么我们在以后使用线程的时候,就需要特别的注意,一定要在线程中进行异常的处理和捕获,千万别遗留任何未处理的异常,因为如果线程中有未被处理的异常会导致整个程序都会受到影响,可能导致整个软件崩溃。
线程同步
在上一篇推文中,我们了解到了Lock加锁的机制,它是可以保证将某个变量或者某个模块锁住,当出现多个线程同时访问时锁就会起作用,只允许一个线程访问,其余的等待,其访问完后其余的才可以进行访问。
但是这种机制有一定的局限性,在多核CPU设备中,让其余线程等待是极大浪费资源的,而且这种解决办法会导致死锁的现象。
上面说的也就是所谓的竞争条件问题的解决方法,导致这个问题的原因是多线程的执行并没有正确同步。当一个线程执行递增和递减操作时,其他线程需要依次等待。这种常见问题通常被称为线程同步。这种问题出现在当线程中有共享资源或者对象时,才会进行线程同步,如果无共享对象,则无需进行线程同步。
当在线程中有共享资源时,可使用下面的两种方式进行处理。
一、原子操作
来实现对共享资源的访问。其实就是一个操作只占用一个量子的时间,一次就可以完成。也就是说只有当前操作完成后,其他线程才能执行其他操作。这样就避免了使用锁,排除了死锁的情况。
原子操作就是使用C#系统自带的Interlocked类来对线程不安全的对象进行处理,借助Interlocked类,无需锁定任何对象即可获取到正确的结果,Interlocked类提供Increment,Decrement和Add等基本数学操作的原子方法,从而可以帮助我们无需使用锁🔒,避免出现各种死锁问题。
/// <summary> /// 线程不安全 /// </summary> class Counter { private int _count; public int Count { get { return _count; } } public void Add() { _count++; } public void Delete() { _count--; } }
/// <summary> /// 不加锁 ,但线程是安全的。 /// 可避免在线程中出现死锁 /// </summary> class CounterNoLock { private int _count; public int Count { get { return _count; } } public void Add() { Interlocked.Increment(ref _count); //递增 } public void Delete() { Interlocked.Decrement(ref _count); //递减 } }
二、将等待的线程置于阻塞状态
这个处理方法是在第一个原子操作无效切程序的逻辑更加复杂的情况下才使用的,用于协调线程。
当线程处理阻塞状态时,只会占用尽可能少的CPU时间,这就意味着将引入至少一次所谓的上下文切换。
上下文切换:指操作系统的线程调度器,该调度器会保持等待的线程的状态,并切换到另一个线程,依次恢复等待的线程状态。虽然会消耗极大的资源,但是如果线程被挂起很长时间这么做是值得的。这种也就内核模式,因为只有操作系统的内核才能阻止线程使用CPU时间。
用户模式: 如果线程只是等候一小会,那最好只是简单的等待,而不用将线程切换到阻塞状态。还有一种为混合模式,也就是先尝试使用用户模式,如果线程等候时间过长,则会切换到阻塞状态以节省CPU资源。
下面的DEMO主要介绍SemaphoreSlim类,该类用于限制了同时访问同一个资源的线程数量。
static void Main(string[] args) { for (int i = 1; i <=6; i++) { string threadName = "Thread " + i; int secondsWait = 2; var thread = new Thread(( )=>DataConnect(threadName,secondsWait)); thread.Start(); } Console.ReadKey(); } static SemaphoreSlim _semaphore = new SemaphoreSlim(4); //默认4个线程可同时访问 static void DataConnect(string name,int seconds) { Console.WriteLine("wait 线程的名字:",name); _semaphore.Wait(); Console.WriteLine("Connect 线程的名字:" + name); Thread.Sleep(TimeSpan.FromSeconds(seconds)); _semaphore.Release(); }
上面的代码利用SemaphoreSlim类,设置其构造函数为4,也就是其指定允许的并发线程数量。
上面使用信号系统限制了访问数据连接的并发数为4个,当有4个线程进行访问时,其他两个线程需要等待,知道之前线程中某一个完成工作并调用Relece方法来发出信号。