C#学习笔记——线程池与Task类

目录

目录

一、线程池

1、Unity线程回顾

  • Unity支持多线程。
  • Unity开启多线程后,非主线程不能使用Unity场景上的对象,比如this.transform
  • Unity开启多线程后一定记得关闭,否则会在编辑模式中继续运行。

2、线程池

(1)概述

命名空间:System.Threading
类名:ThreadPool

在多线程的应用程序开发中,频繁的创建删除线程会带来性能消耗,产生内存垃圾,为了避免这种开销,可以使用线程池。

ThreadPool中有若干数量的线程,如果有任务需要处理时,会从线程池中获取一个空闲的线程来执行任务 ,任务执行完毕后线程不会销毁,而是被线程池回收以供后续任务使用。当线程池中所有的线程都在忙碌时,又有新任务要处理时,线程池才会新建一个线程来处理该任务。如果线程数量达到设置的最大值,任务会排队,等待其他任务释放线程后再执行。

线程池相当于就是一个专门装线程的缓存池,优点是节省开销,减少线程的创建,有效减少GC触发;缺点是不能控制线程池中线程的执行顺序,也不能获取线程池内线程取消/异常/完成的通知

(2)使用

ThreadPool是一个静态类,提供了很多比较重要的方法:

  • 获取可用工作线程数和I/O线程数
public static void GetAvailableThreads(out int workerThreads, out int completionPortThreads);
  • 获取工作线程和 I/O 线程的最大(最小)数目
public static void GetMaxThreads(out int workerThreads, out int completionPortThreads);
public static void GetMinThreads(out int workerThreads, out int completionPortThreads);
  • 设置工作线程和 I/O 线程的最大(最小)数目
    • 返回值代表是否设置成功
public static bool SetMaxThreads(int workerThreads, int completionPortThreads);
public static bool SetMinThreads(int workerThreads, int completionPortThreads);
  • 向线程池提交任务到队列中,由线程池自动调度执行
public static bool QueueUserWorkItem(WaitCallback callBack);

其中这个WaitCallback就是一个委托,这个state可以使用QueueUserWorkItem的重载方法传进去

public delegate void WaitCallback(object state);

二、Task类

1、概述

命名空间:System.Threading.Tasks
类名:Task

Task顾名思义就是任务的意思,是在线程池基础上进行的改进,它拥有线程池的优点,同时解决了使用线程池不易控制的弊端。Task看起来像一个Thread,实际上,它是在ThreadPool的基础上进行的封装,使用Task可以让我们更方便高效的进行多线程开发。

2、创建Task

首先是无返回值的委托,即委托参数为Action action

  • 方式一:new Task
Task t1 = new Task(action);
t1.Start();
  • 方式二:Task.Run
Task t2 = Task Run(action);
  • 方式三:Task.Factory.StartNew
Task t3 = Task.Factory.StartNew(action);

如果创建的任务需要有返回值,则添加泛型即可,此时传入的委托参数就是Func<TResult> function

  • 方式一:new Task<TResult>
Task<int> t1 = new Task<int>(function);
t1.Start();
  • 方式二:Task.Run<TResult>
Task<int> t2 = Task Run<int>(function);
  • 方式三:Task.Factory.StartNew<TResult>
Task<int> t3 = Task.Factory.StartNew<int>(action);

3、获取返回值

如何获取Task的返回值呢?很简单:

int res = t1.Result;

注意事项

  • Result 获取结果时会阻塞该线程,即如果 task 没有执行完成,会等待 task 执行完成获取到 Result,然后再执行后边的代码。
  • 因此如果 task 的 return 之前的代码是死循环的话,获取 Result 的该线程就会一直等待资源,导致卡死。

4、同步执行Task

之前的Task都是异步执行的,如果想要同步执行,则需要使用new Task的方式,并且调用task的RunSynchronously方法(因为使用Run和StartNew方法会在创建时就启动)。

举个例子:

Task t = new Task(() =>
{
    Thread.Sleep(1000);
    print("线程同步执行");
});
t.RunSynchronously();
print("主线程执行");

之前使用t.start()时,是不影响主线程执行的,此时主线程会比线程先打印。但是使用了RunSynchronously之后,主线程就会在t执行完之后再执行。

5、Task线程阻塞

  • Wait:针对单个任务,线程会等待该任务执行完毕再继续执行
t.Wait(); // 等待t执行完才可以执行后续代码
  • WaitAny:任务列表中任何一个线程执行完毕即可继续执行
Task.WaitAny(t1, t2); // 传入变长参数
  • WaitAll:任务列表中所有线程执行完毕方可继续执行
Task.WaitAll(t1, t2); // 传入变长参数

6、Task任务延续

  • WhenAll + ContinueWith所有任务完毕后再执行某任务
    • 其中这个t代表的是前面Task.WhenAll(t1, t2) 这个整体任务
Task.WhenAll(t1, t2).ContinueWith((t) =>
{
    print("新任务开始了...");
});
  • WhenAny + ContinueWith任意任务完成后即可执行某任务
Task.WhenAny(t1, t2).ContinueWith((t) =>
{
    print("新任务开始了...");
});

当然上面两个方法可以使用Task.Factory里的这两个方法替代,效果是一样的:

  • ContinueWhenAll:等价于Task的WhenAll+ContinueWith
Task.Factory.ContinueWhenAll(new Task[] { t1, t2 }, (t) =>
{
    print("新任务开始了...");
});
  • ContinueWhenAny:等价于Task的WhenAny+ContinueWith
Task.Factory.ContinueWhenAny(new Task[] { t1, t2 }, (t) =>
{
    print("新任务开始了...");
});

7、取消Task执行

  • 方式一:加入bool标识,控制线程内死循环结束
  • 方式二:通过CancellationTokenSource取消标识源类 控制,下例中,cts.IsCancellationRequested表示是否已经请求取消线程的执行
CancellationTokenSource cts = new CancellationTokenSource();
Task.Run(() =>
{
    while (!cts.IsCancellationRequested)
    {
        print("任务执行中...");
        Thread.Sleep(1000);
    }
});

如果需要取消任务执行,只需要调用这个方法即可:

cts.Cancel();

当然你可能会问这和方式一不是一样吗?其实CancellationTokenSource除了可以取消执行,还提供了延迟取消、取消后的回调等功能,比方式一功能更加丰富:

// 延迟5秒取消
cts.CancelAfter(5000);
// 任务取消过后自动调用回调函数
cts.Token.Register(() =>
{
    print("任务取消了...");
});

8、Task类相对于Thread的优点

  • Task可以带返回值,而Thread没有返回值
  • Task类可以执行后续操作,如任务延续等等
  • Task可以更加方便的取消任务
  • Task具备线程池的优点,更加节约性能