小安妮呀 發表於 2020-4-26 22:58:00

C#多线程(11):线程等待

<p></p><div class="toc"><div class="toc-container-header">目录</div><ul><li>前言<ul><li>volatile 关键字</li><li>三种常用等待</li><li>再说自旋和阻塞</li></ul></li><li>SpinWait 结构<ul><li>属性和方法</li><li>自旋示例</li><li>新的实现</li></ul></li><li>SpinLock 结构<ul><li>属性和方法</li><li>示例</li><li>等待性能对比</li></ul></li></ul></div><br>
前面我们学习了很多用于线程管理的 类型,也学习了多种线程同步的使用方法,这一篇主要讲述线程等待相关的内容。<p></p>
<p>在笔者认真探究多线程前,只会<code>new Thread</code>;锁?<code>Lock</code>;线程等待?<code>Thread.Sleep()</code>。</p>
<p>前面已经探究了创建线程的创建姿势和各种锁的使用,也学习了很多类型,也使用到了很多种等待方法,例如 <code>Thread.Sleep()</code>、<code>Thread.SpinWait();</code>、<code>{某种锁}.WaitOne()</code> 等。</p>
<p>这些等待会影响代码的算法逻辑和程序的性能,也有可能会造成死锁,在本篇我们将会慢慢探究线程中等待。</p>
<h2 id="前言">前言</h2>
<h3 id="volatile-关键字">volatile 关键字</h3>
<p><code>volatile</code> 关键字指示一个字段可以由多个同时执行的线程修改。</p>
<p>我们继续使用《C#多线程(3):原子操作》中的示例:</p>
<pre><code class="language-csharp">      static void Main(string[] args)
      {
            for (int i = 0; i &lt; 5; i++)
            {
                new Thread(AddOne).Start();
            }
            Thread.Sleep(TimeSpan.FromSeconds(5));
            Console.WriteLine("sum = " + sum);
            Console.ReadKey();
      }
      private static int sum = 0;
      public static void AddOne()
      {
            for (int i = 0; i &lt; 100_0000; i++)
            {
                sum += 1;
            }
      }
</code></pre>
<p>运行后你会发现,结果不为 500_0000,而使用 <code>Interlocked.Increment(ref sum);</code>后,可以获得准确可靠的结果。</p>
<p>你试试再运行下面的示例:</p>
<pre><code class="language-csharp">      static void Main(string[] args)
      {
            for (int i = 0; i &lt; 5; i++)
            {
                new Thread(AddOne).Start();
            }
            Thread.Sleep(TimeSpan.FromSeconds(5));
            Console.WriteLine("sum = " + sum);
            Console.ReadKey();
      }
      private static volatile int sum = 0;
      public static void AddOne()
      {
            for (int i = 0; i &lt; 100_0000; i++)
            {
                sum += 1;
            }
      }
</code></pre>
<p>你以为正常了?哈哈哈,并没有。</p>
<p>volatile 的作用在于读,保证了观察的顺序和写入的顺序一致,每次读取的都是最新的一个值;不会干扰写操作。</p>
<p>详情请点击:https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/keywords/volatile</p>
<p>其原理解释:https://theburningmonk.com/2010/03/threading-understanding-the-volatile-modifier-in-csharp/</p>
<p><img src="https://img2020.cnblogs.com/blog/1315495/202004/1315495-20200426225619468-27042466.png" alt="" loading="lazy"></p>
<h3 id="三种常用等待">三种常用等待</h3>
<p>这三种等待分别是:</p>
<pre><code class="language-csharp">Thread.Sleep();
</code></pre>
<pre><code class="language-csharp">Thread.SpinWait();
</code></pre>
<pre><code class="language-csharp">Task.Delay();
</code></pre>
<p><code>Thread.Sleep();</code> 会阻塞线程,使得线程交出时间片,然后处于休眠状态,直至被重新唤醒;适合用于长时间的等待;</p>
<br>
<p><code>Thread.SpinWait();</code> 使用了自旋等待,等待过程中会进行一些的运算,线程不会休眠,用于微小的时间等待;长时间等待会影响性能;</p>
<br>
<p><code>Task.Delay();</code> 用于异步中的等待,异步的文章后面才写,这里先不理会;</p>
<br>
<p>这里我们还需要继续 SpinWait 和 SpinLock 这两个类型,最后再进行总结对照。</p>
<h3 id="再说自旋和阻塞">再说自旋和阻塞</h3>
<p>前面我们学习过自旋和阻塞的区别,这里再来撸清楚一下。</p>
<p>线程等待有内核模式(Kernel Mode)和用户模式(User Model)。</p>
<p>因为只有操作系统才能控制线程的生命周期,因此使用 <code>Thread.Sleep()</code> 等方式阻塞线程,发生上下文切换,此种等待称为内核模式。</p>
<p>用户模式使线程等待,并不需要线程切换上下文,而是让线程通过执行一些无意义的运算,实现等待。也称为自旋。</p>
<h2 id="spinwait-结构">SpinWait 结构</h2>
<p>微软文档定义:为基于自旋的等待提供支持。</p>
<p>
    </p><div style="color: rgba(23, 23, 23, 1); font-family: &quot;Segoe UI&quot;, SegoeUI, &quot;Segoe WP&quot;, &quot;Helvetica Neue&quot;, Helvetica, Tahoma, Arial, sans-serif; background-color: rgba(255, 241, 204, 1); border-radius: 10px; padding: 20px">
