二十九、原始线程同步构造(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 跨操场大锁 程序防多开

🧠 实战建议

  • 标志/计数volatileInterlocked
  • 常规共享资源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:volatileInterlocked 的区别是什么?分别适用什么场景?

答案:

  • 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:请解释 ManualResetEventAutoResetEvent 的区别,并给出一个应用场景。

答案:

  • 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(); // 释放连接
    }
}

  1. Q1 → UI 线程避免阻塞等待,await 才是正解。
  2. Q2volatile 用于标志位,Interlocked 用于计数器。
  3. Q3SpinLock = 临界区极短时快,否则耗 CPU。
  4. Q4ManualResetEvent = 同时唤醒,AutoResetEvent = 一个一个。
  5. Q5Semaphore 控制有限资源池(连接池/下载池)。

✅ 总结

  • 用户模式 = 短兵器,快狠准。
  • 内核模式 = 重武器,慢但能打硬仗。
  • 场景对了 → 程序既安全又高效;场景错了 → CPU/线程都哭了。
posted @ 2025-08-26 10:08  世纪末の魔术师  阅读(11)  评论(0)    收藏  举报