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具备线程池的优点,更加节约性能