概念:
多个线程在同时修改共享数据时可能发生始料未及的错误,这样的共享数据叫做临界区,线程互斥是指某一资源同一时间只允许一个访问者进行访问,具有唯一性和排他性,但是互斥无法限制访问者对资源的访问顺序,也就是说访问过程是无序的
例子:
public void add()
{
int n;
n = x;
n++;
Thread.Sleep(ran.Next(5, 10));
x = n;
Console.WriteLine(string.Format("{0} modified x: {1}", Thread.CurrentThread.Name, x));
}
public void sub()
{
int n;
n = x;
n--;
Thread.Sleep(ran.Next(5, 10));
x = n;
Console.WriteLine(string.Format("{0} modified x: {1}", Thread.CurrentThread.Name, x));
}
我们在这里定义两个方法,一个用来实现对x进行+1操作,一个用来实现对x进行-1操作,那么现在我们将这两种行为交给两个线程去做,那么x的值从初始值0开始应该就是0,1这两种变化
static void Main(string[] args)
{
Program program = new Program();
for (int i = 0;i < 100; i++)
{
Thread thread1 = new Thread(program.add);
Thread thread2 = new Thread(program.sub);
thread1.Name = "thread1";
thread2.Name = "thread2";
thread1.Start();
thread2.Start();
Thread.Sleep(100);
thread1.Abort();
thread2.Abort();
}
Console.ReadLine();
}
运行结果:
我们发现x的值是不可预测的无规律加减,完全乱了套,这也就是两个线程同时访问,争抢去修改资源导致的错误发生
引入:
下面就要引入我们的线程互斥了,线程互斥目前我只有三种方法,分别是lock,mutex,monitor,接下来我会一一赘述,并阐述使用场景和区别
1.使用lock加锁
代码如下:
public void add()
{
lock (this) // 使用 lock 语句锁住当前实例对象,确保只有一个线程能够进入这段代码
{
int n;
n = x;
n++;
Thread.Sleep(ran.Next(5, 10));
x = n;
Console.WriteLine($"{Thread.CurrentThread.Name} modified x: {x}");
}
}
public void sub()
{
lock (this) // 使用 lock 语句锁住当前实例对象,确保只有一个线程能够进入这段代码
{
int n;
n = x;
n--;
Thread.Sleep(ran.Next(5, 10));
x = n;
Console.WriteLine($"{Thread.CurrentThread.Name} modified x: {x}");
}
}
lock (this)
的作用是锁住当前实例对象,确保只有一个线程能够进入 lock
语句块。这样,当一个线程在执行 add
或 sub
方法时,其他线程就不能同时执行这两个方法,从而避免了对共享资源 x
的竞争条件。
请注意,使用 lock
语句时,锁住的对象应该是所有线程都能够访问的共享对象。在这里,使用 this
表示锁住当前实例对象,因为 add
和 sub
方法都是在同一个实例对象上调用的。如果有多个实例对象,并且它们共享一个变量 x
,那么可以考虑使用其他对象作为锁。
在代码中,使用 lock (this) 的目的是确保在同一时刻只有一个线程能够进入被锁住的代码块。这是为了防止多个线程同时修改共享资源(这里是变量 `x`)而导致竞态条件和数据不一致性。
让我们具体解释为什么它们必须等对方完成:
1. 线程安全性: 当一个线程进入 `lock (this)` 代码块时,它获得了对 `this` 锁的控制权。这意味着其他线程在同一时刻无法获得相同的锁,因此它们必须等待。这是为了确保在任何给定时刻只有一个线程能够执行被锁住的代码块,从而避免对共享资源的并发修改。
2. 防止竞态条件:如果两个线程可以同时进入 `add` 和 `sub` 方法中的 `lock (this)` 代码块,那么它们可能同时读取并修改变量 `x`,导致竞态条件。这种情况下,最终的结果可能是不确定的,因为它取决于线程的执行顺序。
通过让线程等待,确保每个线程在进入 `lock (this)` 代码块时,能够独占对共享资源的访问权。一旦一个线程完成了对 `x` 的修改,其他线程才能获取锁并进行它们自己的操作。
需要注意的是,使用 `lock` 机制可能会导致线程之间的竞争和等待,可能影响程序的性能。在实际应用中,可以考虑使用更细粒度的锁、无锁数据结构或其他并发控制手段,以提高程序的并发性能。
2.使用Mutex
public void add()
{
int n;
mutex.WaitOne();
n = x;
n++;
Thread.Sleep(ran.Next(5, 10));
x = n;
Console.WriteLine($"{Thread.CurrentThread.Name} modified x:{x}");
mutex.ReleaseMutex();
}
public void sub()
{
int n;
mutex.WaitOne();
n = x;
n--;
Thread.Sleep(ran.Next(5, 10));
x = n;
Console.WriteLine($"{Thread.CurrentThread.Name} modified x:{x}");
mutex.ReleaseMutex();
}
让我们一步一步来解释这段文字:
1. **Mutex 是 C# 中用于实现互斥锁的一个类:**
- `Mutex` 是一个C#中的类,它提供了一种实现互斥锁(Mutual Exclusion Lock)的机制。互斥锁是一种同步机制,用于确保在任何给定时刻只有一个线程能够访问共享资源。这是为了避免多个线程同时修改共享资源而导致数据不一致性的问题。
2. **Mutex 提供了一个全局的同步对象,可以在不同线程间共享:**
- `Mutex` 不仅可以在同一个进程中的不同线程之间使用,还可以在不同进程之间共享。这是因为 `Mutex` 是一个全局同步对象,可以被多个线程和多个进程共享。这使得它成为一种强大的同步工具,可用于确保在整个系统中对共享资源的互斥访问。
3. **在这个例子中,Mutex 被用于保护 sharedResource 变量的并发访问:**
- 在给定的示例中,有一个名为 `sharedResource` 的变量,它被多个线程同时访问。为了确保这个共享资源的安全访问,`Mutex` 被用作互斥锁的工具。只有获得了 `Mutex` 的线程才能访问 `sharedResource`,其他线程必须等待。
4. **mutex.WaitOne() 用于获取互斥锁,而 mutex.ReleaseMutex() 用于释放互斥锁:**
- `WaitOne` 方法用于请求获取互斥锁,如果锁可用,线程将获得对共享资源的访问权限。`ReleaseMutex` 方法用于释放互斥锁,使得其他线程可以获取锁并访问共享资源。这样,只有一个线程能够同时修改共享资源,确保互斥访问。
5. **当一个线程正在修改 sharedResource 时,其他线程必须等待互斥锁释放,从而确保对 sharedResource 的互斥访问:**
- 当一个线程成功获取互斥锁时,其他线程必须等待,直到拥有锁的线程通过 `ReleaseMutex` 释放了锁。这样确保了在任何给定时刻只有一个线程能够修改 `sharedResource`,避免了竞态条件和数据不一致性。
总体而言,`Mutex` 是一种用于实现线程安全的强大机制,它可以被用于跨线程和跨进程的同步。
和lock的区别
`Mutex` 和 `lock` 都是用于实现互斥访问的机制,但它们之间有一些关键的区别:
1. **跨足范围:**
- `lock` 关键字是 C# 中的语法糖,只能用于同一个应用程序域内的同一个实例。它用于同步对共享资源的访问,确保在同一实例上的代码块在任何给定时刻只能被一个线程执行。
- `Mutex` 是一个系统级别的同步原语,可以用于跨足不同应用程序域、不同进程,甚至不同计算机的线程之间。它提供了更大的灵活性,允许在更广泛的上下文中实现互斥访问。
2. **异常处理:**
- `lock` 关键字在发生异常时会自动释放锁,确保锁的释放。这是因为 `lock` 通常在 `try` 块中使用,而在发生异常时会执行 `finally` 块来确保资源的释放。
- `Mutex` 需要显式地调用 `ReleaseMutex` 方法来释放锁。如果在使用 `Mutex` 的代码块中发生异常,确保在适当的地方调用 `ReleaseMutex` 非常重要,否则可能导致锁无法正确释放。
3. **性能:**
- `lock` 是基于 Monitor 类的,是一个高级别的互斥锁,相对于 `Mutex` 来说,它可能具有更轻量级的开销,适用于更简单的同步需求。
- `Mutex` 是系统级别的同步原语,通常涉及到更多的系统调用和内核开销,相对于 `lock` 可能更昂贵。因此,在一些简单的同步场景下,`lock` 可能更适用。
4. **用法:**
- `lock` 是 C# 中的语法糖,使用起来更简单,适合简单的同步场景。
- `Mutex` 更适合于需要在不同应用程序域、进程或计算机之间进行同步的复杂场景。
综上所述,选择使用 `Mutex` 还是 `lock` 取决于您的具体需求。如果您只需在同一实例的同一应用程序域中进行同步,且性能较为关键,那么 `lock` 可能更适合。如果您需要进行更广泛的同步,跨足不同应用程序域或进程,那么 `Mutex` 提供了更大的灵活性。
3.使用Monitor
public void add()
{
int n;
//mutex.WaitOne();
Monitor.Enter(this);
n = x;
n++;
Thread.Sleep(ran.Next(5, 10));
x = n;
Console.WriteLine($"{Thread.CurrentThread.Name} modified x:{x}");
//mutex.ReleaseMutex();
Monitor.Exit(this);
}
public void sub()
{
int n;
//mutex.WaitOne();
Monitor.Enter(this);
n = x;
n--;
Thread.Sleep(ran.Next(5, 10));
x = n;
Console.WriteLine($"{Thread.CurrentThread.Name} modified x:{x}");
//mutex.ReleaseMutex();
Monitor.Exit(this);
}
`lock` 关键字实际上是基于 `Monitor` 类的简化语法。`lock` 本质上是 `Monitor` 类的一种封装,使用起来更简便,适用于一些基本的同步需求。`Monitor` 提供了更多的方法和更复杂的特性,使其更灵活,可用于处理更复杂的同步场景。
主要的区别在于:
1. **使用简便性:**
- `lock` 关键字提供了一种更简单、更直观的方式来实现同步,尤其是对于一般的同步需求。
- `Monitor` 提供了更多的控制和灵活性,但使用起来可能相对繁琐。
2. **封装:**
- `lock` 是 `Monitor` 的语法糖,它封装了 `Monitor` 的 `Enter` 和 `Exit` 操作,使得使用起来更加方便。
3. **更多的方法:**
- `Monitor` 提供了一些额外的方法,如 `Wait`、`Pulse`、`PulseAll`,用于更复杂的线程同步和协调。
4. **更大的灵活性:**
- `Monitor` 允许手动管理锁,可以手动调用 `Monitor.Enter` 和 `Monitor.Exit`,使得在代码中更灵活地处理同步。
总体而言,`lock` 关键字适用于简单的同步场景,而 `Monitor` 则提供了更多的工具和方法,适用于更复杂的线程同步和通信需求。在大多数情况下,使用 `lock` 关键字是更推荐的,因为它更简单、易读,同时足够满足许多基本的同步需求。