二十九、原始线程同步构造(Primitive Thread Synchronization Constructs )
🔐 第29章:线程同步构造的兵器谱(实战版)
🎯 导言
在实际开发中,线程就像一群工人,大家都要用工具、机器、库房。如果没有秩序——工人们可能会:
- 抢同一台机器(数据竞争)
- 一直等不到轮到自己(死锁)
- 结果出来错误(数据不一致)
同步构造就是工地的规章制度+工具,让工人们有序工作。
🧩 一、用户模式兵器(快,轻,适合局部的小冲突)
1️⃣ volatile —— 全局开关
📌 真实例子:一个服务里的后台线程循环检查“是否关闭系统”。
private volatile bool _shutdownRequested;
[Button("StartWorker")]
public void StartWorker()
{
new Thread(() =>
{
while (!_shutdownRequested)
{
// 检查队列、做点小任务
Thread.Sleep(1000);
Debug.Log("后台线程正在运行...");
}
Debug.Log("后台线程安全退出");
}).Start();
}
[Button("StopWorker")]
public void Stop() => _shutdownRequested = true;
[Button("StartRequest")]
public void StartRequest()=>_shutdownRequested = false;
👉 实战场景:游戏服务器的 后台守护线程,用它来安全退出。
2️⃣ Interlocked —— 安全计数器
📌 真实例子:统计 Web API 调用次数(并发请求)。
private static int _requestCount = 0;
public void HandleRequest()
{
Interlocked.Increment(ref _requestCount);
Console.WriteLine($"总请求数: {_requestCount}");
}
private volatile bool _shutdownRequested;
private static int _requestCount=0;
public void HandleRequest()=> Interlocked.Increment(ref _requestCount);
[Button("StartWorker")]
public void StartWorker()
{
Random random = new Random();
new Thread(() =>
{
while (!_shutdownRequested)
{
//模拟并发计数
int requestCount = random.Next(1,5);
Parallel.For(0, requestCount, i =>
{
HandleRequest();
});
// 检查队列、做点小任务
Thread.Sleep(1000);
Debug.Log($"总请求数: {_requestCount}后台线程正在运行...");
}
Debug.Log("后台线程安全退出");
}).Start();
}
👉 实战场景:Web/游戏服里统计访问量、在线人数、任务完成数。
3️⃣ SpinLock —— 短时间的临界资源
📌 真实例子:游戏引擎里一个对象池,线程从池子里拿/还对象,操作极快。
private static SpinLock _spinLock = new SpinLock();
private static Queue<int> _pool = new Queue<int>(Enumerable.Range(1, 10));
public static int Rent()
{
bool taken = false;
try
{
_spinLock.Enter(ref taken);
return _pool.Dequeue();
}
finally
{
if (taken) _spinLock.Exit();
}
}
👉 场景:高频极短操作(比如 Unity 里粒子、子弹对象池)。
🧱 二、内核模式兵器(重,但全能)
🔸 lock —— 共享资源保护
📌 真实例子:日志系统,多个线程写同一个文件。
private static readonly object _logLock = new object();
public static void WriteLog(string msg)
{
lock (_logLock)
{
File.AppendAllText("log.txt", $"{DateTime.Now}: {msg}\n");
}
}
👉 场景:写文件、更新全局配置、写数据库事务。
🔸 ManualResetEvent(手工的,手动的) —— 发信号,多个线程同时出发
📌 真实例子:赛车游戏起跑线,所有选手等裁判开枪。
ManualResetEvent startSignal = new ManualResetEvent(false);
void Runner(string name)
{
Console.WriteLine($"{name} 准备就绪...");
startSignal.WaitOne();
Console.WriteLine($"{name} 出发!");
}
// 创建选手线程
new Thread(() => Runner("A")).Start();
new Thread(() => Runner("B")).Start();
// 裁判发令
Thread.Sleep(1000);
Console.WriteLine("裁判开枪!");
startSignal.Set();
👉 场景:并发任务同时开始,如压测工具模拟 1000 个玩家同时登录。
🔸 AutoResetEvent —— 一个接一个
📌 真实例子:打印机一次只能打印一个文件,但多个线程要打印。
AutoResetEvent printer = new AutoResetEvent(true);
void Print(string doc)
{
printer.WaitOne();
Console.WriteLine($"正在打印: {doc}");
Thread.Sleep(1000);
Console.WriteLine($"{doc} 完成");
printer.Set();
}
new Thread(() => Print("文件1")).Start();
new Thread(() => Print("文件2")).Start();
new Thread(() => Print("文件3")).Start();
👉 场景:串行访问硬件/IO。
🔸 Semaphore(打信号,打旗语) —— 有限资源池
📌 真实例子:数据库连接池(最多 3 个连接)。
Semaphore dbConnections = new Semaphore(3, 3);
void QueryDB(int id)
{
dbConnections.WaitOne();
Console.WriteLine($"线程{id} 使用数据库连接...");
Thread.Sleep(2000);
Console.WriteLine($"线程{id} 释放连接");
dbConnections.Release();
}
for (int i = 0; i < 6; i++)
{
int id = i;
new Thread(() => QueryDB(id)).Start();
}
👉 场景:并发下载、数据库连接池、线程池限流。
🔸 Mutex (互斥)—— 跨进程大锁
📌 真实例子:防止同一程序被重复启动。
static void Main()
{
using var mutex = new Mutex(false, "Global\\MyUniqueApp");
if (!mutex.WaitOne(0, false))
{
Console.WriteLine("程序已在运行!");
return;
}
Console.WriteLine("程序运行中...");
Console.ReadLine();
}
👉 场景:跨进程互斥,如防止多开。
🔄 三、比喻对照表
| ⚔️ 武器 | 比喻 | 实战例子 |
|---|---|---|
volatile |
大声喊出来 | 程序是否停止标志位 |
Interlocked |
自动售货机按钮 | 并发计数器(在线人数) |
SpinLock |
短刀对决 | 对象池取还 |
lock |
房门钥匙 | 日志写文件 |
ManualResetEvent |
裁判开枪 | 并发压测启动 |
AutoResetEvent |
打印机排队 | 串行 IO 设备 |
Semaphore |
地铁闸机 | DB 连接池 |
Mutex |
跨操场大锁 | 程序防多开 |
🧠 实战建议
- ✅ 标志/计数:
volatile、Interlocked - ✅ 常规共享资源:
lock - ✅ 有限资源池:
Semaphore - ⚠️ 跨进程锁:
Mutex(慎用) - ⚠️ SpinLock:只在极短代码块用
🎮 Unity 场景
- 后台资源加载:用
Interlocked统计已完成任务数。 - 限制同时下载数:
Semaphore控制最多 N 个并发请求。 - 工具类插件(比如日志系统):用
lock确保文件写安全。
🎯 并发面试题
Q1:为什么在 Unity/WinForms/WPF 主线程中不推荐使用 Task.Wait() 或 Result?
答案:
因为它们会阻塞当前线程,而 UI/游戏主线程负责渲染和输入循环,一旦阻塞,就会造成界面卡死(Unity 会掉帧)。
解析:
-
Task.Wait()/Result= 阻塞等待,常见于控制台/后台任务。 -
在 UI/游戏主线程应使用
await(异步等待),不会阻塞消息泵或渲染循环。 -
推荐写法:
var data = await File.ReadAllTextAsync("config.json");
Q2:volatile 和 Interlocked 的区别是什么?分别适用什么场景?
答案:
volatile:保证读写总是直接访问内存,避免缓存不一致;不保证原子性。Interlocked:保证复合操作(加减、交换、CAS)原子性。
解析:
- 如果只是读写一个标志位(如“是否停止服务”),用
volatile即可。 - 如果是计数器、并发累加,必须用
Interlocked,否则 ++ 操作会发生竞争。
示例:
private volatile bool _stopping; // 状态标志
private int _counter = 0; // 计数器
// 停止标志
if (_stopping) return;
// 线程安全计数
Interlocked.Increment(ref _counter);
Q3:在高并发场景下,SpinLock 为什么可能比 lock 更快?又有什么缺点?
答案:
- 优点:SpinLock 在等待时“忙等”,不进入内核,不触发线程上下文切换;如果临界区非常短(几十纳秒级),可能比
lock更快。 - 缺点:如果临界区稍长,SpinLock 会疯狂占用 CPU,导致性能更差。
解析:
SpinLock适合 对象池取还、短暂标志交换等极快操作。lock(Monitor)适合常规业务逻辑,因为它会把线程挂起,等待时不占 CPU。
Q4:请解释 ManualResetEvent 和 AutoResetEvent 的区别,并给出一个应用场景。
答案:
ManualResetEvent:信号保持开启,唤醒所有等待线程。AutoResetEvent:信号一次性,只唤醒一个线程,之后自动重置。
解析:
ManualResetEvent场景:模拟比赛起跑信号,所有线程同时开始。AutoResetEvent场景:打印机任务队列,一次只允许一个线程打印。
示例:
AutoResetEvent printer = new AutoResetEvent(true);
void Print(string doc)
{
printer.WaitOne();
Console.WriteLine($"打印 {doc}");
Thread.Sleep(1000);
printer.Set();
}
Q5:在高并发服务里,如何限制“同时访问数据库的连接数”?请给出代码方案。
答案:
使用 Semaphore 来控制可同时进入的线程数。
解析:
- 数据库连接是有限资源,不可能无限开。
Semaphore可以定义最大并发数,例如 10。- 超过的线程会等待,直到有连接被释放。
示例:
Semaphore dbPool = new Semaphore(10, 10);
void HandleQuery()
{
dbPool.WaitOne(); // 占用一个名额
try
{
Console.WriteLine("查询数据库...");
Thread.Sleep(500); // 模拟查询
}
finally
{
dbPool.Release(); // 释放连接
}
}
- Q1 → UI 线程避免阻塞等待,
await才是正解。 - Q2 →
volatile用于标志位,Interlocked用于计数器。 - Q3 →
SpinLock= 临界区极短时快,否则耗 CPU。 - Q4 →
ManualResetEvent= 同时唤醒,AutoResetEvent= 一个一个。 - Q5 →
Semaphore控制有限资源池(连接池/下载池)。
✅ 总结
- 用户模式 = 短兵器,快狠准。
- 内核模式 = 重武器,慢但能打硬仗。
- 场景对了 → 程序既安全又高效;场景错了 → CPU/线程都哭了。
作者:世纪末的魔术师
出处:https://www.cnblogs.com/Firepad-magic/
Unity最受欢迎插件推荐:点击查看
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

浙公网安备 33010602011771号