杂七杂八的砖C#编码技巧 --- 同步锁对象的选定
Fantasy-ke引言
在C#中,让线程同步有两种方式:
- 锁(lock、Monitor)
- 信号量(EventWaitHandle、Semaphore、Mutex)
线程锁的原理,就是锁住一个资源,使得应用程序在此刻只有一个线程访问该资源。通俗地讲,就是让多线程变成单线程。在C#中,可以将被锁定的资源理解成 new 出来的普通CLR对象。
如何选定
既然需要锁定的资源就是C#中的一个对象,我们就该仔细思考,到底什么样的对象能够成为一个锁对象(也叫同步对象)?
那么选择同步对象的时候,应当始终注意以下几点:
- 同步对象在需要同步的多个线程中是可见的同一个对象。
- 在非静态方法中,静态变量不应作为同步对象。
- 值类型对象不能作为同步对象。
- 避免将字符串作为同步对象。
- 降低同步对象的可见性。
原因分析
接下来就探讨一下这五种情况。
注意事项1:需要锁定的对象在多个线程中是可见的,而且是同一个对象。
“可见的”这是显而易见的,如果对象不可见,就不能被锁定。
“同一个对象”,这也很容易理解,如果锁定的不是同一个对象,那又如何来同步两个对象呢?
虽然理解起来简单,但不见得我们在这上面就不会犯错误。
我们模拟一个必须使用到锁的场景:在遍历一个集合的过程中,同时在另外一个线程中删除集合中的某项。
下面这个例子中,如果没有 lock 语句,将会抛出异常System.InvalidOperationException:“Collection was modified; enumeration operation may not execute.”
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| public partial class Form1 : Form { public Form1() { InitializeComponent(); }
AutoResetEvent autoResetEvent = new AutoRe
List<string> strings = new List<string>()
private void btn_StartThreads_Click(object { object syncObj = new object();
Thread t1 = new Thread(() => { autoResetEvent.WaitOne();
lock (syncObj) { foreach (var item in strings) { Thread.Sleep(1000); } } }); t1.IsBackground = false;
t1.Start();
Thread t2 = new Thread(() => { autoResetEvent.Set();
Thread.Sleep(1000);
lock (syncObj) { strings.RemoveAt(1); }
}); t2.IsBackground = false;
t2.Start(); }) }
|
上述例子是 Winform 窗体应用程序,按钮的单击事件中演示该功能。对象 syncObj 对于线程 t1 和 t2 来说,在CLR中肯定是同一个对象。所以,上面的示例运行是没有问题的。
现在,我们将此示例重构。将实际的工作代码移到一个类型 SampleClass 中,该示例要在多个 SampleClass 实例间操作一个静态字段,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
| public partial class Form1 : Form { public Form1() { InitializeComponent(); }
private void btn_StartThreads_Click(object sender, EventArgs e) { SampleClass sampleClass1 = new SampleClass(); SampleClass sampleClass2 = new SampleClass(); sampleClass1.StartT1(); sampleClass2.StartT2(); } }
public class SampleClass { public static AutoResetEvent autoResetEvent = new AutoResetEvent(false);
static List<string> strings = new List<string>() { "str1", "str2", "str3" };
object syncObj = new object();
public void StartT1() { Thread t1 = new Thread(() => { autoResetEvent.WaitOne();
lock (syncObj) { foreach (var item in strings) { Thread.Sleep(1000); } } }); t1.IsBackground = false;
t1.Start(); } public void StartT2() { Thread t2 = new Thread(() => { autoResetEvent.Set();
Thread.Sleep(1000);
lock (syncObj) { strings.RemoveAt(1); }
}); t2.IsBackground = false;
t2.Start(); } }
|
该例子运行起来就会抛出异常System.InvalidOperationException:“Collection was modified; enumeration operation may not execute.”
查看类型 SampleClass 的方法 StartT1 和 StartT2 ,方法内部锁定的是 SampleClass 的实例变量 syncObj 。
实例变量意味着,每创建一个 SampleClass 的实例都会生成一个 syncObj 对象。
在本例中,调用者一共创建了两个 SampleClass 实例,继而分别调用: