C#多线程(11):线程等待
前面我们学习了很多用于线程管理的 类型,也学习了多种线程同步的使用方法,这一篇主要讲述线程等待相关的内容。
在笔者认真探究多线程前,只会new Thread;锁?Lock;线程等待?Thread.Sleep()。
前面已经探究了创建线程的创建姿势和各种锁的使用,也学习了很多类型,也使用到了很多种等待方法,例如 Thread.Sleep()、Thread.SpinWait();、{某种锁}.WaitOne() 等。
这些等待会影响代码的算法逻辑和程序的性能,也有可能会造成死锁,在本篇我们将会慢慢探究线程中等待。
前言
volatile 关键字
volatile 关键字指示一个字段可以由多个同时执行的线程修改。
我们继续使用《C#多线程(3):原子操作》中的示例:
static void Main(string[] args)
{
for (int i = 0; i < 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 < 100_0000; i++)
{
sum += 1;
}
}
运行后你会发现,结果不为 500_0000,而使用 Interlocked.Increment(ref sum);后,可以获得准确可靠的结果。
你试试再运行下面的示例:
static void Main(string[] args)
{
for (int i = 0; i < 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 < 100_0000; i++)
{
sum += 1;
}
}
你以为正常了?哈哈哈,并没有。
volatile 的作用在于读,保证了观察的顺序和写入的顺序一致,每次读取的都是最新的一个值;不会干扰写操作。
详情请点击:https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/keywords/volatile
其原理解释:https://theburningmonk.com/2010/03/threading-understanding-the-volatile-modifier-in-csharp/

