在多线程(线程同步)中,我们将学习多线程中操作共享资源的技术,学习到的知识点如下所示:
- 执行基本的原子操作
- 使用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”方法释放资源之后才能访问数据库。
