最近一直在学习C#的多线程编程,发现多线程编程并不容易。在启动访问相同数据的多个线程时,会间歇性地遇到难以发现的问题。下面来讨论与线程相关的问题:争用条件。如果两个或多个线程访问相同的对象,或者访问不同步的共享状态,就会出现争用条件。看下面的例子:
1: using System;
2: using System.Collections.Generic;
3: using System.Linq;
4: using System.Text;
5: using System.Diagnostics;
6: using System.Threading;
7: using System.Threading.Tasks;
8:
9: namespace ThreadingIssues
10: {
11: public class StateObject
12: {
13: private int state = 5;
14: private object sync = new object();
15: /// <summary>
16: /// 当state等于5时,改变state的值
17: /// </summary>
18: /// <param name="loop">循环次数</param>
19: public void ChangeState(int loop)
20: {
21: if (state == 5)
22: {
23: state++;
//递增之后立即验证state现在是否6
24: Trace.Assert(state == 6, "Race condition occurred after " + loop + " loops");
25: }
26: state = 5;
27: }
28: }
29:
30: //创建一个分配给任务的一个方法
31: public class SampleTask
32: {
33: public void RaceCondition(object o)
34: {
35: Trace.Assert(o is StateObject,"o must be of type StateObject");
36: StateObject state = o as StateObject;
37:
38: int i = 0;
39: while(true)
40: {
41: state.ChangeState(i++);
42: }
43: }
44: }
45:
46: class Program
47: {
48: static void Main(string[] args)
49: {
50: var state = new StateObject();
51: for (int i = 0; i < 20; i++)
52: {
53: new Task(new SampleTask().RaceCondition, state).Start();
54: }
55: Thread.Sleep(10000);
56: }
57: }
58: }
59:
60:
61:
运行这个示例的结果:
在运行到63414个循环时,出现争用条件的程序断言。多次启动应用程序,总会得到不同的结果。要避免这个问题,可以锁定共享的对象。这个过程通过在使用共享对象的方法中进行锁定以及将共享对象设置为线程安全的对象来实现。
代码如下:
1: using System;
2: using System.Collections.Generic;
3: using System.Linq;
4: using System.Text;
5: using System.Diagnostics;
6: using System.Threading;
7: using System.Threading.Tasks;
8:
9: namespace ThreadingIssues
10: {
11: public class StateObject
12: {
13: private int state = 5;
14: private object sync = new object();
15: /// <summary>
16: /// 当state等于5时,改变state的值
17: /// </summary>
18: /// <param name="loop"></param>
19: public void ChangeState(int loop)
20: {
21: lock (sync)
22: {
23: if (state == 5)
24: {
25: state++;
26: Trace.Assert(state == 6, "Race condition occurred after " + loop + " loops");
27: }
28: }
29: state = 5;
30: }
31: }
32:
33: //创建一个分配给任务的一个方法
34: public class SampleTask
35: {
36: public void RaceCondition(object o)
37: {
38: Trace.Assert(o is StateObject,"o must be of type StateObject");
39: StateObject state = o as StateObject;
40:
41: int i = 0;
42: while(true)
43: {
44: lock(state)
45: {
46: state.ChangeState(i++);
47: }
48: }
49: }
50: }
51:
52: class Program
53: {
54: static void Main(string[] args)
55: {
56: var state = new StateObject();
57: for (int i = 0; i < 20; i++)
58: {
59: new Task(new SampleTask().RaceCondition, state).Start();
60: }
61: Thread.Sleep(10000);
62: }
63: }
64: }
运行结果如下:
总结:使用lock语句锁定共享state对象的代码块,只有一个线程能在锁定块中处理共享的state对象。由于这个对象在所有的线程之前共享,因此如果一个线程锁定了state对象,另外一个线程必须等待该锁定的解除。一旦接收锁定,该线程就拥有该锁定,直到该锁定块的末尾才解除锁定。如果改变state变量引用的对象的每个线程都使用一个锁定,就不会出现争用条件。
在将共享对象state设置为线程安全的对象时,由于StateObject类内部的变量state(注意这个state变量和前面的state对象不是同一个)是int类型,由于lock不能锁定值类型本身(只有引用类型才能用于锁定),因此需要定义一个object类型的变量sync,将它用于lock语句。如果每次state的值更改时,都使用同步对象来锁定,就不会出现争用条件.