进程和线程

不同程序执行需要进行调度和独立的内存空间

在单核计算机中,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) {}
}

阻塞线程的执行

  1. 当线程调用 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); // 永久休眠
}
}
  1. 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 超时取消
  1. 取消正在执行的线程
/// <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("结束执行");
}
}
  1. 取消正在阻塞或者执行的线程
/// <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# 线程查漏补缺的更多相关文章

  1. 【Android面试查漏补缺】之事件分发机制详解

    前言 查漏补缺,查漏补缺,你不知道哪里漏了,怎么补缺呢?本文属于[Android面试查漏补缺]系列文章第一篇,持续更新中,感兴趣的朋友可以[关注+收藏]哦~ 本系列文章是对自己的前段时间面试经历的总结 ...

  2. Flutter查漏补缺1

    Flutter 基础知识查漏补缺 Hot reload原理 热重载分为这几个步骤 扫描项目改动:检查是否有新增,删除或者改动,直到找到上次编译后发生改变的dart代码 增量编译:找到改变的dart代码 ...

  3. 《CSS权威指南》基础复习+查漏补缺

    前几天被朋友问到几个CSS问题,讲道理么,接触CSS是从大一开始的,也算有3年半了,总是觉得自己对css算是熟悉的了.然而还是被几个问题弄的"一脸懵逼"... 然后又是刚入职新公司 ...

  4. js基础查漏补缺(更新)

    js基础查漏补缺: 1. NaN != NaN: 复制数组可以用slice: 数组的sort.reverse等方法都会改变自身: Map是一组键值对的结构,Set是key的集合: Array.Map. ...

  5. Entity Framework 查漏补缺 (一)

    明确EF建立的数据库和对象之间的关系 EF也是一种ORM技术框架, 将对象模型和关系型数据库的数据结构对应起来,开发人员不在利用sql去操作数据相关结构和数据.以下是EF建立的数据库和对象之间关系 关 ...

  6. 2019Java查漏补缺(一)

    看到一个总结的知识: 感觉很全面的知识梳理,自己在github上总结了计算机网络笔记就很累了,猜想思维导图的方式一定花费了作者很大的精力,特共享出来.原文:java基础思维导图 自己学习的查漏补缺如下 ...

  7. 20165223 week1测试查漏补缺

    week1查漏补缺 经过第一周的学习后,在蓝墨云班课上做了一套31道题的小测试,下面是对测试题中遇到的错误的分析和总结: 一.背记题 不属于Java后继技术的是? Ptyhon Java后继技术有? ...

  8. 今天開始慢下脚步,開始ios技术知识的查漏补缺。

    从2014.6.30 開始工作算起. 如今已经是第416天了.不止不觉.时间过的真快. 通过对之前工作的总结.发现,你的知识面.会决定你面对问题时的态度.过程和结果. 简单来讲.知识面拓展了,你才干有 ...

  9. Mysql查漏补缺笔记

    目录 查漏补缺笔记2019/05/19 文件格式后缀 丢失修改,脏读,不可重复读 超键,候选键,主键 构S(Stmcture)/完整性I(Integrity)/数据操纵M(Malippulation) ...

  10. 【spring源码分析】IOC容器初始化——查漏补缺(四)

    前言:在前几篇查漏补缺中,其实我们已经涉及到bean生命周期了,本篇内容进行详细分析. 首先看bean实例化过程: 分析: bean实例化开始后 注入对象属性后(前面IOC初始化十几篇文章). 检查激 ...

随机推荐

  1. Landau-Vishkin

    基础算法 假设我们有两个字符串:,每个字符串由A C G T四个字母组成. 在两个字符串上,都有三种可能的编辑操作(突变): 删除某个字符 在某个位置插入字符 改变某个字符 每一个编辑操作都有惩罚值. ...

  2. 【神经网络】softmax回归

    前言 softmax回归为一种分类模型. 基本原理 由于softmax回归也是一种线性叠加算法,且需要输出离散值. 很自然地想到,可以取值最大的输出为置信输出.更进一步想到,如果有三个人A.B.C分别 ...

  3. fastjson反序列化漏洞历史CVE学习整理

    fastjson 1.2.24反序列化漏洞复现 先写一个正常的使用 fastjson的web服务 我们使用 springboot创建 主要是pom.xml 里面要添加fastjson fastjson ...

  4. Nginx四层负载均衡1

    1.Nginx负载均衡Redis 服务器 IP地址 作用 系统版本 Nginx代理服务器 10.0.0.38 负载均衡服务器 Rocky8.6 Redis服务器1 10.0.0.18 Redis服务器 ...

  5. .NET性能优化-是时候换个序列化协议了

    计算机单机性能一直受到摩尔定律的约束,随着移动互联网的兴趣,单机性能不足的瓶颈越来越明显,制约着整个行业的发展.不过我们虽然不能无止境的纵向扩容系统,但是我们可以分布式.横向的扩容系统,这听起来非常的 ...

  6. 搜索"xxxx"的进程,同时杀进程

    一.搜索"xxxx"的进程,同时杀进程,命令如下: ps -ef|grep xxxx|grep -v 'grep'|awk '{print $2}'|xargs kill -9 命 ...

  7. vue3路由的使用

    一.路由的概要 1.1.什么是路由? 路由就是一组映射关系,根据不同的 url 地址展示不同的内容或页面(key-value): key为路径,value可能是function或component 路 ...

  8. dp入门30题

    前言:本文章主要记录一些 \(dp\) 入门题,都是我做过的,希望读者能从这些基础题中打好 \(dp\) 扎实的基础,有不足的地方也欢迎指出.大部分是 \(CodeFoces\) 和 \(Atcode ...

  9. git@github.com: Permission denied (publickey). fatal: Could not read from remote repository.

    当我们拿到一天别人用的电脑,里面是上一位前辈的git ssh key,这时候我们要自己改,改完之后上传代码到远程repository时, 可能会报git@github.com: Permission ...

  10. 更改HTML请求方式的几种方法

    以ctfhub中的请求方式题目为例,则可以有: 法一:通过burpsuite抓包修改 在burpsuite中抓包后发送到repeater模块中,对请求方式进行修改即可 法二:通过curl命令进行 cu ...