在上一篇C#多线程之基础篇2中,我们主要讲述了确定线程的状态、线程优先级、前台线程和后台线程以及向线程传递参数的知识,在这一篇中我们将讲述如何使用C#的lock关键字锁定线程、使用Monitor锁定线程以及线程中的异常处理。
九、使用C#的lock关键字锁定线程
1、使用Visual Studio 2015创建一个新的控制台应用程序。
2、双击打开“Program.cs”文件,然后修改为如下代码:
1 using System; 2 using System.Threading; 3 using static System.Console; 4 5 namespace Recipe09 6 { 7 abstract class CounterBase 8 { 9 public abstract void Increment(); 10 public abstract void Decrement(); 11 } 12 13 class Counter : CounterBase 14 { 15 public int Count { get; private set; } 16 17 public override void Increment() 18 { 19 Count++; 20 } 21 22 public override void Decrement() 23 { 24 Count--; 25 } 26 } 27 28 class CounterWithLock : CounterBase 29 { 30 private readonly object syncRoot = new Object(); 31 32 public int Count { get; private set; } 33 34 public override void Increment() 35 { 36 lock (syncRoot) 37 { 38 Count++; 39 } 40 } 41 42 public override void Decrement() 43 { 44 lock (syncRoot) 45 { 46 Count--; 47 } 48 } 49 } 50 51 class Program 52 { 53 static void TestCounter(CounterBase c) 54 { 55 for (int i = 0; i < 100000; i++) 56 { 57 c.Increment(); 58 c.Decrement(); 59 } 60 } 61 62 static void Main(string[] args) 63 { 64 WriteLine("Incorrect counter"); 65 var c1 = new Counter(); 66 var t1 = new Thread(() => TestCounter(c1)); 67 var t2 = new Thread(() => TestCounter(c1)); 68 var t3 = new Thread(() => TestCounter(c1)); 69 t1.Start(); 70 t2.Start(); 71 t3.Start(); 72 t1.Join(); 73 t2.Join(); 74 t3.Join(); 75 WriteLine($"Total count: {c1.Count}"); 76 77 WriteLine("--------------------------"); 78 79 WriteLine("Correct counter"); 80 var c2 = new CounterWithLock(); 81 t1 = new Thread(() => TestCounter(c2)); 82 t2 = new Thread(() => TestCounter(c2)); 83 t3 = new Thread(() => TestCounter(c2)); 84 t1.Start(); 85 t2.Start(); 86 t3.Start(); 87 t1.Join(); 88 t2.Join(); 89 t3.Join(); 90 WriteLine($"Total count: {c2.Count}"); 91 } 92 } 93 }
3、运行该控制台应用程序,运行效果(每次运行效果可能不同)如下图所示:
在第65行代码处,我们创建了Counter类的一个对象,该类定义了一个简单的counter变量,该变量可以自增1和自减1。然后在第66~68行代码处,我们创建了三个线程,并利用lambda表达式将Counter对象传递给了“TestCounter”方法,这三个线程共享同一个counter变量,并且对这个变量进行自增和自减操作,这将导致结果的不正确。如果我们多次运行这个控制台程序,它将打印出不同的counter值,有可能是0,但大多数情况下不是。
发生这种情况是因为Counter类是非线程安全的。我们假设第一个线程在第57行代码处执行完毕后,还没有执行第58行代码时,第二个线程也执行了第57行代码,这个时候counter的变量值自增了2次,然后,这两个线程同时执行了第58行处的代码,这会造成counter的变量只自减了1次,因此,造成了不正确的结果。
为了确保不发生上述不正确的情况,我们必须保证在某一个线程访问counter变量时,另外所有的线程必须等待其执行完毕才能继续访问,我们可以使用lock关键字来完成这个功能。如果我们在某个线程中锁定一个对象,其他所有线程必须等到该线程解锁之后才能访问到这个对象,因此,可以避免上述情况的发生。但是要注意的是,使用这种方式会严重影响程序的性能。更好的方式我们将会在仙童同步中讲述。
十、使用Monitor锁定线程
在这一小节中,我们将描述一个多线程编程中的常见的一个问题:死锁。我们首先创建一个死锁的示例,然后使用Monitor避免死锁的发生。
1、使用Visual Studio 2015创建一个新的控制台应用程序。
2、双击打开“Program.cs”文件,编写代码如下:
1 using System; 2 using System.Threading; 3 using static System.Console; 4 using static System.Threading.Thread; 5 6 namespace Recipe10 7 { 8 class Program 9 { 10 static void LockTooMuch(object lock1, object lock2) 11 { 12 lock (lock1) 13 { 14 Sleep(1000); 15 lock (lock2) 16 { 17 } 18 } 19 } 20 21 static void Main(string[] args) 22 { 23 object lock1 = new object(); 24 object lock2 = new object(); 25 26 new Thread(() => LockTooMuch(lock1, lock2)).Start(); 27 28 lock (lock2) 29 { 30 WriteLine("This will be a deadlock!"); 31 Sleep(1000); 32 lock (lock1) 33 { 34 WriteLine("Acquired a protected resource succesfully"); 35 } 36 } 37 } 38 } 39 }
3、运行该控制台应用程序,运行效果如下图所示:
在上述结果中我们可以看到程序发生了死锁,程序一直结束不了。
在第10~19行代码处,我们定义了一个名为“LockTooMuch”的方法,在该方法中我们锁定了第一个对象lock1,等待1秒钟后,希望锁定第二个对象lock2。
在第26行代码处,我们创建了一个新的线程来执行“LockTooMuch”方法,然后立即执行第28行代码。
在第28~32行代码处,我们在主线程中锁定了对象lock2,然后等待1秒钟后,希望锁定第一个对象lock1。
在创建的新线程中我们锁定了对象lock1,等待1秒钟,希望锁定对象lock2,而这个时候对象lock2已经被主线程锁定,所以新建线程会等待对象lock2被主线程解锁。然而,在主线程中,我们锁定了对象lock2,等待1秒钟,希望锁定对象lock1,而这个时候对象lock1已经被创建的线程锁定,所以主线程会等待对象lock1被创建的线程解锁。当发生这种情况的时候,死锁就发生了,所以我们的控制台应用程序目前无法正常结束。
4、要避免死锁的发生,我们可以使用“Monitor.TryEnter”方法来替换lock关键字,“Monitor.TryEnter”方法在请求不到资源时不会阻塞等待,可以设置超时时间,获取不到直接返回false。修改代码如下所示:
1 using System; 2 using System.Threading; 3 using static System.Console; 4 using static System.Threading.Thread; 5 6 namespace Recipe10 7 { 8 class Program 9 { 10 static void LockTooMuch(object lock1, object lock2) 11 { 12 lock (lock1) 13 { 14 Sleep(1000); 15 lock (lock2) 16 { 17 } 18 } 19 } 20 21 static void Main(string[] args) 22 { 23 object lock1 = new object(); 24 object lock2 = new object(); 25 26 new Thread(() => LockTooMuch(lock1, lock2)).Start(); 27 28 lock (lock2) 29 { 30 WriteLine("This will be a deadlock!"); 31 Sleep(1000); 32 //lock (lock1) 33 //{ 34 // WriteLine("Acquired a protected resource succesfully"); 35 //} 36 if (Monitor.TryEnter(lock1, TimeSpan.FromSeconds(5))) 37 { 38 WriteLine("Acquired a protected resource succesfully"); 39 } 40 else 41 { 42 WriteLine("Timeout acquiring a resource!"); 43 } 44 } 45 } 46 } 47 }
5、运行该控制台应用程序,运行效果如下图所示:
此时,我们的控制台应用程序就避免了死锁的发生。
十一、处理异常
在这一小节中,我们讲述如何在线程中正确地处理异常。正确地将try/catch块放置在线程内部是非常重要的,因为在线程外部捕获线程内部的异常通常是不可能的。
1、使用Visual Studio 2015创建一个新的控制台应用程序。
2、双击打开“Program.cs”文件,修改代码如下所示:
1 using System; 2 using System.Threading; 3 using static System.Console; 4 using static System.Threading.Thread; 5 6 namespace Recipe11 7 { 8 class Program 9 { 10 static void BadFaultyThread() 11 { 12 WriteLine("Starting a faulty thread..."); 13 Sleep(TimeSpan.FromSeconds(2)); 14 throw new Exception("Boom!"); 15 } 16 17 static void FaultyThread() 18 { 19 try 20 { 21 WriteLine("Starting a faulty thread..."); 22 Sleep(TimeSpan.FromSeconds(1)); 23 throw new Exception("Boom!"); 24 } 25 catch(Exception ex) 26 { 27 WriteLine($"Exception handled: {ex.Message}"); 28 } 29 } 30 31 static void Main(string[] args) 32 { 33 var t = new Thread(FaultyThread); 34 t.Start(); 35 t.Join(); 36 37 try 38 { 39 t = new Thread(BadFaultyThread); 40 t.Start(); 41 } 42 catch (Exception ex) 43 { 44 WriteLine(ex.Message); 45 WriteLine("We won't get here!"); 46 } 47 } 48 } 49 }
3、运行该控制台应用程序,运行效果如下图所示:
在第10~15行代码处,我们定义了一个名为“BadFaultyThread”的方法,在该方法中抛出一个异常,并且没有使用try/catch块捕获该异常。
在第17~29行代码处,我们定义了一个名为“FaultyThread”的方法,在该方法中也抛出一个异常,但是我们使用了try/catch块捕获了该异常。
在第33~35行代码处,我们创建了一个线程,在该线程中执行了“FaultyThread”方法,我们可以看到在这个新创建的线程中,我们正确地捕获了在“FaultyThread”方法中抛出的异常。
在第37~46行代码处,我们又新创建了一个线程,在该线程中执行了“BadFaultyThread”方法,并且在主线程中使用try/catch块来捕获在新创建的线程中抛出的异常,不幸的的是我们在主线程中无法捕获在新线程中抛出的异常。
由此可以看到,在一个线程中捕获另一个线程中的异常通常是不可行的。
至此,多线程(基础篇)我们就讲述到这儿,之后我们将讲述线程同步相关的知识,敬请期待!