SpinWait 是结构体;Thread.SpinWait() 的原理就是 SpinWait 。<br>如果你想了解 Thread.SpinWait() 是怎么实现的,可以参考 https://www.tabsoverspaces.com/233735-how-is-thread-spinwait-actually-implemented
</div>
<p></p>
<p>线程阻塞是会耗费上下文切换的,对于过短的线程等待,这种切换的代价会比较昂贵的。在我们前面的示例中,大量使用了 <code>Thread.Sleep()</code> 和各种类型的等待方法,这其实是不合理的。</p>
<p>SpinWait 则提供了更好的选择。</p>
<h3 id="属性和方法">属性和方法</h3>
<p>老规矩,先来看一下 SpinWait 常用的属性和方法。</p>
<p>属性:</p>
<table>
<thead>
<tr>
<th>属性</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>Count</td>
<td>获取已对此实例调用 SpinOnce() 的次数。</td>
</tr>
<tr>
<td>NextSpinWillYield</td>
<td>获取对 SpinOnce() 的下一次调用是否将产生处理器,同时触发强制上下文切换。</td>
</tr>
</tbody>
</table>
<p>方法:</p>
<table>
<thead>
<tr>
<th>方法</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>Reset()</td>
<td>重置自旋计数器。</td>
</tr>
<tr>
<td>SpinOnce()</td>
<td>执行单一自旋。</td>
</tr>
<tr>
<td>SpinOnce(Int32)</td>
<td>执行单一自旋,并在达到最小旋转计数后调用 Sleep(Int32) 。</td>
</tr>
<tr>
<td>SpinUntil(Func)</td>
<td>在指定条件得到满足之前自旋。</td>
</tr>
<tr>
<td>SpinUntil(Func, Int32)</td>
<td>在指定条件得到满足或指定超时过期之前自旋。</td>
</tr>
<tr>
<td>SpinUntil(Func, TimeSpan)</td>
<td>在指定条件得到满足或指定超时过期之前自旋。</td>
</tr>
</tbody>
</table>
<h3 id="自旋示例">自旋示例</h3>
<p>下面来实现一个让当前线程等待其它线程完成任务的功能。</p>
<p>其功能是开辟一个线程对 sum 进行 <code>+1</code>,当新的线程完成运算后,主线程才能继续运行。</p>
<pre><code class="language-csharp">    class Program
    {
      static void Main(string[] args)
      {
            new Thread(DoWork).Start();

            // 等待上面的线程完成工作
            MySleep();

            Console.WriteLine("sum = " + sum);
            Console.ReadKey();
      }

      private static int sum = 0;
      private static void DoWork()
      {
            for (int i = 0; i &lt; 1000_0000; i++)
            {
                sum++;
            }
            isCompleted = true;
      }

      // 自定义等待等待
      private static bool isCompleted = false;
      private static void MySleep()
      {
            int i = 0;
            while (!isCompleted)
            {
                i++;
            }
      }
    }
</code></pre>
<h3 id="新的实现">新的实现</h3>
<p>我们改进上面的示例,修改 MySleep 方法,改成:</p>
<pre><code class="language-csharp">      private static bool isCompleted = false;      
      private static void MySleep()
      {
            SpinWait wait = new SpinWait();
            while (!isCompleted)
            {
                wait.SpinOnce();
            }
      }
</code></pre>
<p>或者改成</p>
<pre><code class="language-csharp">      private static bool isCompleted = false;      
      private static void MySleep()
      {
            SpinWait.SpinUntil(() =&gt; isCompleted);
      }
