小毛台 發表於 2025-5-27 23:50:00

关于多线程本质的思考

<h1 id="关于多线程本质的思考和使用技巧">关于多线程本质的思考和使用技巧</h1>
<h2 id="前言">前言</h2>
<p>​        近来,公司因为项目过多,人手不足,一直在进行面试。过程中同事总是问道:多线程是什么,谈谈你对多线程的理解?以我愚见,这并不是一个可以在面试中快速回答的问题,如果面试的时候向我提问,我觉得我无法有条理的回答这个问题。因此,以总结多线程开发为目标,我写下这篇笔记,用于记录自己对多线程的理解和思考,以备不时之需。</p>
<h2 id="什么是多线程">什么是多线程</h2>
<p>​        不论初入开发生涯的小白和深耕多年的老兵,提及多线程,第一想到就是加锁,用来确保代码正确执行,避免程序调度的不可预测性导致的错误。但这只是问题的表层,在复杂的并发场景中,<strong>锁只是工具,而不是答案</strong>。多线程开发远不止“避免冲突”,它是一场在“性能”与“正确性”之间的博弈。</p>
<h3 id="多线程开发的目标">多线程开发的目标</h3>
<p>​        多线程开发的根本目标:在并发环境下,正确高效的保证对共享资源的访问。</p>
<ul>
<li><strong>正确性:</strong>不论线程如何调度,指令如何优化,确保程序的正常运行。</li>
<li><strong>性能:</strong>充分利用多核 CPU,提升吞吐、响应速度。</li>
</ul>
<h3 id="多线程开发问题的本源">多线程开发问题的本源</h3>
<p>我将多线程问题总结为这四类。</p>
<table>
<thead>
<tr>
<th>问题类型</th>
<th>描述</th>
</tr>
</thead>
<tbody>
<tr>
<td>指令重排</td>
<td>CPU 或编译器为了优化,会调整语句顺序执行,导致逻辑失真</td>
</tr>
<tr>
<td>缓存不一致</td>
<td>不同线程可能看到同一个变量的不同值</td>
</tr>
<tr>
<td>非原子操作</td>
<td>多步骤操作中途被其他线程打断,导致逻辑出错</td>
</tr>
<tr>
<td>同步代价</td>
<td>加锁带来上下文切换和等待,降低性能</td>
</tr>
</tbody>
</table>
<h2 id="代码实践">代码实践</h2>
<ol>
<li>
<h3 id="指令重排序"><strong>指令重排序</strong></h3>
<p>指令重排并不会影响单线程的语义,但在多线程环境中,它可能导致“已经构造的对象”被其他线程提前访问,触发 <code>NullReferenceException</code> 或逻辑错误。</p>
<p>例:双重检查锁下的单例初始化</p>
<pre><code class="language-c#">class Singleton {
    private static volatile Singleton _instance;
    private static object _lock = new object();

    public static Singleton Instance {
      get {
            if (_instance == null) {
                lock (_lock) {
                  if (_instance == null) {
                        _instance = new Singleton(); // 非原子操作 + 指令重排
                  }
                }
            }
            return _instance;
      }
    }
}

</code></pre>
<p><code>volatile</code> 禁止指令重排</p>
<p>没有 volatile 的场景下,可能出现对象<strong>“已分配但未初始化”</strong></p>
<h6 id="延伸方案">延伸方案</h6>
<ul>
<li>使用 <code>Lazy&lt;T&gt;</code> 避免双检锁与重排问题</li>
<li>使用 <code>Thread.MemoryBarrier()</code> 精细控制执行顺序</li>
</ul>
</li>
<li>
<p><strong>缓存不一致</strong></p>
</li>
</ol>
<p>多核 CPU 每个核心拥有自己的缓存,导致线程对同一变量的访问结果可能不一致。</p>
<p>例:线程 A 设置标志位,线程 B 却一直看不到变化</p>
<pre><code class="language-c#">volatile bool _shouldStop = false;

void Worker() {
    while (!_shouldStop) {
      // do something
    }
}

