线程同步 (二)
下面我们接着讲线程同步相关的知识点,本节主要讲解以下四小节的内容:
- CountDownEvent
- Barrier
- ReaderWriterLockSlim
- SpinWait
零、CountDownEvent
CountdownEvent 是一个同步基元,它在收到一定次数的信号之后,将会解除对其等待线程的锁定。 一般用于必须使用 ManualResetEvent 或 ManualResetEventSlim 并且必须在用信号通知事件之前手动递减一个变量的情况,简单的说就是主要用在需要等待多个异步操作完成的情况。
using System;
using System.Threading;
using static System.Console;
using static System.Threading.Thread;
namespace CountDownEventClass
{
class Program
{
static void Main(string[] args)
{
WriteLine("您好,您吃点什么?");
Thread tomThread = new Thread(() => Order("鱼香肉丝、红烧茄子、凉拌腐竹"));
tomThread.Name = "Tom";
Thread jackThread = new Thread(() => Order("猪肉大葱饺子、紫菜蛋花汤"));
jackThread.Name = "Jack";
Thread cookThreaf = new Thread(()=>Cook());
tomThread.Start();
jackThread.Start();
cookThreaf.Start();
Read();
}
static CountdownEvent countDownEvent = new CountdownEvent(2);
static void Order(string order)
{
WriteLine($"{CurrentThread.Name} 开始点餐");
Random ran = new Random();
int sleep = ran.Next(1000, 10000);
Sleep(sleep);
countDownEvent.Signal();
WriteLine($"{CurrentThread.Name} 点餐完毕,我要吃 {order}");
}
static void Cook()
{
WriteLine("大厨等待做饭....");
countDownEvent.Wait();
WriteLine("大厨开始做饭");
Random ran = new Random();
int sleep = ran.Next(1000, 5000);
Sleep(sleep);
WriteLine("大厨做完饭了");
countDownEvent.Dispose();
}
}
}
在上面的代码中我们模拟了一个去饭店吃饭点餐大厨做饭的简单流程。首先我们创建了一个 CountdownEvent 实例,并指定会有两个操作完成时发出信号量。接着我们创建了 Order 和 Cook 方法,分别来模拟点餐和下单。我们在 Cook 方法中调用了 Wait 方法是等待两个信号量的发出,当两个操作完成并都发出信号量时会继续执行后面的代码。同样我们在 Order 方法中调用了 Signal 方法,用来在操作完成后发出信号量。运行结果如下图:
Tip:这里需要注意的是 如果 调用 Signal()没达到指定的次数,那么 Wait() 将一直等待,因此这里要明确有多少个操作会在执行后需要发出信号量,并且要保证每次操作完成后都要调用 Signal() 方法。
一、Barrier
Barrier 是一个很有意思的类,他和 CountDownEvent 类的功能类似,只不过比它多了一个回调函数,这个回调函数会在每个线程完成一节阶段后调用。 Barrier 类经常被用在多线程迭代运算中,用来控制每个线程的阶段。
using System;
using System.Threading;
using static System.Threading.Thread;
using static System.Console;
using System.Collections.Generic;
namespace BarrierClass
{
class Program
{
static void Main(string[] args)
{
List<string> names = new List<string>
{
"Jack",
"Tome",
"Rose",
"Sun"
};
foreach (string name in names)
{
Thread thread = new Thread(() => Work());
thread.Name = name;
thread.Start();
}
Read();
}
static Barrier barrier = new Barrier(4, Publish);
private static void Publish(Barrier b)
{
WriteLine($"{b.ParticipantCount} 名开发人员全部开发完成项目的第 {b.CurrentPhaseNumber + 1} 期,开始发布上线!");
if (b.CurrentPhaseNumber + 1 == 3)
{
WriteLine("项目全部开发完成!");
}
}
static void Work()
{
WriteLine($"{CurrentThread.Name} 完成第 1 期开发");
barrier.SignalAndWait();
WriteLine($"{CurrentThread.Name} 完成第 2 期开发");
barrier.SignalAndWait();
WriteLine($"{CurrentThread.Name} 完成第 3 期开发");
barrier.SignalAndWait();
}
}
}
在上面的代码中我们模拟了项目开发的流程。我们首先定义了一个 Barrier 类的实例,并指定了 4 个需要同步的线程,每个线程都会在调用 SignalAndWai 方法后去调用回调函数 Publish 。这个类在多线程迭代运算中非常有用,我们可以在每个迭代结束前执行一些计算。当最后一个线程调用 SignalAndWait 方法时可以执行一些特殊的操作。
二、ReaderWriterLockSlim
ReaderWriterLockSlim 类会创建线程安全机制,它允许多个线程读取的同时只有一个线程独占资源。我们一般使用 ReaderWriterLockSlim 来保护由多个线程读取但每次只采用一个线程写入的资源。 ReaderWriterLockSlim 允许多个线程均处于读取模式,允许一个线程处于写入模式并独占锁定状态,同时还允许一个具有读取权限的线程处于可升级的读取模式,在此模式下线程无需放弃对资源的读取权限即可升级为写入模式。
using System;
using System.Threading;
using static System.Threading.Thread;
using static System.Console;
using System.Collections.Generic;
using System.Linq;
namespace ReaderWriterLockSlimClass
{
class Program
{
static void Main(string[] args)
{
Thread thread1 = new Thread(Read);
thread1.Start();
Thread thread2 = new Thread(Read);
thread2.Start();
Thread thread3 = new Thread(Read);
thread3.Start();
Thread thread4 = new Thread(Write);
thread4.Name = "Write1";
thread4.Start();
Thread thread5 = new Thread(Write);
thread5.Name = "Write2";
thread5.Start();
Read();
}
static ReaderWriterLockSlim lockSlim = new ReaderWriterLockSlim();
static List<int> items = new List<int>();
static void Read()
{
WriteLine("开始读取 List");
while(true)
{
try
{
lockSlim.EnterReadLock();
foreach (int item in items)
{
WriteLine($"{CurrentThread.Name} 读取 {item}");
}
}
finally
{
lockSlim.ExitReadLock();
}
}
}
static void Write()
{
while(true)
{
try
{
lockSlim.EnterUpgradeableReadLock();
int num = new Random().Next(100);
WriteLine($"{CurrentThread.Name} 写入 {num}");
if (!items.Any(p=>p==num))
{
try
{
lockSlim.EnterWriteLock();
items.Add(num);
}
finally
{
lockSlim.ExitWriteLock();
}
}
}
finally
{
lockSlim.ExitUpgradeableReadLock();
}
}
}
}
}
在上面的代码中我们创建了 5 个线程,其中 3 个线程用来读取数据,而另 2 个线程用来写入数据。这里使用两种锁:读锁允许多线程读取数据,写锁在被释放前会阻塞了其他线程的所有操作。当一旦得到写锁,会阻止阅读者读取数据,进而浪费大量的时间,因此获取写锁后集合会处于阻塞状态。如果要减少阻塞浪费的时间,我们可以使用 EnterUpgradeableReadLock 和 ExitUpgradeableReadLock 方法。先获取读锁后读取数据,如果发现必须修改数据,就使用 EnterWriteLock 方法升级锁,然后执行一次写操作后使用 ExitWriteLock 释放写锁。
三、SpinWait
SpinWait 类是一个混合同步构造,使用用户模式等待一段时间然后切换到内核模式以节省CPU时间减少CPU负载。
using System;
using System.Threading;
using static System.Console;
using static System.Threading.Thread;
namespace SpinWaitClass
{
class Program
{
static void Main(string[] args)
{
Thread thread1 = new Thread(UserMode);
Thread thread2 = new Thread(Spinwait);
thread1.Start();
Sleep(20000);
isCompleted = true;
Sleep(1000);
WriteLine("-----------------------------------------------------");
isCompleted = false;
thread2.Start();
Sleep(50000);
isCompleted = true;
}
static volatile bool isCompleted = false;
static void UserMode()
{
while(!isCompleted)
{
Write("!!!!!!");
}
}
static void Spinwait()
{
SpinWait spinWait = new SpinWait();
while(!isCompleted)
{
spinWait.SpinOnce();
WriteLine(spinWait.NextSpinWillYield);
}
}
}
}
运行上述代码,我们通过任务管理器中的 CPU 使用情况可以看出当程序开始输出 ! 号时 CPU 的使用率明显变高了,但是在运行 20 秒后,切换到 SpinWait 下运行 CPU 的使用率明显降低并接近于平时的使用率。
四、总结
通过两篇文章讲解线程同步,希望大家可以理解其中的内容,在多线程开发中我们可以根据不同的场景使用不同的线程同步的方法或者这些方法的组合。
五、代码下载
- 【自然框架】n级下拉列表框的原理
- 【自然框架】分享 n级联动下拉列表框
- GSM Hacking Part ②:使用SDR捕获GSM网络数据并解密
- 【自然框架】之数据访问 —— 再小的类库也需要设计。
- 【自然框架】之表单控件(一)实体类(Class)VS 字典(Dictionary)
- 跨平台后门Mokes现已加入OS X豪华午餐
- 【问底】严澜:数据挖掘入门——分词
- 机器学习-简单线性回归教程
- NSA(美国国安局)泄漏Exploit分析
- 详解Windows Shim的攻防利用
- 基于Github的源码白盒扫描工具Raptor
- 把业务逻辑变成数据结构和SQL语句的例子。自然架构改成自然框架
- 【自然框架】之通用权限(六):权限到节点
- 【自然框架】之“元数据”的威力
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- 浙大版《C语言程序设计(第3版)》题目集 习题6-2 使用函数求特殊a串数列和
- 浙大版《C语言程序设计(第3版)》题目集 习题6-4 使用函数输出指定范围内的Fibonacci数
- 浙大版《C语言程序设计(第3版)》题目集 习题6-5 使用函数验证哥德巴赫猜想
- Linux Used内存到底哪里去了?
- 浙大版《C语言程序设计(第3版)》题目集 习题6-6 使用函数输出一个整数的逆序数
- 浙大版《C语言程序设计(第3版)》题目集 练习8-2 计算两数的和与差
- SQL查找是否"存在",别再count了!
- 浙大版《C语言程序设计(第3版)》题目集 练习8-8 移动字母
- 超赞!墙裂推荐这款开源、轻量无 Agent 自动化运维平台
- 详解Docker中Image、Container与 Volume 的迁移
- 浙大版《C语言程序设计(第3版)》题目集 习题8-1 拆分实数的整数与小数部分
- 如何在 Linux 上恢复误删除的文件或目录
- 浙大版《C语言程序设计(第3版)》题目集 习题8-2 在数组中查找指定元素
- Pandas学习笔记之时间序列总结
- HTML+JS动态获取当前时间