在上一篇C#多线程之线程同步篇2中,我们主要学习了AutoResetEvent构造、ManualResetEventSlim构造和CountdownEvent构造,在这一篇中,我们将学习Barrier构造、ReaderWriterLockSlim构造和SpinWait构造。

七、使用Barrier构造

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

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

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

 using System;
using System.Threading;
using static System.Console;
using static System.Threading.Thread; namespace Recipe07
{
class Program
{
static Barrier barrier = new Barrier(, b => WriteLine($"End of phase {b.CurrentPhaseNumber + 1}")); static void PlayMusic(string name, string message, int seconds)
{
for(int i = ; i < ; i++)
{
WriteLine("----------------------------------------------");
Sleep(TimeSpan.FromSeconds(seconds));
WriteLine($"{name} starts to {message}");
Sleep(TimeSpan.FromSeconds(seconds));
WriteLine($"{name} finishes to {message}");
barrier.SignalAndWait();
} } static void Main(string[] args)
{
var t1 = new Thread(() => PlayMusic("the guitarist", "play an amazing solo", ));
var t2 = new Thread(() => PlayMusic("the singer", "sing his song", )); t1.Start();
t2.Start();
}
}
}

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”文件,编写代码如下所示:

 using System;
using System.Collections.Generic;
using System.Threading;
using static System.Console;
using static System.Threading.Thread; namespace Recipe08
{
class Program
{
// 表示用于管理资源访问的锁定状态,可实现多线程读取或进行独占式写入访问
static ReaderWriterLockSlim rw = new ReaderWriterLockSlim();
static Dictionary<int, int> items = new Dictionary<int, int>(); static void Read()
{
WriteLine("Reading contents of a dictionary");
while (true)
{
try
{
// 尝试进入读取模式锁定状态
rw.EnterReadLock();
foreach(var key in items.Keys)
{
Sleep(TimeSpan.FromSeconds(0.1));
}
}
finally
{
// 减少读取模式的递归计数,并在生成的计数为 0(零)时退出读取模式
rw.ExitReadLock();
}
}
} static void Write(string threadName)
{
while (true)
{
try
{
int newKey = new Random().Next();
// 尝试进入可升级模式锁定状态
rw.EnterUpgradeableReadLock();
if (!items.ContainsKey(newKey))
{
try
{
// 尝试进入写入模式锁定状态
rw.EnterWriteLock();
items[newKey] = ;
WriteLine($"New key {newKey} is added to a dictionary by a {threadName}");
}
finally
{
// 减少写入模式的递归计数,并在生成的计数为 0(零)时退出写入模式
rw.ExitWriteLock();
}
}
Sleep(TimeSpan.FromSeconds(0.1));
}
finally
{
// 减少可升级模式的递归计数,并在生成的计数为 0(零)时退出可升级模式
rw.ExitUpgradeableReadLock();
}
}
} static void Main(string[] args)
{
new Thread(Read) { IsBackground = true }.Start();
new Thread(Read) { IsBackground = true }.Start();
new Thread(Read) { IsBackground = true }.Start(); new Thread(() => Write("Thread 1")) { IsBackground = true }.Start();
new Thread(() => Write("Thread 2")) { IsBackground = true }.Start(); Sleep(TimeSpan.FromSeconds());
}
}
}

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”文件,编写代码如下所示:

 using System;
using System.Threading;
using static System.Console;
using static System.Threading.Thread; namespace Recipe09
{
class Program
{
static volatile bool isCompleted = false; static void UserModeWait()
{
while (!isCompleted)
{
Write(".");
}
WriteLine();
WriteLine("Waiting is complete");
} static void HybridSpinWait()
{
// 提供对基于自旋的等待的支持
var w = new SpinWait();
while (!isCompleted)
{
// 执行单一自旋
w.SpinOnce();
// 获取对 System.Threading.SpinWait.SpinOnce 的下一次调用是否将产生处理器,同时触发强制上下文切换
WriteLine(w.NextSpinWillYield);
}
WriteLine("Waiting is complete");
} static void Main(string[] args)
{
var t1 = new Thread(UserModeWait);
var t2 = new Thread(HybridSpinWait); WriteLine("Running user mode waiting");
t1.Start();
Sleep();
isCompleted = true;
Sleep(TimeSpan.FromSeconds());
isCompleted = false;
WriteLine("Running hybrid SpinWait construct waiting");
t2.Start();
Sleep();
isCompleted = true;
}
}
}

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

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

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

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

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

  源码下载

