在多线程环境中,您必须注意同步,这样两个线程就不会因为同时执行修改而破坏状态。否则,您的代码中可能会出现竞争条件(例如,请参阅infamous Therac-25 accident。)您还必须调度线程来执行各种任务。然后,您必须确保您的同步和调度不会导致多个线程无限期地相互等待的死锁。
同步
像增加计数器这样简单的事情需要同步:
counter += 1;
假设这一系列事件:
-
counter 初始化为 0
- 线程A从内存中检索
counter到cpu(0)
- 上下文切换
- 线程 B 从内存中检索
counter 到 cpu (0)
- 线程 B 在 cpu 上增加
counter
- 线程 B 将
counter 从 cpu 写回内存 (1)
- 上下文切换
- 线程A在cpu上增加
counter
- 线程 A 将
counter 从 cpu 写回内存 (1)
此时counter 为1,但两个线程都尝试增加它。必须通过某种锁定机制来同步对计数器的访问:
lock (myLock) {
counter += 1;
}
只允许一个线程执行锁定块内的代码。执行此代码的两个线程可能会导致以下事件序列:
- 计数器初始化为 0
- 线程A获取
myLock
- 上下文切换
- 线程 B 尝试获取
myLock 但必须等待
- 上下文切换
- 线程A从内存中检索
counter到cpu(0)
- 线程A在cpu上增加
counter
- 线程 A 将
counter 从 cpu 写回内存 (1)
- 线程 A 释放
myLock
- 上下文切换
- 线程B获取
myLock
- 线程 B 从内存中检索
counter 到 cpu (1)
- 线程 B 在 cpu 上增加
counter
- 线程 B 将
counter 从 cpu 写回内存 (2)
- 线程 B 释放
myLock
此时counter 为2。
调度
调度是同步的另一种形式,您必须使用线程同步机制(如事件、信号量、消息传递等)来启动和停止线程。这是 C# 中的一个简化示例:
AutoResetEvent taskEvent = new AutoResetEvent(false);
Task task;
// Called by the main thread.
public void StartTask(Task task) {
this.task = task;
// Signal the worker thread to perform the task.
this.taskEvent.Set();
// Return and let the task execute on another thread.
}
// Called by the worker thread.
void ThreadProc() {
while (true) {
// Wait for the event to become signaled.
this.taskEvent.WaitOne();
// Perform the task.
}
}
您会注意到对this.task 的访问可能没有正确同步,工作线程无法将结果返回给主线程,并且无法发出终止工作线程的信号。所有这些都可以在更详细的示例中得到纠正。
死锁
一个常见的死锁示例是当您有两个锁并且您不小心如何获取它们时。在某一时刻,您在lock2 之前获得了lock1:
public void f() {
lock (lock1) {
lock (lock2) {
// Do something
}
}
}
在另一点上,您在lock1 之前获得了lock2:
public void g() {
lock (lock2) {
lock (lock1) {
// Do something else
}
}
}
让我们看看这可能会如何死锁:
- 线程A调用
f
- 线程A获取
lock1
- 上下文切换
- 线程 B 调用
g
- 线程B获取
lock2
- 线程 B 尝试获取
lock1 但必须等待
- 上下文切换
- 线程 A 尝试获取
lock2 但必须等待
- 上下文切换
此时线程 A 和 B 正在互相等待并死锁。