void Stop() {
    _shouldStop = true;
}
</code></pre>
<p>加上 <code>volatile</code> 保证线程 B 能“看到”线程 A 的写入</p>
<p>或者通过锁封装 <code>_shouldStop</code>,隐式解决可见性</p>
<ol start="3">
<li>
<p><strong>非原子操作</strong></p>
<p>例:多线程计数器累加出错</p>
<pre><code class="language-c#">int counter = 0;
Parallel.For(0, 10000, i =&gt; {
    counter++; // 错误! 非原子操作
});
int counter = 0;
Parallel.For(0, 10000, i =&gt; {
   Interlocked.Increment(ref counter); // 正确 原子操作
});

</code></pre>
<p>避免使用 lock 的性能开销</p>
<p>支持 Increment, Decrement, CompareExchange 等原子操作</p>
</li>
<li>
<p>同步代价</p>
<p>例:任务过多时线程阻塞严重,导致性能瓶颈</p>
<pre><code class="language-c#">SemaphoreSlim semaphore = new SemaphoreSlim(10);

Parallel.ForEach(tasks, async task =&gt; {
    await semaphore.WaitAsync();
    try {
      await DoWork(task);
    }
    finally {
      semaphore.Release();
    }
});

</code></pre>
<h4 id="替代思路">替代思路</h4>
<ul>
<li>限流但不阻塞的任务调度:<code>Channel&lt;T&gt;</code> + 消费者模型</li>
<li>利用 <code>Task.Factory.StartNew</code> 创建长时间运行任务,避免线程饥饿</li>
<li>用对象池(比如 <code>ConcurrentBag</code>)重用资源,减少锁粒度</li>
</ul>
</li>
</ol>
<h2 id="延伸多线程的使用方式">延伸多线程的使用方式</h2>
<p>以下是我在多线程开发中常用的一些工具,它们不是简单的 API,而是<strong>有明确使用语境、性能取舍的并发工具</strong>。</p>
<ol>
<li>
<h3 id="concurrentdictionarygetoradd"><code>ConcurrentDictionary.GetOrAdd()</code>:</h3>
<p>​        在高并发场景中,我们经常希望“<strong>某个对象在多个线程中只初始化一次</strong>”,传统做法可能是加锁或双重检查,但 <code>ConcurrentDictionary</code> 通过内部的分段锁和原子操作,实现了线程安全的初始化逻辑:</p>
<pre><code class="language-c#">var instance = dict.GetOrAdd(key, k =&gt; new Lazy(()=&gt;new ExpensiveObject())).Value;
</code></pre>
</li>
<li>
<h3 id="lock-monitor-spinlock"><code>lock</code>, <code>Monitor</code>, <code>SpinLock</code>:</h3>
<h4 id="lock--monitorenterexit"><code>lock</code> / <code>Monitor.Enter/Exit</code></h4>
<pre><code class="language-c#">lock (_lockObj) {
    // 临界区
}
</code></pre>
<ul>
<li>最常用的同步方式,基于 <code>Monitor</code></li>
<li>自动释放锁,结构清晰,推荐首选</li>
</ul>
<h4 id="monitortryenter支持超时"><code>Monitor.TryEnter</code>:支持超时</h4>
<pre><code class="language-c#">if (Monitor.TryEnter(_lockObj, TimeSpan.FromSeconds(2))) {
    try { /* ... */ }
    finally { Monitor.Exit(_lockObj); }
}
</code></pre>
<h4 id="spinlock避免上下文切换的高性能锁"><code>SpinLock</code>:避免上下文切换的高性能锁</h4>
<pre><code class="language-c#">SpinLock _spinLock = new SpinLock();
bool lockTaken = false;
_spinLock.Enter(ref lockTaken);
// 临界区
_spinLock.Exit();
</code></pre>
<ul>
<li>适合<strong>锁持有时间极短</strong>的场景</li>
<li>无线程切换,减少调度开销</li>
<li><strong>注意死锁风险 + 不支持递归加锁</strong></li>
</ul>
</li>
<li>
<h3 id="threadlocalt"><code>ThreadLocal&lt;T&gt;</code>:</h3>
<p>多线程共享变量容易引发冲突,不如<strong>不共享</strong>。<code>ThreadLocal&lt;T&gt;</code> 允许每个线程持有自己的副本,避免锁:</p>
<pre><code>ThreadLocal&lt;Random&gt; rng = new ThreadLocal&lt;Random&gt;(() =&gt; new Random());

