编码技巧 --- 同步锁对象的选定

引言

在C#中,让线程同步有两种方式:

  • 锁(lock、Monitor)
  • 信号量(EventWaitHandle、Semaphore、Mutex)

线程锁的原理,就是锁住一个资源,使得应用程序在此刻只有一个线程访问该资源。通俗地讲,就是让多线程变成单线程。在C#中,可以将被锁定的资源理解成 new 出来的普通CLR对象。

如何选定

既然需要锁定的资源就是C#中的一个对象,我们就该仔细思考,到底什么样的对象能够成为一个锁对象(也叫同步对象)?

那么选择同步对象的时候,应当始终注意以下几点:

  1. 同步对象在需要同步的多个线程中是可见的同一个对象。
  2. 在非静态方法中,静态变量不应作为同步对象。
  3. 值类型对象不能作为同步对象。
  4. 避免将字符串作为同步对象。
  5. 降低同步对象的可见性。

原因分析

接下来就探讨一下这五种情况。

注意事项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(() =>
{
//确保等待t2开始之后才运行下面的代码
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 对于线程 t1t2 来说,在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(() =>
{
//确保等待t2开始之后才运行下面的代码
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 的方法 StartT1StartT2 ,方法内部锁定的是 SampleClass 的实例变量 syncObj

实例变量意味着,每创建一个 SampleClass 的实例都会生成一个 syncObj 对象。

在本例中,调用者一共创建了两个 SampleClass 实例,继而分别调用: