C# 线程查漏补缺
进程和线程
不同程序执行需要进行调度和独立的内存空间
在单核计算机中,CPU 是独占的,内存是共享的,这时候运行一个程序的时候是没有问题。但是运行多个程序的时候,为了不发生一个程序霸占整个 CPU 不释放的情况(如一个程序死循环无法结束了,那么其他程序就没有机会运行了),就需要开发者给不同程序划分不同的执行时间。为了避免不同程序之间互相操作数据或代码,导致程序被破坏的情况,就需要开发者给程序划分独立的内存范围。也就是程序需要开发者进行调度以及和划分独立的内存空间。
进程是应用程序的一个实例
为了避免每个开发者来进行这个工作,所以有了操作系统。操作系统负责整个计算机的程序调度,让每个程序都有机会使用CPU,同时使用来进程来为程序维护一个独立虚拟空间,确保程序间的运行不会互相干扰。所以进程就是程序的一个实例,拥有程序需要使用的资源集合,确保自己的资源不会被其他进程破坏。
线程是操作系统进行调度的最小单位
这时候一个进程一次只能处理一个任务,如果需要一边不停输出 hellowork,一边计时,那么需要启动两个进程。如果需要对一个队列同时入队出队,那么不仅需要两个进程,还需要两个进程可以访问相同的内存空间。所以为了进程可以并发地处理任务,同时共享相同的资源,就需要给进程一个更小的调度单位,也就是线程,因此,线程也叫轻量化进程。所以在现代计算机中,操作继续不会直接调度进程在 CPU 上执行,而是调度线程在 CPU 上执行,所以说,线程是操作系统进行调度的最小单位。
线程操作
新建线程、启动线程、线程优先级
public void Test()
{
var t = new Thread(() => { }); // 使用无参委托
var t2 = new Thread(state => { }); // 使用 object? 参数委托
var t3 = new Thread(DoWork);
var t4 = new Thread(DoWork2);
t.Priority = ThreadPriority.Highest; // 设置线程的优先级,默认是 ThreadPriority.Normal
t.Start(); // 不传入参数,启动线程
t2.Start("参数"); // 传入参数,启动线程
void DoWork() {}
void DoWork2(object? state) {}
}
阻塞线程的执行
- 当线程调用 Sleep() 或者等待锁时,进入阻塞状态。
public void Test()
{
var pool = new SemaphoreSlim(0, 1);
var t = new Thread(DoWork);
var t2 = new Thread(DoWork2);
t.Start();
t2.Start();
void DoWork()
{
pool.Wait(); // 等待信号量
}
void DoWork2()
{
Thread.Sleep(Timeout.Infinite); // 永久休眠
}
}
- Thread.Sleep() 不仅用于休眠,也可以用于让出当前 CPU 时间,让其他正在等待 CPU 的线程也有机会抢到 CPU 时间。
tip:相似的方法,Thread.Yield() 也有让出 CPU 时间的功能。
tip:不同的方法,Thread.SpinWait() 不会让出 CPU 控制权,而是进行自旋。
Thread.Sleep(0) 让出控制权给同等优先级的线程执行,如果没有,就继续执行本线程。
Thread.Sleep(1) 让出控制权给正在等待的线程执行。
Thread.Yield() 让出控制权给CPU上的其他线程。
Thread.SpinWait() 不让出控制权,在CPU上自旋一段时间。
中断阻塞中的线程
当线程处于阻塞状态时,其他线程调用阻塞线程的 Thread.Interrupt() 时,会中断线程并抛出 System.Threading.ThreadInterruptedException。
tip:如果线程没有处于阻塞状态,那么调用 Thread.Interrupt() 则不会有效果。
public void Test3()
{
var sleepSwitch = false;
var pool = new SemaphoreSlim(0, 1);
var t = new Thread(DoWork);
t.Start();
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} 调用 {t.ManagedThreadId} 的 Interrupt()");
t.Interrupt();
Thread.Sleep(3000);
sleepSwitch = true;
var t2 = new Thread(DoWork2);
t2.Start();
Thread.Sleep(2000);
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} 调用 {t2.ManagedThreadId} 的 Interrupt()");
t2.Interrupt();
void DoWork()
{
try
{
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}: 开始执行");
while (!sleepSwitch)
{
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}: 自旋 SpinWait()");
Thread.SpinWait(10000000); // 只是进行自旋,不阻塞线程,所以不会被中断
}
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}: 休眠 Sleep()");
Thread.Sleep(Timeout.Infinite);
}
catch (Exception e)
{
Console.WriteLine(e);
}
}
void DoWork2()
{
try
{
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}: 开始执行");
pool.Wait();
}
catch (Exception e)
{
Console.WriteLine(e);
}
}
}
取消线程的执行
取消正在执行中或者阻塞中的线程有多种方法
- 调用 Thread.Interrupt() 中断线程
- 调用 CancellationTokenSource.Cancel() 或者超时取消
- 通过 WaitHandle 超时取消
- 取消正在执行的线程
/// <summary>
/// 使用 CancellationToken 取消处于死循环的线程,或者超时取消
/// </summary>
public void Test2()
{
var cts = new CancellationTokenSource(5000);
Task.Run(() =>
{
Console.WriteLine("按下 c 取消线程,或者五秒后取消");
if (Console.ReadKey().Key == ConsoleKey.C)
{
cts.Cancel();
}
});
var t = new Thread(DoWork);
t.Start(cts.Token);
void DoWork(object? state)
{
var ct = (CancellationToken)state;
while (!ct.IsCancellationRequested)
{
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} 自旋");
Thread.SpinWait(10000000);
}
Console.WriteLine("结束执行");
}
}
- 取消正在阻塞或者执行的线程
/// <summary>
/// 使用 WaitHandle.WaitAny 取消被阻塞的线程,或者超时取消,或者使用 CancellationToken 协助式取消
/// </summary>
public void Test3()
{
var pool = new Semaphore(0, 1);
var cts = new CancellationTokenSource();
Task.Run(() =>
{
Console.WriteLine("按下 c 调用 CancellationTokenSource.Cancel() 取消线程,或者按下 v 调用 Semaphore.Release() 取消线程,或者五秒后取消");
switch (Console.ReadKey().Key)
{
case ConsoleKey.C:
cts.Cancel();
break;
case ConsoleKey.V:
pool.Release();
break;
}
if (Console.ReadKey().Key == ConsoleKey.C)
{
cts.Cancel();
}
});
var t = new Thread(DoWork);
t.Start();
void DoWork()
{
var signalIndex = WaitHandle.WaitAny(new WaitHandle[] { pool, cts.Token.WaitHandle }, 5000);
if (signalIndex == 0)
{
Console.WriteLine("调用 Semaphore.Release() 取消线程");
}
else if (cts.Token.IsCancellationRequested)
{
Console.WriteLine("CancellationTokenSource.Cancel() 取消线程");
}
else if (signalIndex == WaitHandle.WaitTimeout)
{
Console.WriteLine("超时取消");
}
Console.WriteLine("结束运行");
}
}
线程异常和线程返回值
当调用 Thread.Abort() 或者 Thread.Interrupt() 就会抛出异常,线程执行的代码也会抛出异常,所以线程出现异常是很常见的。
当直接新建线程并执行,或者调用 ThreadPool.QueueUserWorkItem() 使用线程池线程执行代码,出现未捕获的异常时,会导致程序崩溃。
在线程中执行方法,是无法直接知道方法是否执行完毕,或者得到返回值的。
避免未捕获异常导致程序崩溃或者得到在其他线程执行方法的返回值,所以可以使用 Task.Run() 来执行代码,Task 已经处理了未捕获异常,也可以直接得到返回值。
也可以使用委托包装一下线程执行的代码,变成一个能安全执行的代码。
internal class ThreadExceptionTest
{
public async void Test()
{
ThreadPool.QueueUserWorkItem(_ => ThreadThrowException()); // 未捕获异常导致程序崩溃
var t = new Thread(_ => ThreadThrowException()); // 未捕获异常导致程序崩溃
t.IsBackground = true;
t.Start();
var _ = Task.Run(ThreadThrowException); // 未捕获异常也不会导致程序崩溃
string? r = null;
Exception? e = null;
var t2 = new Thread(_ => SafeExecute(ThreadReturnValue, out r, out e)); // 通过委托获取返回值
t2.Start();
t2.Join();
Console.WriteLine(r);
var t3 = new Thread(_ => SafeExecute(ThreadThrowException, out r, out e)); // 通过委托处理异常
t3.Start();
t3.Join();
Console.WriteLine(e);
Console.WriteLine(await SafeExecute(ThreadReturnValue)); // 通过委托获取返回值
try
{
await SafeExecute(ThreadThrowException); // 通过委托处理异常
}
catch (Exception exception)
{
Console.WriteLine(exception);
}
}
public string ThreadThrowException()
{
Thread.Sleep(1000);
throw new Exception("线程异常");
}
public string ThreadReturnValue()
{
Thread.Sleep(1000);
return "done";
}
/// <summary>
/// 捕获异常,并通过 out 获取返回值
/// </summary>
public void SafeExecute<T>(Func<T> func, out T? r, out Exception? e)
{
try
{
e = null;
r = func();
}
catch (Exception? exception)
{
r = default;
e = exception;
}
}
/// <summary>
/// 捕获异常,并通过 TaskCompletionSource 获取返回值
/// </summary>
public Task<T> SafeExecute<T>(Func<T> func)
{
var t = new TaskCompletionSource<T>();
try
{
t.TrySetResult(func());
}
catch (Exception e)
{
t.SetException(e);
}
return t.Task;
}
}
插槽和 ThreadStatic
.Net 提供了两种线程相关变量的方法。
- 插槽
Thread.AllocateDataSlot() Thread.AllocateDataSlot() 可以给方法设置一个线程插槽,插槽里面的值是线程相关的,也就是每个线程特有的,同一个变量不同线程无法互相修改。一般在静态构造方法中初始化。
Thread.GetData() Thread.SetData() 可以对插槽取值和赋值。
插槽是动态的,在运行时进行赋值的,而且 Thread.GetData() 返回值是 object,如果线程所需的值类型不固定,可以使用插槽。 - ThreadStaticAttribute
ThreadStaticAttribute 标记静态变量时,该变量是线程相关的,不同线程的静态变量值是不一样的。
[ThreadStatic] IDE 可以提供编译检查,性能和安全性更好,如果线程所需的值类型是固定的,就应该使用 [ThreadStatic]。
tip: 插槽和 [ThreadStatic] 中的值一般不初始化,因为跟线程相关,在哪个线程初始化,只有那个线程可以看到这个初始化后的值,所以初始化也就没啥意义了。
internal class ThreadDemo
{
/// <summary>
/// 测试 ThreadStaticAttribute
/// </summary>
public void Test()
{
Parallel.Invoke(StaticThreadDemo.Test, StaticThreadDemo.Test, StaticThreadDemo.Test); // 打印对应线程的ID,证明被 [ThreadStatic] 标记过的字段是线程相关的。
}
/// <summary>
/// 测试 LocalDataStoreSlot
/// </summary>
public void Test2()
{
Parallel.Invoke(StaticThreadDemo.Test2, StaticThreadDemo.Test2, StaticThreadDemo.Test2); // 打印对应线程的ID,证明 LocalDataStoreSlot 是线程相关的。
}
}
static class StaticThreadDemo
{
[ThreadStatic]
private static int? _threadId = null;
public static void Test()
{
_threadId = Thread.CurrentThread.ManagedThreadId;
Thread.Sleep(500);
Console.WriteLine($"ThreadId:{Thread.CurrentThread.ManagedThreadId} ThreadStatic: {_threadId}");
}
private static LocalDataStoreSlot _localSlot;
static StaticThreadDemo()
{
_localSlot = Thread.AllocateDataSlot();
}
public static void Test2()
{
Thread.SetData(_localSlot, Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(500);
Console.WriteLine($"ThreadId:{Thread.CurrentThread.ManagedThreadId} LocalSlot:{Thread.GetData(_localSlot)}");
}
}
线程池操作
线程需要维护自己的栈和上下文,新建线程是有空间(一个线程大概需要 1M 内存)和时间(CPU 切换线程的时间)上的开销的,所以一般不会手动新建线程并执行代码,而是把代码交给线程池操作,线程池会根据电脑的 CPU 核数初始化线程数量,根据线程忙碌情况新增线程。
Task.Run() 最终也是通过线程池执行异步操作的。
让线程池里的线程执行代码
ThreadPool.QueueUserWorkItem((state) => { Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}"); });
使用 WaitHandle 控制线程池代码的执行
ThreadPool.RegisterWaitForSingleObject() 提供了一种方法,传入一个 WaitHandle 子类或者定时执行线程池的代码。
internal class ThreadPoolDemo
{
public void Test()
{
var ti = new TaskInfo
{
Info = "其他信息"
};
var are = new AutoResetEvent(false);
var handle = ThreadPool.RegisterWaitForSingleObject(are, DoWork, ti, 2000, false); // 定时 2s 执行
//var handle = ThreadPool.RegisterWaitForSingleObject(are, DoWork, ti, Timeout.Infinite, false); // 也可以不定时执行
ti.WaitHandle = handle;
Thread.Sleep(3000);
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} 重新 signal AutoResetEvent");
are.Set();
Thread.Sleep(2000);
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} 第二次 signal AutoResetEvent");
are.Set(); // 调用后没有反应,证明 CallBack 已经被取消注册
void DoWork(object? state, bool timeout)
{
if (timeout)
{
Console.WriteLine("超时");
}
else
{
var taskInfo = (TaskInfo)state;
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} 执行完毕,取消 Callback");
taskInfo.WaitHandle.Unregister(null); // 取消回调,不然会回调会一直循环执行,而且应该用 Unregister 来取消,只在构造函数里面指定 executeOnlyOnce:true 的话,可能会无法 gc 回调。
}
}
}
class TaskInfo
{
public RegisteredWaitHandle WaitHandle { get; set; }
public string Info { get; set; }
}
}
最后
其实日常开发都是用 Task,回顾一下 Thread 可以写出更加优秀的异步代码,下次回顾一下线程同步的知识。
源码 https://github.com/yijidao/blog/tree/master/TPL/ThreadDemo/ThreadDemo3
C# 线程查漏补缺的更多相关文章
- 【Android面试查漏补缺】之事件分发机制详解
前言 查漏补缺,查漏补缺,你不知道哪里漏了,怎么补缺呢?本文属于[Android面试查漏补缺]系列文章第一篇,持续更新中,感兴趣的朋友可以[关注+收藏]哦~ 本系列文章是对自己的前段时间面试经历的总结 ...
- Flutter查漏补缺1
Flutter 基础知识查漏补缺 Hot reload原理 热重载分为这几个步骤 扫描项目改动:检查是否有新增,删除或者改动,直到找到上次编译后发生改变的dart代码 增量编译:找到改变的dart代码 ...
- 《CSS权威指南》基础复习+查漏补缺
前几天被朋友问到几个CSS问题,讲道理么,接触CSS是从大一开始的,也算有3年半了,总是觉得自己对css算是熟悉的了.然而还是被几个问题弄的"一脸懵逼"... 然后又是刚入职新公司 ...
- js基础查漏补缺(更新)
js基础查漏补缺: 1. NaN != NaN: 复制数组可以用slice: 数组的sort.reverse等方法都会改变自身: Map是一组键值对的结构,Set是key的集合: Array.Map. ...
- Entity Framework 查漏补缺 (一)
明确EF建立的数据库和对象之间的关系 EF也是一种ORM技术框架, 将对象模型和关系型数据库的数据结构对应起来,开发人员不在利用sql去操作数据相关结构和数据.以下是EF建立的数据库和对象之间关系 关 ...
- 2019Java查漏补缺(一)
看到一个总结的知识: 感觉很全面的知识梳理,自己在github上总结了计算机网络笔记就很累了,猜想思维导图的方式一定花费了作者很大的精力,特共享出来.原文:java基础思维导图 自己学习的查漏补缺如下 ...
- 20165223 week1测试查漏补缺
week1查漏补缺 经过第一周的学习后,在蓝墨云班课上做了一套31道题的小测试,下面是对测试题中遇到的错误的分析和总结: 一.背记题 不属于Java后继技术的是? Ptyhon Java后继技术有? ...
- 今天開始慢下脚步,開始ios技术知识的查漏补缺。
从2014.6.30 開始工作算起. 如今已经是第416天了.不止不觉.时间过的真快. 通过对之前工作的总结.发现,你的知识面.会决定你面对问题时的态度.过程和结果. 简单来讲.知识面拓展了,你才干有 ...
- Mysql查漏补缺笔记
目录 查漏补缺笔记2019/05/19 文件格式后缀 丢失修改,脏读,不可重复读 超键,候选键,主键 构S(Stmcture)/完整性I(Integrity)/数据操纵M(Malippulation) ...
- 【spring源码分析】IOC容器初始化——查漏补缺(四)
前言:在前几篇查漏补缺中,其实我们已经涉及到bean生命周期了,本篇内容进行详细分析. 首先看bean实例化过程: 分析: bean实例化开始后 注入对象属性后(前面IOC初始化十几篇文章). 检查激 ...
随机推荐
- VUE v-model 语法糖
v-model 语法糖 描述:弹出利用v-model语法糖 父组件 子组件
- Java多线程-线程生命周期(一)
如果要问我Java当中最难的部分是什么?最有意思的部分是什么?最多人讨论的部分是什么?那我会毫不犹豫地说:多线程. Java多线程说它难,也不难,就是有点绕:说它简单,也不简单,需要理解的概念很多,尤 ...
- 2022-11-06 Acwing每日一题
本系列所有题目均为Acwing课的内容,发表博客既是为了学习总结,加深自己的印象,同时也是为了以后回过头来看时,不会感叹虚度光阴罢了,因此如果出现错误,欢迎大家能够指出错误,我会认真改正的.同时也希望 ...
- 使用 JWT 生成 token
JWT 简介 JWT:Json Web Token 官网:https://jwt.io 优点:可生成安全性较高的 token 且可以完成时效性的检验(登陆过期检查) JWT 结构:(由官网获取) JW ...
- 基于python的数学建模---多模糊评价
权重 ak的确定--频数统计法 选取正整数p的方法 画箱形图 取1/4与3/4的距离(IQR) ceil()取整 代码: import numpy as np def frequency(mat ...
- 各类数据库写入Webhsell总结
1.MySQL写入WebShell 1.1写入条件 数据库的当前用户为ROOT或拥有FILE权限: 知道网站目录的绝对路径: PHP的GPC参数为off状态: MySQL中的secure_file_p ...
- MySQL进阶实战6,缓存表、视图、计数器表
一.缓存表和汇总表 有时提升性能最好的方法是在同一张表中保存衍生的冗余数据,有时候还需要创建一张完全独立的汇总表或缓存表. 缓存表用来存储那些获取很简单,但速度较慢的数据: 汇总表用来保存使用grou ...
- 关于ckPlayer 视频加密那些事
最近疫情期间,公司在做一个在线行业教育收费平台,所以不得不做视频转码/切片/加密. 现在只说视频加密如何实现,找遍了所有百度,几乎没有提供相应的源码和例子. 而ckPlayer官网有一个收费的案例:如 ...
- 浅谈入行Qt桌面端开发程序员-从毕业到上岗(1):当我们说到桌面端开发时,我们在谈论什么?
谈谈我自己 大家好,我是轩先生,是一个刚入行的Qt桌面端开发程序员.我的本科是双非一本的数学专业,22年毕业,只是部分课程与计算机之间有所交叉,其实在我毕业的时候并没有想过会成为一名程序员,也没有想过 ...
- java中方法传参形式
成员方法传参形式: 1.基本数据类型:传递的是值 public class Object03 { public static void main(String[] args) { AA aa = ne ...