三种常用等待
这三种等待分别是:
Thread.Sleep();
Thread.SpinWait();
Task.Delay();
Thread.Sleep(); 会阻塞线程,使得线程交出时间片,然后处于休眠状态,直至被重新唤醒;适合用于长时间的等待;
Thread.SpinWait(); 使用了自旋等待,等待过程中会进行一些的运算,线程不会休眠,用于微小的时间等待;长时间等待会影响性能;
Task.Delay(); 用于异步中的等待,异步的文章后面才写,这里先不理会;
这里我们还需要继续 SpinWait 和 SpinLock 这两个类型,最后再进行总结对照。
再说自旋和阻塞
前面我们学习过自旋和阻塞的区别,这里再来撸清楚一下。
线程等待有内核模式(Kernel Mode)和用户模式(User Model)。
因为只有操作系统才能控制线程的生命周期,因此使用 Thread.Sleep() 等方式阻塞线程,发生上下文切换,此种等待称为内核模式。
用户模式使线程等待,并不需要线程切换上下文,而是让线程通过执行一些无意义的运算,实现等待。也称为自旋。
SpinWait 结构
微软文档定义:为基于自旋的等待提供支持。
如果你想了解 Thread.SpinWait() 是怎么实现的,可以参考 https://www.tabsoverspaces.com/233735-how-is-thread-spinwait-actually-implemented
线程阻塞是会耗费上下文切换的,对于过短的线程等待,这种切换的代价会比较昂贵的。在我们前面的示例中,大量使用了 Thread.Sleep() 和各种类型的等待方法,这其实是不合理的。
SpinWait 则提供了更好的选择。
属性和方法
老规矩,先来看一下 SpinWait 常用的属性和方法。
属性:
| 属性 | 说明 |
|---|---|
| Count | 获取已对此实例调用 SpinOnce() 的次数。 |
| NextSpinWillYield | 获取对 SpinOnce() 的下一次调用是否将产生处理器,同时触发强制上下文切换。 |
方法:
| 方法 | 说明 |
|---|---|
| Reset() | 重置自旋计数器。 |
| SpinOnce() | 执行单一自旋。 |
| SpinOnce(Int32) | 执行单一自旋,并在达到最小旋转计数后调用 Sleep(Int32) 。 |
| SpinUntil(Func) | 在指定条件得到满足之前自旋。 |
| SpinUntil(Func, Int32) | 在指定条件得到满足或指定超时过期之前自旋。 |
| SpinUntil(Func, TimeSpan) | 在指定条件得到满足或指定超时过期之前自旋。 |
自旋示例
下面来实现一个让当前线程等待其它线程完成任务的功能。
其功能是开辟一个线程对 sum 进行 +1,当新的线程完成运算后,主线程才能继续运行。
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 < 1000_0000; i++)
{
sum++;
}
isCompleted = true;
}
// 自定义等待等待
private static bool isCompleted = false;
private static void MySleep()
{
int i = 0;
while (!isCompleted)
{
i++;
}
}
}
新的实现
我们改进上面的示例,修改 MySleep 方法,改成:
private static bool isCompleted = false;
private static void MySleep()
{
SpinWait wait = new SpinWait();
while (!isCompleted)
{
wait.SpinOnce();
}
}
或者改成
private static bool isCompleted = false;
private static void MySleep()
{
SpinWait.SpinUntil(() => isCompleted);
}
SpinLock 结构
微软文档:提供一个相互排斥锁基元,在该基元中,尝试获取锁的线程将在重复检查的循环中等待,直至该锁变为可用为止。
SpinLock 称为自旋锁,适合用在频繁争用而且等待时间较短的场景。主要特征是避免了阻塞,不出现昂贵的上下文切换。
笔者水平有限,关于 SpinLock ,可以参考 https://www.c-sharpcorner.com/UploadFile/1d42da/spinlock-class-in-threading-C-Sharp/
另外,还记得 Monitor 嘛?SpinLock 跟 Monitor 比较像噢~https://www.cnblogs.com/whuanle/p/12722853.html#2monitor
在《C#多线程(10:读写锁)》中,我们介绍了 ReaderWriterLock 和 ReaderWriterLockSlim ,而 ReaderWriterLockSlim 内部依赖于 SpinLock,并且比 ReaderWriterLock 快了三倍。
属性和方法
SpinLock 常用属性和方法如下:
属性:
| 属性 | 说明 |
|---|---|
| IsHeld | 获取锁当前是否已由任何线程占用。 |
| IsHeldByCurrentThread | 获取锁是否已由当前线程占用。 |
| IsThreadOwnerTrackingEnabled | 获取是否已为此实例启用了线程所有权跟踪。 |
方法:
| 方法 | 说明 |
|---|---|
| Enter(Boolean) | 采用可靠的方式获取锁,这样,即使在方法调用中发生异常的情况下,都能采用可靠的方式检查 lockTaken 以确定是否已获取锁。 |
| Exit() | 释放锁。 |
| Exit(Boolean) | 释放锁。 |
| TryEnter(Boolean) | 尝试采用可靠的方式获取锁,这样,即使在方法调用中发生异常的情况下,都能采用可靠的方式检查 lockTaken 以确定是否已获取锁。 |
| TryEnter(Int32, Boolean) | 尝试采用可靠的方式获取锁,这样,即使在方法调用中发生异常的情况下,都能采用可靠的方式检查 lockTaken 以确定是否已获取锁。 |
| TryEnter(TimeSpan, Boolean) | 尝试采用可靠的方式获取锁,这样,即使在方法调用中发生异常的情况下,都能采用可靠的方式检查 lockTaken 以确定是否已获取锁。 |
示例
SpinLock 的模板如下:
private static void DoWork()
{
SpinLock spinLock = new SpinLock();
bool isGetLock = false; // 是否已获得了锁
try
{
spinLock.Enter(ref isGetLock);
// 运算
}
finally
{
if (isGetLock)
spinLock.Exit();
}
}
这里就不写场景示例了。
需要注意的是, SpinLock 实例不能共享,也不能重复使用。
等待性能对比
大佬的文章,.NET 中的多种锁性能测试数据:http://kejser.org/synchronisation-in-net-part-3-spinlocks-and-interlocks/
这里我们简单测试一下阻塞和自旋的性能测试对比。
我们经常说,Thread.Sleep() 会发生上下文切换,出现比较大的性能损失。具体有多大呢?我们来测试一下。(以下运算都是在 Debug 下测试)
测试 Thread.Sleep(1):
private static void DoWork()
{
Stopwatch watch = new Stopwatch();
watch.Start();
for (int i = 0; i < 1_0000; i++)
{
Thread.Sleep(1);
}
watch.Stop();
Console.WriteLine(watch.ElapsedMilliseconds);
}
笔者机器测试,结果大约 20018。Thread.Sleep(1) 减去等待的时间 10000 毫秒,那么进行 10000 次上下文切换需要花费 10000 毫秒,约每次 1 毫秒。
上面示例改成:
for (int i = 0; i < 1_0000; i++)
{
Thread.Sleep(2);
}
运算,发现结果为 30013,也说明了上下文切换,大约需要一毫秒。
改成 Thread.SpinWait(1000):
for (int i = 0; i < 100_0000; i++)
{
Thread.SpinWait(1000);
}
结果为 28876,说明自旋 1000 次,大约需要 0.03 毫秒。
C#多线程(11):线程等待的更多相关文章
- c/c++ 多线程 一个线程等待某种事件发生
多线程 一个线程等待某种事件发生 背景:某个线程在能够完成其任务之前可能需要等待另一个线程完成其任务. 例如:坐夜间列车,为了能够不坐过站, 1,整夜保持清醒,但是这样你就会非常累,不能够睡觉. 2, ...
- Java多线程系列--“基础篇”05之 线程等待与唤醒
概要 本章,会对线程等待/唤醒方法进行介绍.涉及到的内容包括:1. wait(), notify(), notifyAll()等方法介绍2. wait()和notify()3. wait(long t ...
- java 多线程—— 线程等待与唤醒
java 多线程 目录: Java 多线程——基础知识 Java 多线程 —— synchronized关键字 java 多线程——一个定时调度的例子 java 多线程——quartz 定时调度的例子 ...
- c++11 线程:让你的多线程任务更轻松
介绍 本文旨在帮助有经验的Win32程序员来了解c++ 11线程库及同步对象 和 Win32线程及同步对象之间的区别和相似之处. 在Win32中,所有的同步对象句柄(HANDLE)是全局句柄.它们 ...
- c/c++ 多线程 多个线程等待同一个线程的一次性事件
多线程 多个线程等待一个线程的一次性事件 背景:从多个线程访问同一个std::future,也就是多个线程都在等待同一个线程的结果,这时怎么处理. 办法:由于std::future只能被调用一次get ...
- Java多线程02(线程安全、线程同步、等待唤醒机制)
Java多线程2(线程安全.线程同步.等待唤醒机制.单例设计模式) 1.线程安全 如果有多个线程在同时运行,而这些线程可能会同时运行这段代码.程序每次运行结果和单线程运行的结果是一样的,而且其他的变量 ...
- java 多线程系列基础篇(五)之线程等待与唤醒
1.wait(), notify(), notifyAll()等方法介绍 在Object.java中,定义了wait(), notify()和notifyAll()等接口.wait()的作用是让当前线 ...
- 多线程threading初识,线程等待
1.线程是程序里面最小的执行单元. 2.进程是资源的集合. 线程是包含在进程里面的,一个进程可以有多个线程,但只要要有一个线程. 一.多线程,就是N个线程一起干活: 1.传统方式,串行,循环5次需要1 ...
- Java 多线程基础(六)线程等待与唤醒
Java 多线程基础(六)线程等待与唤醒 遇到这样一个场景,当某线程里面的逻辑需要等待异步处理结果返回后才能继续执行.或者说想要把一个异步的操作封装成一个同步的过程.这里就用到了线程等待唤醒机制. 一 ...
- Java多线程(五)——线程等待与唤醒
一.wait().notify().notifyAll()等方法介绍 在Object.java中,定义了wait(), notify()和notifyAll()等接口.wait()的作用是让当前线程进 ...
随机推荐
- JS中typeof和instanceof的区别
01==> 浅谈JS中的typeof和instanceof的区别 // JS中的typeof和instanceof常用来变量是什么类型. // typeof一般返回以下几个字符串: // Str ...
- electron-builder
electron-builder打包工具 首先,确保你的项目中已经安装了 electron-builder.可以在项目根目录下运行以下命令来安装它: npm install electron-buil ...
- 收藏-即时通讯(IM)开源项目OpenIM-功能手册
OpenIM简介 OpenIM是由IM技术专家打造的开源即时通讯组件,也是目前最受欢迎的开源IM项目之一,目前github star近万.开发者通过集成OpenIM组件,并私有化部署服务端,可以将即时 ...
- TienChin-课程管理-创建工程
创建方式与之前一样,如下奉上 generateCourse 代码. @Test void generateCourse() { String path = "E:\\Desktop\\Tie ...
- 虚拟IP绑定公网IP访问
绑定公网 IP 我们目前的虚拟 IP,还不能通过公网的形式进行访问,我们首先,来使用内部的 IP 进行访问看看效果如下: curl 虚拟IP 如上图我访问了两次,第一次访问返回的是 2222 的 ng ...
- python快速入门【六】----真题测试
python入门合集: python快速入门[一]-----基础语法 python快速入门[二]----常见的数据结构 python快速入门[三]-----For 循环.While 循环 python ...
- MySQL【二】---数据库查询详细教程{查询、排序、聚合函数、分组}
1.数据准备.基本的查询(回顾一下) 创建数据库 create database python_test charset=utf8; 查看数据库: show databases; 使用数据库: use ...
- 2.1 Windows驱动开发:内核链表与结构体
在Windows内核中,为了实现高效的数据结构操作,通常会使用链表和结构体相结合的方式进行数据存储和操作.内核提供了一个专门用于链表操作的数据结构LIST_ENTRY,可以用来描述一个链表中的每一个节 ...
- C/C++ 病毒破坏手法总结
针对注册表恶意修改: #include <stdio.h> #include <Windows.h> // 禁用系统任务管理器 void RegTaskmanagerForbi ...
- 面试官:什么是JIT、逃逸分析、锁消除、栈上分配和标量替换?
JIT.逃逸分析.锁消除.栈上分配和标量替换等都属于 JVM 的优化手段,JVM 优化手段是指在运行 Java 程序时,通过对字节码的编译和执行过程进行优化,以提升程序的性能和效率. JVM 优化手段 ...