C#多线程系列(3):原子操作
<p>本章主要讲述多线程竞争下的原子操作。</p><p></p><div class="toc"><div class="toc-container-header">目录</div><ul><li>知识点<ul><li>竞争条件</li><li>线程同步</li><li>CPU时间片和上下文切换</li><li>阻塞</li><li>内核模式和用户模式</li></ul></li><li>Interlocked 类<ul><li>1,出现问题</li><li>2,Interlocked.Increment()</li><li>3,Interlocked.Exchange()</li><li>4,Interlocked.CompareExchange()</li><li>5,Interlocked.Add()</li><li>6,Interlocked.Read()</li></ul></li></ul></div><p></p>
<h2 id="知识点">知识点</h2>
<h3 id="竞争条件">竞争条件</h3>
<p>当两个或两个以上的线程访问共享数据,并且尝试同时改变它时,就发生争用的情况。它们所依赖的那部分共享数据,叫做竞争条件。</p>
<p>数据争用是竞争条件中的一种,出现竞争条件可能会导致内存(数据)损坏或者出现不确定性的行为。</p>
<h3 id="线程同步">线程同步</h3>
<p>如果有 N 个线程都会执行某个操作,当一个线程正在执行这个操作时,其它线程都必须依次等待,这就是线程同步。</p>
<p>多线程环境下出现竞争条件,通常是没有执行正确的同步而导致的。</p>
<h3 id="cpu时间片和上下文切换">CPU时间片和上下文切换</h3>
<p>时间片(timeslice)是操作系统分配给每个正在运行的进程微观上的一段 CPU 时间。</p>
<p>
</p><blockquote style="margin: 10px 0; padding: 10px; border-left: 4px solid rgba(221, 221, 221, 1); color: rgba(68, 68, 68, 1); background-color: rgba(249, 249, 249, 1); border-radius: 4px; font-size: 14px; overflow-wrap: break-word" helvetica="" neue",="" 微软雅黑,="" "microsoft="" yahei",="" helvetica,="" arial,="" sans-serif"="">
首先,内核会给每个进程分配相等的初始时间片,然后每个进程轮番地执行相应的时间,当所有进程都处于时间 片耗尽的状态时,内核会重新为每个进程计算并分配时间片,如此往复。
</blockquote>
<p></p>
<p>请参考:https://zh.wikipedia.org/wiki/%E6%97%B6%E9%97%B4%E7%89%87</p>
<p>上下文切换(Context Switch),也称做进程切换或任务切换,是指 CPU 从一个进程或线程切换到另一个进程或线程。</p>
<p>
</p><blockquote style="margin: 10px 0; padding: 10px; border-left: 4px solid rgba(221, 221, 221, 1); color: rgba(68, 68, 68, 1); background-color: rgba(249, 249, 249, 1); border-radius: 4px; font-size: 14px; overflow-wrap: break-word" helvetica="" neue",="" 微软雅黑,="" "microsoft="" yahei",="" helvetica,="" arial,="" sans-serif"="">
在接受到中断(Interrupt)的时候,CPU 必须要进行上下文交换。进行上下文切换时,会带来性能损失。
</blockquote>
<p></p>
<p>请参考[https://zh.wikipedia.org/wiki/上下文交換</p>
<h3 id="阻塞">阻塞</h3>
<p>阻塞状态指线程处于等待状态。当线程处于阻塞状态时,会尽可能少占用 CPU 时间。</p>
<p>当线程从运行状态(Runing)变为阻塞状态时(WaitSleepJoin),操作系统就会将此线程占用的 CPU 时间片分配给别的线程。当线程恢复运行状态时(Runing),操作系统会重新分配 CPU 时间片。</p>
<p>分配 CPU 时间片时,会出现上下文切换。</p>
<h3 id="内核模式和用户模式">内核模式和用户模式</h3>
<p>只有操作系统才能切换线程、挂起线程,因此阻塞线程是由操作系统处理的,这种方式被称为内核模式(kernel-mode)。</p>
<p><code>Sleep()</code>、<code>Join()</code> 等,都是使用内核模式来阻塞线程,实现线程同步(等待)。</p>
<p>
</p><div style="color: rgba(23, 23, 23, 1); font-family: "Segoe UI", SegoeUI, "Segoe WP", "Helvetica Neue", Helvetica, Tahoma, Arial, sans-serif; background-color: rgba(255, 241, 204, 1); border-radius: 10px; padding: 20px">
内核模式实现线程等待时,出现上下文切换。这适合等待时间比较长的操作,这样会减少大量的 CPU 时间损耗。
</div>
<p></p>
<p>如果线程只需要等待非常微小的时间,阻塞线程带来的上下文切换代价会比较大,这时我们可以使用自旋,来实现线程同步,这一方法称为用户模式(user-mode)。</p>
<h2 id="interlocked-类">Interlocked 类</h2>
<p>为多个线程共享的变量提供原子操作。</p>
<p>使用 Interlocked 类,可以在不阻塞线程(lock、Monitor)的情况下,避免竞争条件。</p>
<p>Interlocked 类是静态类,让我们先来看看 Interlocked 的常用方法:</p>
<table>
<thead>
<tr>
<th>方法</th>
<th>作用</th>
</tr>
</thead>
<tbody>
<tr>
<td>CompareExchange()</td>
<td>比较两个数是否相等,如果相等,则替换第一个值。</td>
</tr>
<tr>
<td>Decrement()</td>
<td>以原子操作的形式递减指定变量的值并存储结果。</td>
</tr>
<tr>
<td>Exchange()</td>
<td>以原子操作的形式,设置为指定的值并返回原始值。</td>
</tr>
<tr>
<td>Increment()</td>
<td>以原子操作的形式递增指定变量的值并存储结果。</td>
</tr>
<tr>
<td>Add()</td>
<td>对两个数进行求和并用和替换第一个整数,上述操作作为一个原子操作完成。</td>
</tr>
<tr>
<td>Read()</td>
<td>返回一个以原子操作形式加载的值。</td>
</tr>
</tbody>
</table>
<p>全部方法请查看:https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.interlocked?view=netcore-3.1#methods</p>
<h3 id="1出现问题">1,出现问题</h3>
<p>问题:</p>
<p> C# 中赋值和一些简单的数学运算不是原子操作,受多线程环境影响,可能会出现问题。</p>
<p>我们可以使用 lock 和 Monitor 来解决这些问题,但是还有没有更加简单的方法呢?</p>
<p>首先我们编写以下代码:</p>
<pre><code class="language-c#"> private static int sum = 0;
public static void AddOne()
{
for (int i = 0; i < 100_0000; i++)
{
sum += 1;
}
}
</code></pre>
<p>这个方法的工作完成后,sum 会 +100。</p>
<p>我们在 Main 方法中调用:</p>
<pre><code class="language-c#"> static void Main(string[] args)
{
AddOne();
AddOne();
AddOne();
AddOne();
AddOne();
Console.WriteLine("sum = " + sum);
}
</code></pre>
<p>结果肯定是 5000000,无可争议的。</p>
<p>但是这样会慢一些,如果作死,要多线程同时执行呢?</p>
<p>好的,Main 方法改成如下:</p>
<pre><code class="language-c#"> static void Main(string[] args)
{
for (int i = 0; i < 5; i++)
{
Thread thread = new Thread(AddOne);
thread.Start();
}
Thread.Sleep(TimeSpan.FromSeconds(2));
Console.WriteLine("sum = " + sum);
}
</code></pre>
<p>笔者运行一次,出现了 <code>sum = 2633938</code></p>
<p>我们将每次运算的结果保存到数组中,截取其中一段发现:</p>
<pre><code>8757
8758
8760
8760
8760
8761
8762
8763
8764
8765
8766
8766
8768
8769
</code></pre>
<p>多个线程使用同一个变量进行操作时,并不知道此变量已经在其它线程中发生改变,导致执行完毕后结果不符合期望。</p>
<p>我们可以通过下面这张图来解释:</p>
<p><img src="https://img2020.cnblogs.com/blog/1315495/202004/1315495-20200418100349578-1959064601.png" alt="" loading="lazy"></p>
<p>因此,这里就需要原子操作,在某个时刻,必须只有一个线程能够进行某个操作。而上面的操作,指的是读取、计算、写入这一过程。</p>
<p>当然,我们可以使用 lock 或者 Monitor 来解决,但是这样会带来比较大的性能损失。</p>
<p>这时 Interlocked 就起作用了,对于一些简单的操作运算, Interlocked 可以实现原子性的操作。</p>
<p>
</p><div style="color: rgba(23, 23, 23, 1); font-family: "Segoe UI", SegoeUI, "Segoe WP", "Helvetica Neue", Helvetica, Tahoma, Arial, sans-serif; background-color: rgba(255, 241, 204, 1); border-radius: 10px; padding: 20px">
实现原子性,可以通过多种锁来解决,目前我们学习到了 lock、Monitor,现在来学习 Interlocked ,后面会学到更加多的锁的实现。
</div>
<p></p>
<h3 id="2interlockedincrement">2,Interlocked.Increment()</h3>
<p>用于自增操作。</p>
<p>我们修改一下 AddOne 方法:</p>
<pre><code class="language-c#"> public static void AddOne()
{
for (int i = 0; i < 100_0000; i++)
{
Interlocked.Increment(ref sum);
}
}
</code></pre>
<p>然后运行,你会发现结果 sum = 5000000 ,这就对了。</p>
<p>说明 Interlocked 可以对简单值类型进行原子操作。</p>
<p>
</p><blockquote style="margin: 10px 0; padding: 10px; border-left: 4px solid rgba(221, 221, 221, 1); color: rgba(68, 68, 68, 1); background-color: rgba(249, 249, 249, 1); border-radius: 4px; font-size: 14px; overflow-wrap: break-word" helvetica="" neue",="" 微软雅黑,="" "microsoft="" yahei",="" helvetica,="" arial,="" sans-serif"="">
<code>Interlocked.Increment()</code> 是递增,而 <code>Interlocked.Decrement()</code> 是递减。
</blockquote>
<p></p>
<h3 id="3interlockedexchange">3,Interlocked.Exchange()</h3>
<p><code>Interlocked.Exchange()</code> 实现赋值运算。</p>
<p>这个方法有多个重载,我们找其中一个来看看:</p>
<pre><code class="language-C#">public static int Exchange(ref int location1, int value);
</code></pre>
<p>意思是将 value 赋给 location1 ,然后返回 location1 改变之前的值。</p>
<p>测试:</p>
<pre><code class="language-csharp"> static void Main(string[] args)
{
int a = 1;
int b = 5;
// a 改变前为1
int result1 = Interlocked.Exchange(ref a, 2);
Console.WriteLine($"a新的值 a = {a} |a改变前的值 result1 = {result1}");
Console.WriteLine();
// a 改变前为 2,b 为 5
int result2 = Interlocked.Exchange(ref a, b);
Console.WriteLine($"a新的值 a = {a} | b不会变化的b = {b} | a 之前的值result2 = {result2}");
}
</code></pre>
<p>另外 <code>Exchange()</code> 也有对引用类型的重载:</p>
<pre><code class="language-csharp">Exchange<T>(T, T)
</code></pre>
<h3 id="4interlockedcompareexchange">4,Interlocked.CompareExchange()</h3>
<p>其中一个重载:</p>
<pre><code class="language-csharp">public static int CompareExchange (ref int location1, int value, int comparand)
</code></pre>
<p>比较两个 32 位有符号整数是否相等,如果相等,则替换第一个值。</p>
<p>如果 <code>comparand</code> 和 <code>location1</code> 中的值相等,则将 <code>value</code> 存储在 <code>location1</code>中。 否则,不会执行任何操作。</p>
<p>看准了,是 <code>location1</code> 和 <code>comparand</code> 比较!</p>
<p>使用示例如下:</p>
<pre><code class="language-csharp"> static void Main(string[] args)
{
int location1 = 1;
int value = 2;
int comparand = 3;
Console.WriteLine("运行前:");
Console.WriteLine($" location1 = {location1} | value = {value} | comparand = {comparand}");
Console.WriteLine("当 location1 != comparand 时");
int result = Interlocked.CompareExchange(ref location1, value, comparand);
Console.WriteLine($" location1 = {location1} | value = {value} |comparand = {comparand} |location1 改变前的值{result}");
Console.WriteLine("当 location1 == comparand 时");
comparand = 1;
result = Interlocked.CompareExchange(ref location1, value, comparand);
Console.WriteLine($" location1 = {location1} | value = {value} |comparand = {comparand} |location1 改变前的值{result}");
}
</code></pre>
<h3 id="5interlockedadd">5,Interlocked.Add()</h3>
<p>对两个 32 位整数进行求和并用和替换第一个整数,上述操作作为一个原子操作完成。</p>
<pre><code class="language-csharp">public static int Add (ref int location1, int value);
</code></pre>
<p>只能对 int 或 long 有效。</p>
<p>回到第一小节的多线程求和问题,使用 <code>Interlocked.Add()</code> 来替换<code>Interlocked.Increment()</code>。</p>
<pre><code class="language-c#"> static void Main(string[] args)
{
for (int i = 0; i < 5; i++)
{
Thread thread = new Thread(AddOne);
thread.Start();
}
Thread.Sleep(TimeSpan.FromSeconds(2));
Console.WriteLine("sum = " + sum);
}
private static int sum = 0;
public static void AddOne()
{
for (int i = 0; i < 100_0000; i++)
{
Interlocked.Add(ref sum,1);
}
}
</code></pre>
<h3 id="6interlockedread">6,Interlocked.Read()</h3>
<p>返回一个以原子操作形式加载的 64 位值。</p>
<p>64位系统上不需要 Read 方法,因为64位读取操作已是原子操作。 在32位系统上,64位读取操作不是原子操作,除非使用 Read 执行。</p>
<pre><code class="language-csharp">public static long Read (ref long location);
</code></pre>
<p>就是说 32 位系统上才用得上。</p>
<p>具体场景我没有找到。</p>
<p>你可以参考一下 https://www.codenong.com/6139699/</p>
<p>貌似没有多大用处?那我懒得看了。</p>
<p><img src="https://img2020.cnblogs.com/blog/1315495/202004/1315495-20200418100404958-174348810.png" alt="" loading="lazy"></p>
</div>
<div id="MySignature" role="contentinfo">
痴者工良(https://whuanle.cn)<br><br>
来源:https://www.cnblogs.com/whuanle/p/12724371.html
頁:
[1]