跳到主要内容

C# 线程同步

介绍

在多线程编程中,多个线程可能会同时访问共享资源,这可能导致数据不一致或程序行为异常。为了解决这个问题,C#提供了多种线程同步机制,确保在同一时间只有一个线程可以访问共享资源。本文将介绍C#中常用的线程同步技术,并通过代码示例和实际案例帮助你理解这些概念。

线程同步的必要性

假设有两个线程同时访问一个共享变量,一个线程正在读取数据,而另一个线程正在修改数据。如果没有适当的同步机制,可能会导致读取到不一致或错误的数据。线程同步的目的是确保多个线程在访问共享资源时能够有序地进行,从而避免竞争条件(Race Condition)。

常用的线程同步机制

1. lock 关键字

lock 是C#中最常用的线程同步机制之一。它通过锁定一个对象来确保在同一时间只有一个线程可以执行被锁定的代码块。

csharp
using System;
using System.Threading;

class Program
{
private static object _lock = new object();
private static int _counter = 0;

static void Main(string[] args)
{
Thread t1 = new Thread(IncrementCounter);
Thread t2 = new Thread(IncrementCounter);

t1.Start();
t2.Start();

t1.Join();
t2.Join();

Console.WriteLine($"Final Counter Value: {_counter}");
}

static void IncrementCounter()
{
for (int i = 0; i < 100000; i++)
{
lock (_lock)
{
_counter++;
}
}
}
}

输出:

Final Counter Value: 200000

在这个例子中,lock 确保了 _counter 的递增操作是线程安全的。如果没有 lock,两个线程可能会同时修改 _counter,导致最终结果小于预期。

提示

lock 关键字只能锁定引用类型的对象,不能锁定值类型。

2. Monitor

Monitor 类提供了与 lock 类似的功能,但更加灵活。它允许你手动控制锁的获取和释放。

csharp
using System;
using System.Threading;

class Program
{
private static object _lock = new object();
private static int _counter = 0;

static void Main(string[] args)
{
Thread t1 = new Thread(IncrementCounter);
Thread t2 = new Thread(IncrementCounter);

t1.Start();
t2.Start();

t1.Join();
t2.Join();

Console.WriteLine($"Final Counter Value: {_counter}");
}

static void IncrementCounter()
{
for (int i = 0; i < 100000; i++)
{
Monitor.Enter(_lock);
try
{
_counter++;
}
finally
{
Monitor.Exit(_lock);
}
}
}
}

输出:

Final Counter Value: 200000

Monitor.EnterMonitor.Exit 分别用于获取和释放锁。使用 try-finally 块可以确保锁在发生异常时也能被正确释放。

警告

如果忘记调用 Monitor.Exit,可能会导致死锁(Deadlock)。

3. Mutex

Mutex 是一种跨进程的同步机制,适用于需要在多个进程之间同步资源的场景。

csharp
using System;
using System.Threading;

class Program
{
private static Mutex _mutex = new Mutex();
private static int _counter = 0;

static void Main(string[] args)
{
Thread t1 = new Thread(IncrementCounter);
Thread t2 = new Thread(IncrementCounter);

t1.Start();
t2.Start();

t1.Join();
t2.Join();

Console.WriteLine($"Final Counter Value: {_counter}");
}

static void IncrementCounter()
{
for (int i = 0; i < 100000; i++)
{
_mutex.WaitOne();
try
{
_counter++;
}
finally
{
_mutex.ReleaseMutex();
}
}
}
}

输出:

Final Counter Value: 200000

MutexWaitOneReleaseMutex 方法分别用于获取和释放锁。与 Monitor 类似,Mutex 也需要确保锁被正确释放。

注意

Mutex 的性能通常比 lockMonitor 差,因为它涉及到操作系统级别的同步。

4. Semaphore

Semaphore 用于控制对一组资源的访问。与 Mutex 不同,Semaphore 允许多个线程同时访问资源,但数量有限。

csharp
using System;
using System.Threading;

class Program
{
private static Semaphore _semaphore = new Semaphore(2, 2); // 允许2个线程同时访问
private static int _counter = 0;

static void Main(string[] args)
{
Thread t1 = new Thread(IncrementCounter);
Thread t2 = new Thread(IncrementCounter);
Thread t3 = new Thread(IncrementCounter);

t1.Start();
t2.Start();
t3.Start();

t1.Join();
t2.Join();
t3.Join();

Console.WriteLine($"Final Counter Value: {_counter}");
}

static void IncrementCounter()
{
for (int i = 0; i < 100000; i++)
{
_semaphore.WaitOne();
try
{
_counter++;
}
finally
{
_semaphore.Release();
}
}
}
}

输出:

Final Counter Value: 300000

在这个例子中,Semaphore 允许最多两个线程同时访问 _counter,从而限制了并发访问的数量。

备注

Semaphore 适用于需要限制并发访问数量的场景,例如数据库连接池。

实际案例:银行账户转账

假设你正在开发一个银行系统,多个线程可能会同时执行转账操作。为了确保账户余额的正确性,你需要使用线程同步机制。

csharp
using System;
using System.Threading;

class BankAccount
{
private decimal _balance;
private object _lock = new object();

public BankAccount(decimal initialBalance)
{
_balance = initialBalance;
}

public void Transfer(BankAccount target, decimal amount)
{
lock (_lock)
{
if (_balance >= amount)
{
_balance -= amount;
target.Deposit(amount);
}
}
}

public void Deposit(decimal amount)
{
lock (_lock)
{
_balance += amount;
}
}

public decimal GetBalance()
{
lock (_lock)
{
return _balance;
}
}
}

class Program
{
static void Main(string[] args)
{
BankAccount account1 = new BankAccount(1000);
BankAccount account2 = new BankAccount(1000);

Thread t1 = new Thread(() => account1.Transfer(account2, 500));
Thread t2 = new Thread(() => account2.Transfer(account1, 300));

t1.Start();
t2.Start();

t1.Join();
t2.Join();

Console.WriteLine($"Account1 Balance: {account1.GetBalance()}");
Console.WriteLine($"Account2 Balance: {account2.GetBalance()}");
}
}

输出:

Account1 Balance: 800
Account2 Balance: 1200

在这个案例中,lock 确保了转账操作的原子性,避免了多个线程同时修改账户余额导致的数据不一致。

总结

线程同步是多线程编程中的重要概念,它确保了多个线程在访问共享资源时的安全性。C#提供了多种线程同步机制,包括 lockMonitorMutexSemaphore,每种机制都有其适用的场景。通过合理使用这些工具,你可以编写出高效且安全的并发程序。

附加资源与练习

  • 练习1:尝试修改银行账户转账案例,使用 Monitor 代替 lock,并观察结果。
  • 练习2:使用 Semaphore 实现一个简单的线程池,限制同时运行的线程数量。
提示

深入学习线程同步时,建议阅读《C#并发编程经典实例》一书,了解更多高级同步技术。