详解C#多线程之线程同步
前言
在多线程编程中,线程同步是一个非常重要的概念。当多个线程并发执行同一段代码时,由于线程执行顺序和时机的不确定性,可能会导致各种不可预测的结果,比如死锁、竞态条件等问题。因此,为了确保多线程程序的正确性,我们必须使用正确的线程同步机制来协调线程之间的访问。
本文将详细讲解C#中的线程同步机制,包括锁、互斥量、信号量和事件等。
锁
锁是最基本的线程同步机制,通常用于保护临界区(critical section)。临界区是一段需要互斥访问的代码区域,例如对共享资源的读写操作。
C#中的锁可以使用lock
关键字来实现。lock
语句可以将一段代码标记为临界区,并且确保在任意时刻只有一个线程可以进入这个区域执行代码,其他线程必须等待当前线程退出临界区后才能进入。
下面是一个简单的示例,演示了如何使用lock
来实现线程同步:
class Counter {
private int count = 0;
public void Increment() {
lock (this) {
count++;
}
}
public void Decrement() {
lock (this) {
count--;
}
}
public int GetCount() {
return count;
}
}
class Program {
static void Main(string[] args) {
Counter counter = new Counter();
Thread t1 = new Thread(() => {
for (int i = 0; i < 100000; i++) {
counter.Increment();
}
});
Thread t2 = new Thread(() => {
for (int i = 0; i < 100000; i++) {
counter.Decrement();
}
});
t1.Start();
t2.Start();
t1.Join();
t2.Join();
Console.WriteLine(counter.GetCount()); // 应该输出0
}
}
在上面的例子中,我们定义了一个Counter
类,这个类有一个私有的计数器count
。Increment
方法将计数器加1,Decrement
方法将计数器减1,GetCount
方法返回计数器的值。注意,Increment
和Decrement
方法都是使用lock
语句来标记临界区,以确保任意时刻只有一个线程可以进入这个区域执行代码。
在Main
方法中,我们创建了两个新线程t1
和t2
,分别执行Increment
和Decrement
方法。我们使用Join
方法等待这两个线程执行完毕。最后,我们输出计数器的值,这个值应该是0。
互斥量
除了锁,互斥量也是一种常用的线程同步机制。互斥量可以用于实现多个线程之间的互斥访问,保护临界区。
C#中的互斥量可以使用Mutex
类来实现。Mutex
类是一个系统级别的同步原语,可以跨进程使用。在创建Mutex
对象时,需要指定一个名称。如果在同一台计算机上创建了多个名称相同的Mutex
对象,这些对象将被视为是互斥的,即只有一个线程可以获得这些对象的锁。
下面是一个示例,演示了如何使用Mutex
来实现线程同步:
class Counter {
private int count = 0;
private static Mutex mutex = new Mutex();
public void Increment() {
mutex.WaitOne();
count++;
mutex.ReleaseMutex();
}
public void Decrement() {
mutex.WaitOne();
count--;
mutex.ReleaseMutex();
}
public int GetCount() {
return count;
}
}
class Program {
static void Main(string[] args) {
Counter counter = new Counter();
Thread t1 = new Thread(() => {
for (int i = 0; i < 100000; i++) {
counter.Increment();
}
});
Thread t2 = new Thread(() => {
for (int i = 0; i < 100000; i++) {
counter.Decrement();
}
});
t1.Start();
t2.Start();
t1.Join();
t2.Join();
Console.WriteLine(counter.GetCount()); // 应该输出0
}
}
在上面的例子中,我们使用了一个静态的Mutex
对象mutex
,这个对象在整个程序中是共享的。在Increment
和Decrement
方法内部,我们分别调用了WaitOne
和ReleaseMutex
方法来获取和释放锁。
与锁不同的是,将mutex.WaitOne()
和mutex.ReleaseMutex()
包含在锁定块内是不合法的。因为Mutex
并非C#语言级别的构造,而是由系统提供的同步原语,使用WaitOne
方法可以跨越方法边界对代码进行同步。
信号量
信号量是一种常用的线程同步机制,可以用于控制多个线程之间的并发数量。信号量可以看作是一个计数器,当信号量的计数器大于零时,允许某个线程继续执行;当计数器等于零时,阻塞该线程。当另一个线程释放了一个信号量时,计数器加1,唤醒一个(或多个)被阻塞的线程。
C#中的信号量可以使用Semaphore
类来实现。Semaphore
类有两个构造函数:一个接受一个初始计数器,另一个接受一个初始计数器和一个最大计数器。Semaphore.WaitOne
方法用来获取信号量,Semaphore.Release
方法用来释放信号量。
下面是一个示例,演示了如何使用Semaphore
来实现线程同步:
class Counter {
private int count = 0;
private static Semaphore semaphore = new Semaphore(1, 1);
public void Increment() {
semaphore.WaitOne();
count++;
semaphore.Release();
}
public void Decrement() {
semaphore.WaitOne();
count--;
semaphore.Release();
}
public int GetCount() {
return count;
}
}
class Program {
static void Main(string[] args) {
Counter counter = new Counter();
Thread t1 = new Thread(() => {
for (int i = 0; i < 100000; i++) {
counter.Increment();
}
});
Thread t2 = new Thread(() => {
for (int i = 0; i < 100000; i++) {
counter.Decrement();
}
});
t1.Start();
t2.Start();
t1.Join();
t2.Join();
Console.WriteLine(counter.GetCount()); // 应该输出0
}
}
在上面的例子中,我们使用了一个静态的Semaphore
对象semaphore
,这个对象在整个程序中是共享的。在Increment
和Decrement
方法内部,我们分别调用了WaitOne
和Release
方法来获取和释放信号量。这里我们使用了一个计数器为1的Semaphore
对象,表示同时只允许一个线程进入临界区。
事件
事件是一种多线程同步机制,可以用来在多个线程之间传递信号。事件有两种状态:已触发和未触发。已触发的事件等价于一个开关,打开开关后,所有通过WaitOne
方法等待该事件的线程都可以开始执行;未触发的事件则表示等待状态,直到有线程通过Set
方法将事件触发为止。
C#中的事件可以使用ManualResetEvent
和AutoResetEvent
类来实现。这两个类都继承自EventWaitHandle
类。两者的区别在于:ManualResetEvent
需要显式地调用Reset
方法将事件重置到未触发状态;而AutoResetEvent
会在一个线程通过WaitOne
方法等待该事件时自动触发,无需再调用Set
方法。
下面是一个示例,演示了如何使用ManualResetEvent
来实现线程同步:
class Counter {
private int count = 0;
private static ManualResetEventSlim mre = new ManualResetEventSlim();
public void Increment() {
count++;
mre.Set();
}
public void Decrement() {
count--;
mre.Set();
}
public int GetCount() {
return count;
}
public void WaitForChange() {
mre.Wait();
mre.Reset();
}
}
class Program {
static void Main(string[] args) {
Counter counter = new Counter();
Thread t1 = new Thread(() => {
Random r = new Random();
for (int i = 0; i < 1000; i++) {
counter.Increment();
Thread.Sleep(r.Next(10));
}
});
Thread t2 = new Thread(() => {
Random r = new Random();
for (int i = 0; i < 1000; i++) {
counter.Decrement();
Thread.Sleep(r.Next(10));
}
});
Thread t3 = new Thread(() => {
for (int i = 0; i < 10; i++) {
counter.WaitForChange();
Console.WriteLine(counter.GetCount());
}
});
t1.Start();
t2.Start();
t3.Start();
t1.Join();
t2.Join();
t3.Join();
}
}
在上面的例子中,我们使用了一个静态的ManualResetEventSlim
对象mre
,这个对象在整个程序中是共享的。在Increment
和Decrement
方法中,我们分别将计数器加1或减1,然后调用Set
方法来触发事件。在WaitForChange
方法中,我们使用Wait
方法等待事件的触发,并在事件触发后调用Reset
方法将事件重置到未触发状态。在Main
函数中,我们创建了三个线程t1
、t2
和t3
,分别执行Increment
、Decrement
和WaitForChange
方法。在t3
线程中,我们使用WaitForChange
方法等待计数器的变化,并在计数器变化后输出计数器的值。
结语
本文详细讲解了C#中的线程同步机制,包括锁、互斥量、信号量和事件。在编写多线程程序时,根据实际情况选择合适的同步机制非常重要,以确保程序的正确性和健壮性。
本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:详解C#多线程之线程同步 - Python技术站