• 热门专题

C#多线程之线程同步3

作者:编码之道  发布日期:2016-12-26 20:23:36
Tag标签:C    多线  线程  
  •   在上一篇C#多线程之线程同步2中,我们主要学习了AutoResetEvent构造、ManualResetEventSlim构造和CountdownEvent构造,在这一篇中,我们将学习Barrier构造、ReaderWriterLockSlim构造和SpinWait构造。

    七、使用Barrier构造

      在这一小节中,我们将学习一个比较有意思的同步构造:Barrier。Barrier构造可以帮助我们控制多个等待线程达到指定数量后,才发送通知信号,然后所有等待线程才能继续执行,并且在每次等待线程达到指定数量后,还能执行一个回调方法。具体步骤如下所示:

    1、使用Visual Studio 2015创建一个新的控制台应用程序。

    2、双击打开“Program.cs”文件,编写代码如下所示:

     1 using System;
     2 using System.Threading;
     3 using static System.Console;
     4 using static System.Threading.Thread;
     5 
     6 namespace Recipe07
     7 {
     8     class Program
     9     {
    10         static Barrier barrier = new Barrier(2, b => WriteLine($'End of phase {b.CurrentPhaseNumber + 1}'));
    11 
    12         static void PlayMusic(string name, string message, int seconds)
    13         {
    14             for(int i = 1; i < 3; i++)
    15             {
    16                 WriteLine('----------------------------------------------');
    17                 Sleep(TimeSpan.FromSeconds(seconds));
    18                 WriteLine($'{name} starts to {message}');
    19                 Sleep(TimeSpan.FromSeconds(seconds));
    20                 WriteLine($'{name} finishes to {message}');
    21                 barrier.SignalAndWait();
    22             }
    23             
    24         }
    25 
    26         static void Main(string[] args)
    27         {
    28             var t1 = new Thread(() => PlayMusic('the guitarist', 'play an amazing solo', 5));
    29             var t2 = new Thread(() => PlayMusic('the singer', 'sing his song', 2));
    30 
    31             t1.Start();
    32             t2.Start();
    33         }
    34     }
    35 }

    3、运行该控制台应用程序,运行效果如下图所示:

      在第10行代码处,我们创建了一个Barrier的实例barrier,并给其构造方法的“participantCount”参数赋值为2,表示barrier参与线程的数量为2,也就是说要有2个线程达到阻塞后,barrier才发送通知信号,其阻塞线程才能继续执行。第二个参数“postPhaseAction”是一个Action类型的委托,表示当阻塞线程达到规定数量后要执行的回调方法。

      在第28~29行代码处,我们创建了2个线程t1和t2,用于执行“PlayMusic”方法。t2线程首先执行到第21行代码处,在这一行代码中,我们在线程t2中调用了barrier的“SignalAndWait”方法,等待参与数量的线程达到构造方法指定的数量2时,才能继续执行,因为,在t2线程调用该方法时,只有一个线程t2被阻塞,没有达到规定数量2,所以,t2线程不能继续执行。当t1线程执行到第21行代码处时,也调用了barrier的“SignalAndWait”方法,这个时候等待线程的数量达到规定的数量2,所以t1和t2线程都能继续执行,并且在barrier的构造方法的第二个参数指定的回调方法也被执行。

      当两个线程执行“PlayMusic”方法的第二次循环时,过程与第一次一样,不在描述。

    八、使用ReaderWriterLockSlim构造

      在这一小节中,我们将学习如何使用ReaderWriterLockSlim构造来线程安全地使用多线程读写集合中的数据。具体步骤如下所示:

    1、使用Visual Studio 2015创建一个新的控制台应用程序。

    2、双击打开“Program.cs”文件,编写代码如下所示:

     1 using System;
     2 using System.Collections.Generic;
     3 using System.Threading;
     4 using static System.Console;
     5 using static System.Threading.Thread;
     6 
     7 namespace Recipe08
     8 {
     9     class Program
    10     {
    11         // 表示用于管理资源访问的锁定状态,可实现多线程读取或进行独占式写入访问
    12         static ReaderWriterLockSlim rw = new ReaderWriterLockSlim();
    13         static Dictionary<int, int> items = new Dictionary<int, int>();
    14 
    15         static void Read()
    16         {
    17             WriteLine('Reading contents of a dictionary');
    18             while (true)
    19             {
    20                 try
    21                 {
    22                     // 尝试进入读取模式锁定状态
    23                     rw.EnterReadLock();
    24                     foreach(var key in items.Keys)
    25                     {
    26                         Sleep(TimeSpan.FromSeconds(0.1));
    27                     }
    28                 }
    29                 finally
    30                 {
    31                     // 减少读取模式的递归计数,并在生成的计数为 0(零)时退出读取模式
    32                     rw.ExitReadLock();
    33                 }
    34             }
    35         }
    36 
    37         static void Write(string threadName)
    38         {
    39             while (true)
    40             {
    41                 try
    42                 {
    43                     int newKey = new Random().Next(250);
    44                     // 尝试进入可升级模式锁定状态
    45                     rw.EnterUpgradeableReadLock();
    46                     if (!items.ContainsKey(newKey))
    47                     {
    48                         try
    49                         {
    50                             // 尝试进入写入模式锁定状态
    51                             rw.EnterWriteLock();
    52                             items[newKey] = 1;
    53                             WriteLine($'New key {newKey} is added to a dictionary by a {threadName}');
    54                         }
    55                         finally
    56                         {
    57                             // 减少写入模式的递归计数,并在生成的计数为 0(零)时退出写入模式
    58                             rw.ExitWriteLock();
    59                         }
    60                     }
    61                     Sleep(TimeSpan.FromSeconds(0.1));
    62                 }
    63                 finally
    64                 {
    65                     // 减少可升级模式的递归计数,并在生成的计数为 0(零)时退出可升级模式
    66                     rw.ExitUpgradeableReadLock();
    67                 }
    68             }
    69         }
    70 
    71         static void Main(string[] args)
    72         {
    73             new Thread(Read) { IsBackground = true }.Start();
    74             new Thread(Read) { IsBackground = true }.Start();
    75             new Thread(Read) { IsBackground = true }.Start();
    76 
    77             new Thread(() => Write('Thread 1')) { IsBackground = true }.Start();
    78             new Thread(() => Write('Thread 2')) { IsBackground = true }.Start();
    79 
    80             Sleep(TimeSpan.FromSeconds(20));
    81         }
    82     }
    83 }

    3、运行该控制台应用程序,运行效果(每次运行效果可能不同)如下图所示:

      在第73~75行代码处,我们创建了3个后台线程来读取集合中的数据。在第77~78行代码处,我们创建了2个后台线程向集合中写入数据。为了线程安全地对集合进行操作,我们使用为此场景专门设计的ReaderWriterLockSlim构造。该构造有两种类型的锁:读取模式锁和写入模式锁。读取模式锁允许多线程读取数据,写入模式锁阻塞其他线程的每一个操作直到写入模式锁被释放为止。

      有一个非常有趣的场景,当我们想获得一个读取模式锁从集合中读取一些数据,并根据这些数据获得一个写入模式锁以更新集合时,如果我们立即就获得锁定模式锁的话不仅消耗的时间多,而且还不允许我们读取数据,因为当我们获得一个写入模式锁的时候,集合就被锁定了。为了尽量减少这种时间的浪费,我们可以使用“EnterUpgradeableReadLock”方法获得读取模式锁来读取数据,如果读取完毕数据后,我们发现需要更新底层集合,那么我们可以使用“EnterWriteLock”升级我们的锁,然后快速执行写入操作并使用“ExitWriteLock”释放写入模式锁,最后使用“ExitUpgradeableReadLock”释放可升级模式锁。

      在上述代码中,我们获得一个随机数,然后获得一个读取模式锁,并检查该随机数是否已在集合中存在,如果不存在,我们升级该读取模式锁为写入模式锁,然后向集合中添加一个新的key。使用try/finally块是一个比较好的方式,它可以保证我们总能释放锁获得的锁。

    九、使用SpinWait构造

      在这一小节中,我们将学习如何在不涉及kernel-mode构造的情况下等待一个线程的执行。另外还将介绍SpinWait构造,该构造是一种混合同步构造,主要用于设计在用户模式中等待一段时间后,然后将其切换到内核模式,以节省CUP时间。具体步骤如下所示:

    1、使用Visual Studio 2015创建一个新的控制台应用程序。

    2、双击打开“Program.cs”文件,编写代码如下所示:

     1 using System;
     2 using System.Threading;
     3 using static System.Console;
     4 using static System.Threading.Thread;
     5 
     6 namespace Recipe09
     7 {
     8     class Program
     9     {
    10         static volatile bool isCompleted = false;
    11 
    12         static void UserModeWait()
    13         {
    14             while (!isCompleted)
    15             {
    16                 Write('.');
    17             }
    18             WriteLine();
    19             WriteLine('Waiting is complete');
    20         }
    21 
    22         static void HybridSpinWait()
    23         {
    24             // 提供对基于自旋的等待的支持
    25             var w = new SpinWait();
    26             while (!isCompleted)
    27             {
    28                 // 执行单一自旋
    29                 w.SpinOnce();
    30                 // 获取对 System.Threading.SpinWait.SpinOnce 的下一次调用是否将产生处理器,同时触发强制上下文切换
    31                 WriteLine(w.NextSpinWillYield);
    32             }
    33             WriteLine('Waiting is complete');
    34         }
    35 
    36         static void Main(string[] args)
    37         {
    38             var t1 = new Thread(UserModeWait);
    39             var t2 = new Thread(HybridSpinWait);
    40 
    41             WriteLine('Running user mode waiting');
    42             t1.Start();
    43             Sleep(20);
    44             isCompleted = true;
    45             Sleep(TimeSpan.FromSeconds(1));
    46             isCompleted = false;
    47             WriteLine('Running hybrid SpinWait construct waiting');
    48             t2.Start();
    49             Sleep(5);
    50             isCompleted = true;
    51         }
    52     }
    53 }

    3、运行该控制台应用程序,运行效果(每次运行效果可能不同)如下图所示:

      在上述程序中,我们创建了一个线程执行一个无线循环20毫秒,直到在主线程中将isCompleted变量设置为true。我们可以将此时间设置为20-30秒,然后打开任务管理器,我们可以看到CPU的使用率比较高。

      我们使用volatile关键字声明了一个名为“isCompleted”的静态字段。volatile 关键字指示一个字段可以由多个同时执行的线程修改。声明为 volatile 的字段不受编译器优化(假定由单个线程访问)的限制。这样可以确保该字段在任何时间呈现的都是最新的值。

      然后,我们使用SpinWait版本,在第29行代码处,我们调用了SpinWait的“SpinOnce”方法,执行一次自旋。当SpinWait自旋达到一定次数后,如果有必要当前线程会让出底层的时间片并触发上下文切换。在这个版本中,如果我们将第49行代码的等待时间修改为20~30秒,然后打开任务管理器,可以发现CPU使用率是比较低的。

      至此,关于线程同步的知识就学习到这儿!

      源码下载  http://files.cnblogs.com/files/yonghuacui/ThreadSynchronization.rar

About IT165 - 广告服务 - 隐私声明 - 版权申明 - 免责条款 - 网站地图 - 网友投稿 - 联系方式
本站内容来自于互联网,仅供用于网络技术学习,学习中请遵循相关法律法规