在.NET多线程编程中,当多个线程同时操作同一个共享资源时,很容易出现竞态条件,导致数据错乱、业务逻辑异常等问题,线程同步就是用来协调多个线程对共享资源的访问顺序,保证同一时间只有一个或指定数量的线程能操作共享资源的技术。

为什么需要线程同步
假设我们有一个共享的计数器变量,两个线程同时执行加1操作,正常情况下两次加1后结果应该是2,但如果没有同步机制,可能出现两个线程同时读取到初始值0,各自加1后都写回1,最终结果不符合预期。这种问题在并发量高的时候会更难排查,所以线程同步是多线程开发中的必备技能。
常用的.NET线程同步方案
1. lock语句
lock是.NET中最常用的轻量级同步方式,它本质是Monitor类的语法糖,会保证同一时间只有一个线程能进入lock包裹的代码块。lock的对象必须是引用类型,且推荐使用私有的只读对象,避免使用公共对象或值类型。
using System;
using System.Threading;
class Counter
{
// 私有只读的锁对象,避免外部修改
private readonly object _lockObj = new object();
private int _count = 0;
public void Increment()
{
// lock保证同一时间只有一个线程能执行下面的代码
lock (_lockObj)
{
int temp = _count;
// 模拟耗时操作
Thread.Sleep(10);
_count = temp + 1;
}
}
public int GetCount()
{
return _count;
}
}
class Program
{
static void Main()
{
Counter counter = new Counter();
// 创建两个线程同时执行加1操作
Thread t1 = new Thread(() =>
{
for (int i = 0; i < 100; i++)
{
counter.Increment();
}
});
Thread t2 = new Thread(() =>
{
for (int i = 0; i < 100; i++)
{
counter.Increment();
}
});
t1.Start();
t2.Start();
t1.Join();
t2.Join();
Console.WriteLine($"最终计数器值:{counter.GetCount()}");
}
}
上面的代码中,两个线程各执行100次加1操作,使用lock后最终输出结果一定是200,如果没有lock包裹,结果大概率会小于200。
2. Monitor类
lock语句是Monitor的简化写法,Monitor类提供了更灵活的同步控制,支持尝试进入临界区、设置等待超时等能力。常用的核心方法有Monitor.Enter、Monitor.Exit、Monitor.TryEnter。
using System;
using System.Threading;
class MonitorDemo
{
private readonly object _lockObj = new object();
private int _sharedData = 0;
public void UpdateData()
{
// 尝试进入临界区,最多等待500毫秒
if (Monitor.TryEnter(_lockObj, 500))
{
try
{
int temp = _sharedData;
Thread.Sleep(20);
_sharedData = temp + 1;
Console.WriteLine($"线程{Thread.CurrentThread.ManagedThreadId}更新数据为{_sharedData}");
}
finally
{
// 必须手动释放锁,否则会导致死锁
Monitor.Exit(_lockObj);
}
}
else
{
Console.WriteLine($"线程{Thread.CurrentThread.ManagedThreadId}等待超时,未获取到锁");
}
}
}
class Program
{
static void Main()
{
MonitorDemo demo = new MonitorDemo();
for (int i = 0; i < 5; i++)
{
Thread thread = new Thread(demo.UpdateData);
thread.Start();
}
}
}
Monitor.TryEnter可以在获取不到锁的时候不阻塞线程,而是执行其他逻辑,适合对响应时间有要求的场景。
3. Mutex类
Mutex(互斥体)和lock、Monitor的作用类似,但是Mutex支持跨进程同步,而lock和Monitor只能用于同一进程内的线程同步。Mutex分为本地Mutex和命名Mutex,命名Mutex可以用于不同进程之间的同步。
using System;
using System.Threading;
class MutexDemo
{
// 创建命名Mutex,用于跨进程同步
private static Mutex _mutex = new Mutex(false, "Global\MyMutex");
public static void DoWork()
{
// 等待获取Mutex,无限等待
_mutex.WaitOne();
try
{
Console.WriteLine($"进程{Environment.ProcessId}的线程{Thread.CurrentThread.ManagedThreadId}进入临界区");
Thread.Sleep(1000);
Console.WriteLine($"进程{Environment.ProcessId}的线程{Thread.CurrentThread.ManagedThreadId}离开临界区");
}
finally
{
// 释放Mutex
_mutex.ReleaseMutex();
}
}
}
class Program
{
static void Main()
{
MutexDemo.DoWork();
}
}
如果需要多个进程访问同一个共享资源,比如操作同一个文件,就可以使用命名Mutex来做同步。
4. Semaphore类
Semaphore(信号量)可以限制同时访问共享资源的线程数量,比如某个资源最多允许3个线程同时访问,就可以用Semaphore来实现。Semaphore同样支持跨进程使用,对应的轻量级版本是SemaphoreSlim,只能用于同一进程内。
using System;
using System.Threading;
class SemaphoreDemo
{
// 初始化信号量,最多允许2个线程同时访问
private static SemaphoreSlim _semaphore = new SemaphoreSlim(2, 2);
public static void AccessResource()
{
Console.WriteLine($"线程{Thread.CurrentThread.ManagedThreadId}等待进入资源区");
// 等待获取信号量
_semaphore.Wait();
try
{
Console.WriteLine($"线程{Thread.CurrentThread.ManagedThreadId}进入资源区");
Thread.Sleep(1000);
Console.WriteLine($"线程{Thread.CurrentThread.ManagedThreadId}离开资源区");
}
finally
{
// 释放信号量
_semaphore.Release();
}
}
}
class Program
{
static void Main()
{
for (int i = 0; i < 5; i++)
{
Thread thread = new Thread(SemaphoreDemo.AccessResource);
thread.Start();
}
}
}
上面的代码中,信号量初始计数为2,所以同时最多有2个线程能进入资源区,其他线程需要等待前面的线程释放信号量后才能进入。
5. 事件等待句柄
.NET中的事件等待句柄包括AutoResetEvent、ManualResetEvent、ManualResetEventSlim,用于线程之间的通知和等待,比如一个线程完成某项工作后通知其他等待的线程继续执行。
AutoResetEvent在收到信号后会自动重置为无信号状态,每次只能唤醒一个等待线程;ManualResetEvent需要手动重置状态,收到信号后会保持有信号状态,所有等待的线程都会被唤醒。
using System;
using System.Threading;
class EventDemo
{
// 初始化为无信号状态
private static AutoResetEvent _autoEvent = new AutoResetEvent(false);
public static void WorkerThread()
{
Console.WriteLine($"线程{Thread.CurrentThread.ManagedThreadId}开始等待信号");
// 等待信号
_autoEvent.WaitOne();
Console.WriteLine($"线程{Thread.CurrentThread.ManagedThreadId}收到信号,继续执行");
}
public static void SignalThread()
{
Thread.Sleep(2000);
Console.WriteLine("主线程发送信号");
// 发送信号,唤醒一个等待的线程
_autoEvent.Set();
}
}
class Program
{
static void Main()
{
for (int i = 0; i < 3; i++)
{
Thread worker = new Thread(EventDemo.WorkerThread);
worker.Start();
}
Thread signalThread = new Thread(EventDemo.SignalThread);
signalThread.Start();
}
}
不同同步方案的选择建议
如果只是同一进程内简单的临界区保护,优先使用lock语句,写法简单且不容易出错;如果需要更灵活的控制比如超时等待,可以使用Monitor类;如果需要跨进程同步,选择Mutex;如果需要限制并发访问的线程数量,选择Semaphore或SemaphoreSlim;如果需要线程之间的通知等待,选择事件等待句柄。同时要注意避免死锁问题,比如不要嵌套获取多个锁,或者按照固定的顺序获取锁。
C#_thread_synchronizationlock_statementMonitor_classSemaphore_class修改时间:2026-06-18 00:06:34