在多线程(线程同步)中,我们将学习多线程中操作共享资源的技术,学习到的知识点如下所示:
- 执行基本的原子操作
- 使用Mutex构造
- 使用SemaphoreSlim构造
- 使用AutoResetEvent构造
- 使用ManualResetEventSlim构造
- 使用CountdownEvent构造
- 使用Barrier构造
- 使用ReaderWriterLockSlim构造
- 使用SpinWait构造
一、执行基本的原子操作
在这一小节中,我们将学习如何在没有阻塞线程(blocking threads)发生的情况下,在一个对象上执行基本的原子操作并能阻止竞争条件(race condition)的发生。操作步骤如下所示:
1、使用Visual Studio 2015创建一个新的控制台应用程序。
2、双击打开“Program.cs”文件,编写代码如下所示:
1 using System; 2 using System.Threading; 3 using static System.Console; 4 5 namespace Recipe01 6 { 7 abstract class CounterBase 8 { 9 public abstract void Increment(); 10 11 public abstract void Decrement(); 12 } 13 14 class Counter : CounterBase 15 { 16 private int count; 17 18 public int Count => count; 19 20 public override void Increment() 21 { 22 count++; 23 } 24 25 public override void Decrement() 26 { 27 count--; 28 } 29 } 30 31 class CounterNoLock : CounterBase 32 { 33 private int count; 34 35 public int Count => count; 36 37 public override void Increment() 38 { 39 Interlocked.Increment(ref count); 40 } 41 42 public override void Decrement() 43 { 44 Interlocked.Decrement(ref count); 45 } 46 } 47 48 class Program 49 { 50 static void TestCounter(CounterBase c) 51 { 52 for (int i = 0; i < 100000; i++) 53 { 54 c.Increment(); 55 c.Decrement(); 56 } 57 } 58 59 static void Main(string[] args) 60 { 61 WriteLine("Incorrect counter"); 62 63 var c1 = new Counter(); 64 65 var t1 = new Thread(() => TestCounter(c1)); 66 var t2 = new Thread(() => TestCounter(c1)); 67 var t3 = new Thread(() => TestCounter(c1)); 68 t1.Start(); 69 t2.Start(); 70 t3.Start(); 71 t1.Join(); 72 t2.Join(); 73 t3.Join(); 74 75 WriteLine($"Total count: {c1.Count}"); 76 WriteLine("--------------------------"); 77 78 WriteLine("Correct counter"); 79 80 var c2 = new CounterNoLock(); 81 82 t1 = new Thread(() => TestCounter(c2)); 83 t2 = new Thread(() => TestCounter(c2)); 84 t3 = new Thread(() => TestCounter(c2)); 85 t1.Start(); 86 t2.Start(); 87 t3.Start(); 88 t1.Join(); 89 t2.Join(); 90 t3.Join(); 91 92 WriteLine($"Total count: {c2.Count}"); 93 } 94 } 95 }
3、运行该控制台应用程序,运行效果(每次运行效果可能不同)如下图所示:
在第63行代码处,我们创建了一个非线程安全的Counter类的一个对象c1,由于它是非线程安全的,因此会发生竞争条件(race condition)。
在第65~67行代码处,我们创建了三个线程来运行c1对象的“TestCounter”方法,在该方法中,我们按顺序对c1对象的count变量执行自增和自减操作。由于c1不是线程安全的,因此在这种情况下,我们得到的counter值是不确定的,我们可以得到0值,但多运行几次,多数情况下会得到不是0值得错误结果。
在多线程(基础篇)中,我们使用lock关键字锁定对象来解决这个问题,但是使用lock关键字会造成其他线程的阻塞。但是,在本示例中我们没有使用lock关键字,而是使用了Interlocked构造,它对于基本的数学操作提供了自增(Increment)、自减(Decrement)以及其他一些方法。
二、使用Mutex构造
在这一小节中,我们将学习如何使用Mutex构造同步两个单独的程序,即进程间的同步。具体步骤如下所示:
1、使用Visual Studio 2015创建一个新的控制台应用程序。
2、双击打开“Program.cs”文件,编写代码如下所示:
1 using System; 2 using System.Threading; 3 using static System.Console; 4 5 namespace Recipe02 6 { 7 class Program 8 { 9 static void Main(string[] args) 10 { 11 const string MutexName = "Multithreading"; 12 13 using (var m = new Mutex(false, MutexName)) 14 { 15 // WaitOne方法的作用是阻止当前线程,直到收到其他实例释放的处理信号。 16 // 第一个参数是等待超时时间,第二个是否退出上下文同步域。 17 if (!m.WaitOne(TimeSpan.FromSeconds(10), false)) 18 { 19 WriteLine("Second instance is running!"); 20 ReadLine(); 21 } 22 else 23 { 24 WriteLine("Running!"); 25 ReadLine(); 26 // 释放互斥资源 27 m.ReleaseMutex(); 28 } 29 } 30 31 ReadLine(); 32 } 33 } 34 }
3、编译代码,执行两次该程序,运行效果如下所示:
第一种情况的运行结果:
第二种情况的运行结果:
在第11行代码处,我们定义了一个mutex(互斥量)的名称为“Multithreading”,并在第13行代码处将其传递给了Mutex类的构造方法,该构造方法的第一个参数initialOwner我们赋值为false,这允许程序获得一个已经被创建的mutex。如果没有任何线程锁定互斥资源,程序只简单地显示“Running”,然后等待按下任何键以释放互斥资源。
如果我们启动该程序的第二个实例,如果在10秒内我们没有在第一个实例下按下任何按钮以释放互斥资源,那么在第二个实例中就会显示“Second instance is running!”,如第一种情况的运行结果所示。如果在10内我们在第一个实例中按下任何键以释放互斥资源,那么在第二个实例中就会显示“Running”,如第二种情况的运行结果所示。
三、使用SemaphoreSlim构造
在这一小节中,我们将学习如何在SemaphoreSlim构造的帮助下,限制同时访问资源的线程数量。具体步骤如下所示:
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 Recipe03 7 { 8 class Program 9 { 10 static SemaphoreSlim semaphore = new SemaphoreSlim(4); 11 12 static void AccessDatabase(string name, int seconds) 13 { 14 WriteLine($"{name} waits to access a database"); 15 semaphore.Wait(); 16 WriteLine($"{name} was granted an access to a database"); 17 Sleep(TimeSpan.FromSeconds(seconds)); 18 WriteLine($"{name} is completed"); 19 semaphore.Release(); 20 } 21 22 static void Main(string[] args) 23 { 24 for(int i = 1; i <= 6; i++) 25 { 26 string threadName = "Thread" + i; 27 int secondsToWait = 2 + 2 * i; 28 var t = new Thread(() => AccessDatabase(threadName, secondsToWait)); 29 t.Start(); 30 } 31 } 32 } 33 }
3、运行该控制台应用程序,运行效果(每次运行效果可能不同)如下图所示:
在第10行代码处,我们创建了一个SemaphoreSlim的实例,并对该构造方法传递了参数4,该参数指定了可以有多少个线程同时访问资源。然后,我们启动了6个不同名字的线程。每个线程都试着获取对数据库的访问,但是,我们限制了最多只有4个线程可以访问数据库,因此,当4个线程访问数据库后,其他2个线程必须等待,直到其他线程完成其工作后,调用“Release”方法释放资源之后才能访问数据库。