首页 > Python基础教程 >
-
C#教程之C#多线程编程系列(二)- 线程基础(2)
运行结果如下图所示,与预期结果相符。
1.10 C# Lock关键字的使用#
在多线程的系统中,由于CPU的时间片轮转等线程调度算法的使用,容易出现线程安全问题。具体可参考《深入理解计算机系统》
一书相关的章节。
在C#中lock
关键字是一个语法糖,它将Monitor
封装,给object加上一个互斥锁,从而实现代码的线程安全,Monitor
会在下一节中介绍。
对于lock
关键字还是Monitor
锁定的对象,都必须小心选择,不恰当的选择可能会造成严重的性能问题甚至发生死锁。以下有几条关于选择锁定对象的建议。
- 同步锁定的对象不能是值类型。因为使用值类型时会有装箱的问题,装箱后的就成了一个新的实例,会导致
Monitor.Enter()
和Monitor.Exit()
接收到不同的实例而失去关联性- 避免锁定
this、typeof(type)和string
。this
和typeof(type)
锁定可能在其它不相干的代码中会有相同的定义,导致多个同步块互相阻塞。string
需要考虑字符串拘留的问题,如果同一个字符串常量在多个地方出现,可能引用的会是同一个实例。- 对象的选择作用域尽可能刚好达到要求,使用静态的、私有的变量。
以下演示代码实现了多线程情况下的计数功能,一种实现是线程不安全的,会导致结果与预期不相符,但也有可能正确。另外一种使用了lock
关键字进行线程同步,所以它结果是一定的。
static void Main(string[] args) { Console.WriteLine("错误的多线程计数方式"); var c = new Counter(); // 开启3个线程,使用没有同步块的计数方式对其进行计数 var t1 = new Thread(() => TestCounter(c)); var t2 = new Thread(() => TestCounter(c)); var t3 = new Thread(() => TestCounter(c)); t1.Start(); t2.Start(); t3.Start(); t1.Join(); t2.Join(); t3.Join(); // 因为多线程 线程抢占等原因 其结果是不一定的 碰巧可能为0 Console.WriteLine($"Total count: {c.Count}"); Console.WriteLine("--------------------------"); Console.WriteLine("正确的多线程计数方式"); var c1 = new CounterWithLock(); // 开启3个线程,使用带有lock同步块的方式对其进行计数 t1 = new Thread(() => TestCounter(c1)); t2 = new Thread(() => TestCounter(c1)); t3 = new Thread(() => TestCounter(c1)); t1.Start(); t2.Start(); t3.Start(); t1.Join(); t2.Join(); t3.Join(); // 其结果是一定的 为0 Console.WriteLine($"Total count: {c1.Count}"); Console.ReadLine(); } static void TestCounter(CounterBase c) { for (int i = 0; i < 100000; i++) { c.Increment(); c.Decrement(); } } // 线程不安全的计数 class Counter : CounterBase { public int Count { get; private set; } public override void Increment() { Count++; } public override void Decrement() { Count--; } } // 线程安全的计数 class CounterWithLock : CounterBase { private readonly object _syncRoot = new Object(); public int Count { get; private set; } public override void Increment() { // 使用Lock关键字 锁定私有变量 lock (_syncRoot) { // 同步块 Count++; } } public override void Decrement() { lock (_syncRoot) { Count--; } } } abstract class CounterBase { public abstract void Increment(); public abstract void Decrement(); }
运行结果如下图所示,与预期结果相符。
1.11 使用Monitor类锁定资源#
Monitor
类主要用于线程同步中, lock
关键字是对Monitor
类的一个封装,其封装结构如下代码所示。
try { Monitor.Enter(obj); dosomething(); } catch(Exception ex) { } finally { Monitor.Exit(obj); }
以下代码演示了使用Monitor.TyeEnter()
方法避免资源死锁和使用lock
发生资源死锁的场景。
static void Main(string[] args) { object lock1 = new object(); object lock2 = new object(); new Thread(() => LockTooMuch(lock1, lock2)).Start(); lock (lock2) { Thread.Sleep(1000); Console.WriteLine("Monitor.TryEnter可以不被阻塞, 在超过指定时间后返回false"); // 如果5S不能进入同步块,那么返回。 // 因为前面的lock锁定了 lock2变量 而LockTooMuch()一开始锁定了lock1 所以这个同步块无法获取 lock1 而LockTooMuch方法内也不能获取lock2 // 只能等待TryEnter超时 释放 lock2 LockTooMuch()才会是释放 lock1 if (Monitor.TryEnter(lock1, TimeSpan.FromSeconds(5))) { Console.WriteLine("获取保护资源成功"); } else { Console.WriteLine("获取资源超时"); } } new Thread(() => LockTooMuch(lock1, lock2)).Start(); Console.WriteLine("----------------------------------"); lock (lock2) { Console.WriteLine("这里会发生资源死锁"); Thread.Sleep(1000); // 这里必然会发生死锁 // 本同步块 锁定了 lock2 无法得到 lock1 // 而 LockTooMuch 锁定了 lock1 无法得到 lock2 lock (lock1) { // 该语句永远都不会执行 Console.WriteLine("获取保护资源成功"); } } } static void LockTooMuch(object lock1, object lock2) { lock (lock1) { Thread.Sleep(1000); lock (lock2) ; } }
运行结果如下图所示,因为使用Monitor.TryEnter()
方法在超时以后会返回,不会阻塞线程,所以没有发生死锁。而第二段代码中lock
没有超时返回的功能,导致资源死锁,同步块中的代码永远不会被执行。
1.12 多线程中处理异常#
在多线程中处理异常应当使用就近原则,在哪个线程发生异常那么所在的代码块一定要有相应的异常处理。否则可能会导致程序崩溃、数据丢失。
主线程中使用try/catch
语句是不能捕获创建线程中的异常。但是万一遇到不可预料的异常,可通过监听AppDomain.CurrentDomain.UnhandledException
事件来进行捕获和异常处理。
演示代码如下所示,异常处理 1 和 异常处理 2 能正常被执行,而异常处理 3 是无效的。
static void Main(string[] args) { // 启动线程,线程代码中进行异常处理 var t = new Thread(FaultyThread); t.Start(); t.Join(); // 捕获全局异常 AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; t = new Thread(BadFaultyThread); t.Start(); t.Join(); // 线程代码中不进行异常处理,尝试在主线程中捕获 AppDomain.CurrentDomain.UnhandledException -= CurrentDomain_UnhandledException; try { t = new Thread(BadFaultyThread); t.Start(); } catch (Exception ex) { // 永远不会运行 Console.WriteLine($"异常处理 3 : {ex.Message}"); } } private static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e) { Console.WriteLine($"异常处理 2 :{(e.ExceptionObject as Exception).Message}"); } static void BadFaultyThread() { Console.WriteLine("有异常的线程已启动..."); Thread.Sleep(TimeSpan.FromSeconds(2)); throw new Exception("Boom!"); } static void FaultyThread() { try { Console.WriteLine("有异常的线程已启动..."); Thread.Sleep(TimeSpan.FromSeconds(1)); throw new Exception("Boom!"); } catch (Exception ex) { Console.WriteLine($"异常处理 1 : {ex.Message}"); } }
运行结果如下图所示,与预期结果一致。
参考书籍
本文主要参考了以下几本书,在此对这些作者表示由衷的感谢你们提供了这么好的资料。
- 《CLR via C#》
- 《C# in Depth Third Edition》
- 《Essential C# 6.0》
- 《Multithreading with C# Cookbook Second Edition》
线程基础这一章节终于整理完了,是笔者学习过程中的笔记和思考。计划按照《Multithreading with C# Cookbook Second Edition》这本书的结构,一共更新十二个章节,先立个Flag。
源码下载点击链接 示例源码下载
笔者水平有限,如果错误欢迎各位批评指正!
作者:InCerry
出处:https://www.cnblogs.com/InCerry/p/9404030.html