int num = rng.Value.Next();
</code></pre>
<ul>
<li>常用于 Random、日志上下文、缓冲区等隔离场景</li>
<li>不适合长生命周期对象(会引起内存泄漏)</li>
</ul>
</li>
<li>
<h3 id="线程池调度taskrun-taskfactory-parallelforeach">线程池调度:<code>Task.Run</code>, <code>TaskFactory</code>, <code>Parallel.ForEach</code></h3>
<ul>
<li><code>Task.Run</code>:将工作提交给线程池,避免频繁创建线程</li>
<li><code>TaskFactory.StartNew</code>:高级配置(调度器、长时间运行等)</li>
<li><code>Parallel.ForEach</code>:简洁处理并行集合任务(如批处理、文件处理)</li>
</ul>
<pre><code>Parallel.ForEach(myList, item =&gt; {
    Process(item);
});
</code></pre>
<p><strong>注意</strong>:线程池线程默认不能被控制上下文,如需隔离状态应结合 <code>ThreadLocal</code> 或信号量。</p>
</li>
<li>
<p><code></code>:方法级同步声明(不推荐)</p>
<pre><code>
void MyCriticalMethod() {
    // 隐式锁定 this
}
</code></pre>
<p>等价于在方法体前加 lock(this),可能导致外部死锁,不透明、难调试</p>
</li>
<li>
<h3 id="lazyt"><code>Lazy&lt;T&gt;</code>:</h3>
<pre><code>Lazy&lt;HeavyObject&gt; lazyObj = new Lazy&lt;HeavyObject&gt;(() =&gt; new HeavyObject());

var obj = lazyObj.Value; // 初始化只发生一次
</code></pre>
<ul>
<li>内部实现使用双检锁+volatile,线程安全</li>
<li>默认线程安全(LazyThreadSafetyMode.ExecutionAndPublication)</li>
</ul>
</li>
<li>
<h3 id="systemthreadingchannels"><code>System.Threading.Channels</code>:</h3>
<pre><code>var channel = Channel.CreateUnbounded&lt;string&gt;();

// Producer
await channel.Writer.WriteAsync("msg");

// Consumer
await foreach (var msg in channel.Reader.ReadAllAsync()) {
    Process(msg);
}
</code></pre>
<ul>
<li>内部使用环形缓冲 + 原子操作,无需锁</li>
<li>广泛用于 <strong>高性能日志、异步消息、管道通信</strong></li>
</ul>
</li>
<li>
<h3 id="cancellationtoken"><code>CancellationToken</code>:</h3>
<pre><code>var cts = new CancellationTokenSource();
var token = cts.Token;

var task = Task.Run(() =&gt; {
    while (!token.IsCancellationRequested) {
      // work
    }
}, token);

cts.Cancel(); // 触发取消
</code></pre>
<ul>
<li>支持协作式停止线程</li>
<li>适用于定时任务、消费者线程、异步服务</li>
</ul>
</li>
<li>
<h3 id="blockingcollection线程安全的队列--阻塞消费"><code>BlockingCollection</code>:线程安全的队列 + 阻塞消费</h3>
<pre><code>var queue = new BlockingCollection&lt;string&gt;();

// Producer
Task.Run(() =&gt; {
    for (int i = 0; i &lt; 100; i++) {
      queue.Add($"msg-{i}");
    }
    queue.CompleteAdding();
});

// Consumer
foreach (var msg in queue.GetConsumingEnumerable()) {
    Console.WriteLine(msg);
}
</code></pre>
<ul>
<li>自动处理线程同步</li>
<li>自动等待生产或消费,无需手动 <code>wait</code> 或 <code>signal</code></li>
<li>适合简化 Producer-Consumer 模型</li>
</ul>
</li>
<li>
<h3 id="使用-valuetask-减少分配高频异步方法">使用 <code>ValueTask</code> 减少分配(高频异步方法)</h3>
<pre><code>public ValueTask&lt;int&gt; ReadAsync() {
    if (_cachedResult != null) {
      return new ValueTask&lt;int&gt;(_cachedResult);
    }
    return new ValueTask&lt;int&gt;(ReadFromDiskAsync());
}
</code></pre>
<ul>
<li>避免频繁创建 Task 对象</li>
<li>适合“同步返回的概率高”的场景,如缓存读取</li>
</ul>
</li>
</ol><br><br>
来源:https://www.cnblogs.com/daibitx/p/18899605
頁: [1]
查看完整版本: 关于多线程本质的思考