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() or GetMaxThreads()

    按需创建 最小/最大 线程数

等等

问题

示例如下:

double exp1 = 0;
var mres = new ManualResetEventSlim(false);

ThreadPool.QueueUserWorkItem<ManualResetEventSlim>((_mres) =>
{
	exp1 = Math.Exp(40);
	mres.Set();
}, mres, false);

mres.Wait();

ManualResetEventSlim 类是一个轻量的物体,工作原理有点像发出信号,比如上述代码内容,mres 实例有 SetWait 两种方法,Wait 方法要等到线程中发出的 Set 信号才能执行

这种线程池无法知道第一个参数委托何时能够完成,无法知道线程状态,且代码量更大更复杂时不太好用这种方法管理

所以 Task 出生以解决这类问题

线程切换

线程切换是指 CPU 从一个线程切换到另一个线程执行的过程。线程切换在多任务操作系统中是必不可少的,因为操作系统需要分配 CPU 时间片来运行不同的线程,从而实现并发执行

但线程切换会带来下面的问题:

  1. 性能开销:线程切换需要进行一些上下文切换的操作,包括保存和恢复线程的上下文信息,这些操作会带来一定的性能开销。

  2. 竞态条件:线程切换可能导致竞态条件,即多个线程访问共享资源时的不确定性和冲突问题,从而可能导致死锁、活锁等问题。

  3. 缓存失效:当线程切换时,CPU需要切换到另一个线程的上下文中,这可能导致缓存失效,从而影响系统的性能

解决措施就是使用减少线程、优化算法、使用异步编程

结果

更多

关于任务,还有很多没有探索,任务经过优化,可以在线程池线程上工作,每个任务都有其本地队列

我们没有讨论子任务,它利用了本地队列的数据位置,任务类还包含启发式方法,以便能够找到执行我们任务的最佳方式

总结例子

在以上所举的例子都是 Task 方法更简洁优雅,不用令牌取消(在 Unity 中需要注意),也方便销毁等等,Task 也是下面[[异步]]编程的核心

Thread 类就没有用吗?

不是的,实际 Thread 能用到且高效的地方比较少,用例如下

  • 当我们需要前台执行某些代码时,我们必须使用 ThreadTask 总是在后台线程上运行,这不会阻止应用程序退出

  • Thread 具有特定优先级时,选择本机线程是合理的。在创建过程中,可以通过指定自定义调度程序来间接更改 Task 优先级。实现 Task 调度程序并不总是值得的,这就是为什么产生一个新线程通常是最好的选择

  • Thread 具有关联的稳定身份。这在调试时很有用,因为我们可以知道将执行代码的线程的身份。Task 默认情况下在线程池线程上运行,但是我们已经看到这是可配置的。长时间运行的 Task 有其专用线程,因此,在这种情况下,Task 更有用

总结

以上内容为搜罗试验整理,如果整合不准确或有错误欢迎留言指正!

虽然更多为笔记向但也希望对你能有帮助,如果后续内容有修改增添会及时更新…