惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

F
Fortinet All Blogs
S
Secure Thoughts
月光博客
月光博客
美团技术团队
雷峰网
雷峰网
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
N
News and Events Feed by Topic
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Forbes - Security
Forbes - Security
W
WeLiveSecurity
P
Proofpoint News Feed
阮一峰的网络日志
阮一峰的网络日志
爱范儿
爱范儿
G
GRAHAM CLULEY
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
AI
AI
Last Week in AI
Last Week in AI
Google Online Security Blog
Google Online Security Blog
Schneier on Security
Schneier on Security
云风的 BLOG
云风的 BLOG
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Recent Announcements
Recent Announcements
Webroot Blog
Webroot Blog
T
Tor Project blog
Cisco Talos Blog
Cisco Talos Blog
N
News and Events Feed by Topic
罗磊的独立博客
The Register - Security
The Register - Security
Blog — PlanetScale
Blog — PlanetScale
T
Threat Research - Cisco Blogs
博客园 - 【当耐特】
Apple Machine Learning Research
Apple Machine Learning Research
人人都是产品经理
人人都是产品经理
T
The Exploit Database - CXSecurity.com
www.infosecurity-magazine.com
www.infosecurity-magazine.com
B
Blog
腾讯CDC
Microsoft Azure Blog
Microsoft Azure Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
H
Hacker News: Front Page
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Engineering at Meta
Engineering at Meta
Latest news
Latest news
IT之家
IT之家
D
DataBreaches.Net
博客园 - 司徒正美
N
Netflix TechBlog - Medium
V
V2EX
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知

博客园 - 世纪末の魔术师

《The Wealth Ladder》读书笔记 项目管理,真正的密码只有一句话 Linq延迟执行陷阱 坚毅,是一种缓慢修理自己的方式 语言的边界,与软件的命运 人类与AI协同进化 《坚毅》第一部分读书笔记 用 System.CommandLine 构建工程级 CLI 工具 用 Command 模式构建可扩展的命令行工具 从哎呦”到语言宇宙 ——读《What Is ChatGPT Doing … And Why Does It Work?》 ⏱️ 深入理解定时器中的【时间轮算法】 🚫 为什么「定时器」不应该是线程安全的? C# AOT编译后——调用其类库方法因顺序出错? UnitTask中的Forget()与 CTS 光线追踪和球体追踪 八、方法(method) 二十、异常与状态管理(Exception&State Management) 二十八、IO绑定的异步操作(IO-Bound Async) 二十二、CLR寄宿与AppDomain(CLR Hosting and App Domains ) 二十六、线程与并发(Thread Basic) 二十七、计算密集型异步操作(Compute-Bound Asynchronous Operations) 二十三、程序集加载与反射(Assembly Loading and Reflection)
二十九、原始线程同步构造(Primitive Thread Synchronization Constructs )
世纪末の魔术师 · 2025-08-26 · via 博客园 - 世纪末の魔术师

🔐 第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/线程都哭了。