C# 线程同步
介绍
在多线程编程中,多个线程可能会同时访问共享资源,这可能导致数据不一致或程序行为异常。为了解决这个问题,C#提供了多种线程同步机制,确保在同一时间只有一个线程可以访问共享资源。本文将介绍C#中常用的线程同步技术,并通过代码示例和实际案例帮助你理解这些概念。
线程同步的必要性
假设有两个线程同时访问一个共享变量,一个线程正在读取数据,而另一个线程正在修改数据。如果没有适当的同步机制,可能会导致读取到不一致或错误的数据。线程同步的目的是确保多个线程在访问共享资源时能够有序地进行,从而避免竞争条件(Race Condition)。
常用的线程同步机制
1. lock
关键字
lock
是C#中最常用的线程同步机制之一。它通过锁定一个对象来确保在同一时间只有一个线程可以执行被锁定的代码块。
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
类似的功能,但更加灵活。它允许你手动控制锁的获取和释放。
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.Enter
和 Monitor.Exit
分别用于获取和释放锁。使用 try-finally
块可以确保锁在发生异常时也能被正确释放。
如果忘记调用 Monitor.Exit
,可能会导致死锁(Deadlock)。
3. Mutex
类
Mutex
是一种跨进程的同步机制,适用于需要在多个进程之间同步资源的场景。
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
Mutex
的 WaitOne
和 ReleaseMutex
方法分别用于获取和释放锁。与 Monitor
类似,Mutex
也需要确保锁被正确释放。
Mutex
的性能通常比 lock
和 Monitor
差,因为它涉及到操作系统级别的同步。
4. Semaphore
类
Semaphore
用于控制对一组资源的访问。与 Mutex
不同,Semaphore
允许多个线程同时访问资源,但数量有限。
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
适用于需要限制并发访问数量的场景,例如数据库连接池。
实际案例:银行账户转账
假设你正在开发一个银行系统,多个线程可能会同时执行转账操作。为了确保账户余额的正确性,你需要使用线程同步机制。
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#提供了多种线程同步机制,包括 lock
、Monitor
、Mutex
和 Semaphore
,每种机制都有其适用的场景。通过合理使用这些工具,你可以编写出高效且安全的并发程序。
附加资源与练习
- 练习1:尝试修改银行账户转账案例,使用
Monitor
代替lock
,并观察结果。 - 练习2:使用
Semaphore
实现一个简单的线程池,限制同时运行的线程数量。
深入学习线程同步时,建议阅读《C#并发编程经典实例》一书,了解更多高级同步技术。