C#多线程之线程同步篇3的更多相关文章

  1. C#多线程之线程同步篇2

    在上一篇C#多线程之线程同步篇1中,我们主要学习了执行基本的原子操作.使用Mutex构造以及SemaphoreSlim构造,在这一篇中我们主要学习如何使用AutoResetEvent构造.Manual ...

  2. C#多线程之线程同步篇1

    在多线程(线程同步)中,我们将学习多线程中操作共享资源的技术,学习到的知识点如下所示: 执行基本的原子操作 使用Mutex构造 使用SemaphoreSlim构造 使用AutoResetEvent构造 ...

  3. C#多线程之线程池篇1

    在C#多线程之线程池篇中,我们将学习多线程访问共享资源的一些通用的技术,我们将学习到以下知识点: 在线程池中调用委托 在线程池中执行异步操作 线程池和并行度 实现取消选项 使用等待句柄和超时 使用计时 ...

  4. 关于Java多线程的线程同步和线程通信的一些小问题(顺便分享几篇高质量的博文)

    Java多线程的线程同步和线程通信的一些小问题(顺便分享几篇质量高的博文) 前言:在学习多线程时,遇到了一些问题,这里我将这些问题都分享出来,同时也分享了几篇其他博客主的博客,并且将我个人的理解也分享 ...

  5. C#多线程之线程池篇3

    在上一篇C#多线程之线程池篇2中,我们主要学习了线程池和并行度以及如何实现取消选项的相关知识.在这一篇中,我们主要学习如何使用等待句柄和超时.使用计时器和使用BackgroundWorker组件的相关 ...

  6. C#多线程之线程池篇2

    在上一篇C#多线程之线程池篇1中,我们主要学习了如何在线程池中调用委托以及如何在线程池中执行异步操作,在这篇中,我们将学习线程池和并行度.实现取消选项的相关知识. 三.线程池和并行度 在这一小节中,我 ...

  7. 重新想象 Windows 8 Store Apps (46) - 多线程之线程同步: Lock, Monitor, Interlocked, Mutex, ReaderWriterLock

    [源码下载] 重新想象 Windows 8 Store Apps (46) - 多线程之线程同步: Lock, Monitor, Interlocked, Mutex, ReaderWriterLoc ...

  8. 重新想象 Windows 8 Store Apps (47) - 多线程之线程同步: Semaphore, CountdownEvent, Barrier, ManualResetEvent, AutoResetEvent

    [源码下载] 重新想象 Windows 8 Store Apps (47) - 多线程之线程同步: Semaphore, CountdownEvent, Barrier, ManualResetEve ...

  9. IOS 多线程,线程同步的三种方式

    本文主要是讲述 IOS 多线程,线程同步的三种方式,更多IOS技术知识,请登陆疯狂软件教育官网. 一般情况下我们使用线程,在多个线程共同访问同一块资源.为保护线程资源的安全和线程访问的正确性. 在IO ...

随机推荐

  1. c#与java的区别

    经常有人问这种问题,用了些时间java之后,发现这俩玩意除了一小部分壳子长的还有能稍微凑合上,基本上没什么相似之处,可以说也就是马甲层面上的相似吧,还是比较短的马甲... 一般C#多用于业务系统的开发 ...

  2. java基础_集合List与Set接口

    List接口继承了Collection的方法  当然也有自己特有的方法向指定位置添加元素   add(索引,添加的元素); 移除指定索引的元素   remove(索引) 修改指定索引的元素   set ...

  3. javaScript的原型继承与多态性

    1.prototype 我们可以简单的把prototype看做是一个模版,新创建的自定义对象都是这个模版(prototype)的一个拷贝 (实际上不是拷贝而是链接,只不过这种链接是不可见,给人们的感觉 ...

  4. javascript之活灵活现的Array

    前言 就如同标题一样,这篇文章将会灵活的运行Array对象的一些方法来实现看上去较复杂的应用. 大家都知道Array实例有这四个方法:push.pop.shift.unshift.大家也都知道 pus ...

  5. Xamarin+Prism小试牛刀:定制跨平台Outlook邮箱应用

    通过本文你将学会如下内容: 1,如何使用Xamarin开发跨平台(Windows,Android,iOS)应用. 2,如何使用微软的登录界面登入Microsoft账号. 3,如何使用Outlook邮箱 ...

  6. UWP开发之Mvvmlight实践七:如何查找设备(Mobile模拟器、实体手机、PC)中应用的Log等文件

    在开发中或者后期测试乃至最后交付使用的时候,如果应用出问题了我们一般的做法就是查看Log文件.上章也提到了查看Log文件,这章重点讲解下如何查看Log文件?如何找到我们需要的Packages安装包目录 ...

  7. 一款开源免费的.NET文档操作组件DocX(.NET组件介绍之一)

    在目前的软件项目中,都会较多的使用到对文档的操作,用于记录和统计相关业务信息.由于系统自身提供了对文档的相关操作,所以在一定程度上极大的简化了软件使用者的工作量. 在.NET项目中如果用户提出了相关文 ...

  8. 玩转Vim 编辑器

    一:VIM快速入门 1.vim模式介绍 以下介绍内容来自维基百科Vim 从vi演生出来的Vim具有多种模式,这种独特的设计容易使初学者产生混淆.几乎所有的编辑器都会有插入和执行命令两种模式,并且大多数 ...

  9. 2014年暑假c#学习笔记目录

    2014年暑假c#学习笔记 一.C#编程基础 1. c#编程基础之枚举 2. c#编程基础之函数可变参数 3. c#编程基础之字符串基础 4. c#编程基础之字符串函数 5.c#编程基础之ref.ou ...

  10. 卸载oracle之后,如何清除注册表

    之前卸载了oracle,今天偶然间发现,在服务和应用程序里面,还残存着之前的oracle服务.原来,还需要去清理下注册表. 在开始菜单的这个框里面 输入regedit,进入注册表.找到这个目录 HKE ...