Threads
定义
线程被定义为程序的执行路径。每个线程定义了一个唯一的控制流,如果应用程序涉及复杂而耗时的操作,那么设置不同的执行路径或线程通常很有帮助,每个线程执行特定的工作
线程是轻量级过程。使用线程的一个常见例子是现代操作系统实现并发编程。使用线程可以节省 CPU 周期的浪费,并提高应用程序的效率
Thread
对象代表了一个线程的执行状态和行为,它可以直接创建和管理线程。线程是操作系统调度的最小执行单位,一个进程可以包含多个线程,线程之间可以并行执行,但需要注意线程安全的问题。开发人员需要手动控制线程的生命周期、线程的同步和互斥等问题,否则容易引起线程竞争、死锁等问题
在 .Net Framework 4.0 之前都是都是用 Thread
类来进行操作并发编程,此类实例表示操作系统内部调度的托管线程
创建实例
public static void Main()
{
double i = 0;
var t = new Thread(() => { i = Math.Exp(1); });
// Math.Exp() 中的 e 是欧拉数,常数大约为 2.71828,记作 e 的 d 次方
t.Start();// 启动线程
t.Join();// 会等待目标线程结束,将值传回 i
Console.WriteLine(i);
// output: 2.718281828459045
}
这里我们演示了一遍创建子线程操作,思考一下,如果我们频繁对一些操作处理要开线程的话,其加载初始化对操作系统的负载未免加大
这时,微软在 CLR 引入了线程池和其中的可配置线程池参数,线程池不包含线程,只有当需要用到时,才会按需创建,并当线程完成时不会立即销毁而会以暂停状态返回线程池,会保留一段时间直至唤醒,长时间不使用则会销毁
管理
可用 Thread.Sleep(int millisecondsTimeout)
方法来暂停主线程
销毁
以前可见 Abort()
销毁方法,但在 .Net 5.0 之后被列为过时方法
原因:不知道被销毁的线程哪些代码被执行或未执行,比如调用 Thread.Abort
可能会阻止静态构造函数的执行或托管或非托管资源的发布
如果强行使用此方法,会报出 Abort is not supported on this platform
错误,也可以屏蔽此错误以使用,例如
#pragma warning disable SYSLIB0006
Thread.CurrentThread.Abort();
#pragma warning restore SYSLIB0006
但建议使用 CancellationTokenSource
类进行线程的销毁
示例:
in CancellationTest.cs
public static void Main()
{
// 创建取消令牌源实例
CancellationTokenSource cts = new CancellationTokenSource();
Console.WriteLine("In Main: Creating the BackThreadsPool");
// 第二个参数传入实例令牌
ThreadPool.QueueUserWorkItem(CallToChildThread, cts.Token);
Thread.Sleep(1500);
// 发出实例取消请求
cts.Cancel();
Console.WriteLine("Thread Termination!");
// 回收
cts.Dispose();
}
static void CallToChildThread(object? i)
{
try
{
// 创建取消令牌实例
CancellationToken token = new CancellationToken();
Console.WriteLine("Child thread starts");
for (int counter = 0; counter <= 10; counter++)
{
if (!token.IsCancellationRequested) // 判断取消令牌是否发出请求
{
Thread.Sleep(500);
Console.WriteLine(counter);
}
}
Console.WriteLine("Child Thread Completed");
}
catch (ThreadAbortException e)
{
Console.WriteLine("Thread Abort Exception");
}
finally
{
Console.WriteLine("Couldn't catch the Thread Exception");
}
}
//output:
//0
//1
//2
//Thread Termination!
感觉用了 token
令牌管理,更方便内容规划
注意:因为该类继承了 IDisposable
接口,所以必须调用 CancellationTokenSource.Dispose
方法,以释放它拥有的任何非托管资源
后台线程
后台线程(Background thread)是指不会阻止程序终止的线程,即使后台线程仍在运行,程序也可以正常退出。与之相对的是前台线程(Foreground thread),前台线程是指会阻止程序终止的线程,只有所有前台线程都结束了,程序才会正常退出
后台线程通常用于执行一些较为耗时的任务,如文件下载、数据处理、网络通信等。通过将这些任务放在后台线程中执行,可以避免阻塞程序的主线程,从而提高程序的响应性
代码示例:
using System;
using System.Threading;
class Program
{
public static void Main()
{
Thread t = new Thread(DoWork)
{
IsBackground = true // 设置为后台线程
};
t.Start();
Console.WriteLine("Main thread exit.");
}
static void DoWork()
{
Console.WriteLine("Background thread start.");
Thread.Sleep(5000); // 模拟耗时操作
Console.WriteLine("Background thread exit.");
}
}
此示例创建了一个线程并设置为后台线程,即使主线程已经退出,该线程仍然可以继续运行。如果将线程设置为前台线程,则主线程必须等待该线程执行完毕才能退出
线程池
在 TreadPool
类下,其下有许多可配置线程内容
线程池使用后台线程,如果所有前台线程都已终止,则不会使应用程序保持运行
一些用法
ThreadPool.QueueUserWorkItem(WaitCallback callBack, object? state)
在线程池队列添加实现内容,注意内容是
WaitCallback(Object?)
类型回调ThreadPool.GetAvailableThreads(out int workerThreads, out int completionPortThreads)
获取当前可用的线程和 I/O 线程
ThreadPool.GetMinThreads()
orGetMaxThreads()
按需创建 最小/最大 线程数
等等
问题
示例如下:
double exp1 = 0;
var mres = new ManualResetEventSlim(false);
ThreadPool.QueueUserWorkItem<ManualResetEventSlim>((_mres) =>
{
exp1 = Math.Exp(40);
mres.Set();
}, mres, false);
mres.Wait();
ManualResetEventSlim
类是一个轻量的物体,工作原理有点像发出信号,比如上述代码内容,mres
实例有 Set
和 Wait
两种方法,Wait
方法要等到线程中发出的 Set
信号才能执行
这种线程池无法知道第一个参数委托何时能够完成,无法知道线程状态,且代码量更大更复杂时不太好用这种方法管理
所以 Task
出生以解决这类问题
线程切换
线程切换是指 CPU 从一个线程切换到另一个线程执行的过程。线程切换在多任务操作系统中是必不可少的,因为操作系统需要分配 CPU 时间片来运行不同的线程,从而实现并发执行
但线程切换会带来下面的问题:
性能开销:线程切换需要进行一些上下文切换的操作,包括保存和恢复线程的上下文信息,这些操作会带来一定的性能开销。
竞态条件:线程切换可能导致竞态条件,即多个线程访问共享资源时的不确定性和冲突问题,从而可能导致死锁、活锁等问题。
缓存失效:当线程切换时,CPU需要切换到另一个线程的上下文中,这可能导致缓存失效,从而影响系统的性能
解决措施就是使用减少线程、优化算法、使用异步编程
结果
更多
关于任务,还有很多没有探索,任务经过优化,可以在线程池线程上工作,每个任务都有其本地队列
我们没有讨论子任务,它利用了本地队列的数据位置,任务类还包含启发式方法,以便能够找到执行我们任务的最佳方式
总结例子
在以上所举的例子都是 Task
方法更简洁优雅,不用令牌取消(在 Unity 中需要注意),也方便销毁等等,Task
也是下面[[异步]]编程的核心
那 Thread
类就没有用吗?
不是的,实际 Thread
能用到且高效的地方比较少,用例如下
当我们需要前台执行某些代码时,我们必须使用
Thread
,Task
总是在后台线程上运行,这不会阻止应用程序退出当
Thread
具有特定优先级时,选择本机线程是合理的。在创建过程中,可以通过指定自定义调度程序来间接更改Task
优先级。实现Task
调度程序并不总是值得的,这就是为什么产生一个新线程通常是最好的选择Thread
具有关联的稳定身份。这在调试时很有用,因为我们可以知道将执行代码的线程的身份。Task
默认情况下在线程池线程上运行,但是我们已经看到这是可配置的。长时间运行的Task
有其专用线程,因此,在这种情况下,Task
更有用
总结
以上内容为搜罗试验整理,如果整合不准确或有错误欢迎留言指正!
虽然更多为笔记向但也希望对你能有帮助,如果后续内容有修改增添会及时更新…