</code></pre>
<h2 id="spinlock-结构">SpinLock 结构</h2>
<p>微软文档:提供一个相互排斥锁基元,在该基元中,尝试获取锁的线程将在重复检查的循环中等待,直至该锁变为可用为止。</p>
<p>SpinLock 称为自旋锁,适合用在频繁争用而且等待时间较短的场景。主要特征是避免了阻塞,不出现昂贵的上下文切换。</p>
<p>笔者水平有限,关于 SpinLock ,可以参考 https://www.c-sharpcorner.com/UploadFile/1d42da/spinlock-class-in-threading-C-Sharp/</p>
<p>另外,还记得 Monitor 嘛?SpinLock 跟 Monitor 比较像噢~https://www.cnblogs.com/whuanle/p/12722853.html#2monitor</p>
<p>在《C#多线程(10:读写锁)》中,我们介绍了 ReaderWriterLock 和 ReaderWriterLockSlim ,而 ReaderWriterLockSlim 内部依赖于 SpinLock,并且比 ReaderWriterLock 快了三倍。</p>
<h3 id="属性和方法-1">属性和方法</h3>
<p>SpinLock常用属性和方法如下:</p>
<p>属性:</p>
<table>
<thead>
<tr>
<th>属性</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>IsHeld</td>
<td>获取锁当前是否已由任何线程占用。</td>
</tr>
<tr>
<td>IsHeldByCurrentThread</td>
<td>获取锁是否已由当前线程占用。</td>
</tr>
<tr>
<td>IsThreadOwnerTrackingEnabled</td>
<td>获取是否已为此实例启用了线程所有权跟踪。</td>
</tr>
</tbody>
</table>
<p>方法:</p>
<table>
<thead>
<tr>
<th>方法</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>Enter(Boolean)</td>
<td>采用可靠的方式获取锁,这样,即使在方法调用中发生异常的情况下,都能采用可靠的方式检查 <code>lockTaken</code> 以确定是否已获取锁。</td>
</tr>
<tr>
<td>Exit()</td>
<td>释放锁。</td>
</tr>
<tr>
<td>Exit(Boolean)</td>
<td>释放锁。</td>
</tr>
<tr>
<td>TryEnter(Boolean)</td>
<td>尝试采用可靠的方式获取锁,这样,即使在方法调用中发生异常的情况下,都能采用可靠的方式检查 <code>lockTaken</code> 以确定是否已获取锁。</td>
</tr>
<tr>
<td>TryEnter(Int32, Boolean)</td>
<td>尝试采用可靠的方式获取锁,这样,即使在方法调用中发生异常的情况下,都能采用可靠的方式检查 <code>lockTaken</code> 以确定是否已获取锁。</td>
</tr>
<tr>
<td>TryEnter(TimeSpan, Boolean)</td>
<td>尝试采用可靠的方式获取锁,这样,即使在方法调用中发生异常的情况下,都能采用可靠的方式检查 <code>lockTaken</code> 以确定是否已获取锁。</td>
</tr>
</tbody>
</table>
<h3 id="示例">示例</h3>
<p>SpinLock 的模板如下:</p>
<pre><code class="language-csharp">      private static void DoWork()
      {
            SpinLock spinLock = new SpinLock();
            bool isGetLock = false;   // 是否已获得了锁
            try
            {
                spinLock.Enter(ref isGetLock);
                // 运算
            }
            finally
            {
                if (isGetLock)
                  spinLock.Exit();
            }
      }
</code></pre>
<p>这里就不写场景示例了。</p>
<p>需要注意的是, SpinLock 实例不能共享,也不能重复使用。</p>
<h3 id="等待性能对比">等待性能对比</h3>
<p>大佬的文章,.NET 中的多种锁性能测试数据:http://kejser.org/synchronisation-in-net-part-3-spinlocks-and-interlocks/</p>
<p>这里我们简单测试一下阻塞和自旋的性能测试对比。</p>
<p>我们经常说,<code>Thread.Sleep()</code> 会发生上下文切换,出现比较大的性能损失。具体有多大呢?我们来测试一下。(以下运算都是在 Debug 下测试)</p>
<p>测试 <code>Thread.Sleep(1)</code>:</p>
<pre><code class="language-csharp">      private static void DoWork()
      {
            Stopwatch watch = new Stopwatch();
            watch.Start();
            for (int i = 0; i &lt; 1_0000; i++)
            {
                Thread.Sleep(1);
            }
            watch.Stop();
            Console.WriteLine(watch.ElapsedMilliseconds);
      }
</code></pre>
<p>笔者机器测试,结果大约 20018。<code>Thread.Sleep(1)</code> 减去等待的时间 10000 毫秒,那么进行 10000 次上下文切换需要花费 10000 毫秒,约每次 1 毫秒。</p>
<p>上面示例改成:</p>
<pre><code class="language-csharp">            for (int i = 0; i &lt; 1_0000; i++)
            {
                Thread.Sleep(2);
            }
</code></pre>
<p>运算,发现结果为 30013,也说明了上下文切换,大约需要一毫秒。</p>
<p>改成 <code>Thread.SpinWait(1000)</code>:</p>
<pre><code class="language-csharp">            for (int i = 0; i &lt; 100_0000; i++)
            {
                Thread.SpinWait(1000);
            }
</code></pre>
<p>结果为 28876,说明自旋 1000 次,大约需要 0.03 毫秒。</p>


</div>
<div id="MySignature" role="contentinfo">
    痴者工良(https://whuanle.cn)<br><br>
来源:https://www.cnblogs.com/whuanle/p/12783086.html
頁: [1]
查看完整版本: C#多线程(11):线程等待