线程与线程池以及任务和协商式取消
Posted hkfyf
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了线程与线程池以及任务和协商式取消相关的知识,希望对你有一定的参考价值。
一、线程
使用System.Threading命名空间下的Thread类即可创建专有线程
var t = new Thread(() => Console.WriteLine("new thread"));
构造函数有如下四个版本
Thread(ThreadStart start);
public Thread(ThreadStart start, int maxStackSize);
public Thread(ParameterizedThreadStart start);
public Thread(ParameterizedThreadStart start, int maxStackSize)
其中ThreadStart、ParameterizedThreadStart 是两个委托类型,表示将要执行的函数,一个能接受参数,一个不接受参数。原型如下:
public delegate void ParameterizedThreadStart(object obj);
public delegate void ThreadStart();
使用有参版本如下:
var t = new Thread(state => Console.WriteLine($"My name is {state}"));
t.Start("HK");调用Start如下的重载方法即可,内部会使用这个参数并调用相应的委托。
public void Start(object parameter)
一般来说,你将要执行的函数一般会将这个参数进行强制转换,以便你使用。如下所示
var t = new Thread(state =>
{
Point p = (state as Point) ?? throw new ArgumentException("无效的参数类型");//强制转型
Console.WriteLine($"The sum is {p.X + p.Y}");
});t.Start(new Point(1,2));
用到的类如下:
public class Point
{
public int X { get; private set; }
public int Y { get; private set; }
public Point(int x, int y)
{
this.X = x;
this.Y = y;
}
}函数体执行完之后,该线程就会被释放,但是该创建该线程的实例对象的内存不会释放,直到GC回收该内存。
使用该类创建线程有明显的一个缺点,不能使用有返回值的委托。并且线程不能重用,一经创建,执行完函数体之后,便被销毁了,由于创建线程是有资源消耗的,对于客户端程序可能不明显,但对于高性能的服务端程序,频繁的创建线程会浪费许多系统资源。所以要用下面要介绍的线程池。
二、线程池
顾名思义,就是存储线程的一个池子,当有任务进来的时候,线程池分配一个线程去执行该任务,当该任务执行完之后,线程不会不销毁,而是由线程池回收,待有新任务到来时,再去分配线程执行新任务。线程池中的线程只有第一次创建时才会损耗性能,创建完成之后,便不会了。
同样是在System.Threading命名空间下,使用ThreadPool静态类如下的静态方法即可
public static bool QueueUserWorkItem(WaitCallback callBack, object state);
public static bool QueueUserWorkItem(WaitCallback callBack);
其中WaitCallback 也是一个委托类型,与ParameterizedThreadStart委托类型是一样的
public delegate void WaitCallback(object state);
public delegate void ParameterizedThreadStart(object obj);
注意与Thread类不同,使用QueueUserWorkItem方法之后,该任务会自动加入待执行列表,而不用像Thread类一样显示调用Start方法。注意不管是QueueUserWorkItem还是Start,执行之后函数体(任务)并不一定会马上执行,而是等待调度,得到执行权之后才会执行。在系统资源不紧张的情况下,通常会立即执行。注意使用线程池创建的线程默认是后台线程,使用Thread类创建的实例,默认是前台线程,在Thread类的实例对象下,可以设置IsBackground属性为false从而把线程设置为后台线程。还可以通过Thread类的静态对象Thread.CurrentThread来获得当前线程实例对象,从而设置线程的前后台属性。前台线程和后台线程的区别见第三点。
使用如下:
ThreadPool.QueueUserWorkItem((state) =>
{
Console.WriteLine("new Task");
});这会造成工作项进入一个待执行队列,然后线程池会分配线程去执行这个工作项,线程池刚开始是空的,一但有工作项进来,就会创建线程去执行这个工作项,工作项完成之后,线程会由线程池回收复用,继续去执行队列中的工作项。当线程池中空闲的线程为0时,且待执行队列还有工作项的时候,线程池会创建新的线程(如果系统资源足够的话,否则只能等待有空闲的线程去执行队列中的工作项)去执行这个工作项。
然而线程返回值还是不能直接得到,要想直接得到返回值,可以通过System.Threading.Tasks命名空间下的Task<T>类。
三、前台线程和后台线程以及协商式取消
在CLR中,线程要么是前台线程,要么是后台线程,每个应用程序至少有一个前台线程,所有前台线程停止运行后,CLR将强制终止仍在运行的后台线程。
如下所示:
static void Main(string[] args)
{
var t = new Thread(() =>
{
Thread.Sleep(1000);
Console.WriteLine("Background Thread");
});
t.IsBackground = true;//设置为后台线程
t.Start();
Console.WriteLine("Main thread over");
}设置t.IsBackground = false
有时我们想让执行的工作取消,可以使用协商式取消的方式,为什么叫协商式,是因为要双方都要遵守一个约定。我们使用System.Threading命名空间下的CancellationTokenSource类。然后我们可以通过该类实例下的Token属性获得一个值类型实例,这个值类型是CancellationToken,同样是在System.Threading命名空间下。双方使用这个实例便可实现协作式取消操作。如下所示:
private static void OP(int n,CancellationToken token)
{
Thread.CurrentThread.IsBackground = false;
int sum = 0;
for (int i = 0; i < n; i++)
{
if(token.IsCancellationRequested)//如果是通过Task执行的,这里应该抛出一个异常(token.ThrowIfCancellationRequested),因为任务可以有返回值,如果像这里一样处理,则无法知道该任务是正常结束了还是取消了。
{ //另外注意,通过Thread类的实例或者是使用ThreadPool创建的线程是无法在调用线程直接捕获异常然后进行处理的,所以这里不可以选择抛出异常,一旦抛出异常则应用程序就会终止。
Console.WriteLine("work is not over");
Console.WriteLine(sum.ToString());
return;
}
sum += i;
Thread.Sleep(100);
}
Console.WriteLine("work is over");
Console.WriteLine(sum.ToString());
}
static void Main(string[] args)
{
var cts = new CancellationTokenSource();
ThreadPool.QueueUserWorkItem((statue) => OP(10,cts.Token));
Console.ReadLine();
cts.Cancel();
}
提前按下回车键
正常完成
以上是关于线程与线程池以及任务和协商式取消的主要内容,如果未能解决你的问题,请参考以下文章
#yyds干货盘点# JUC线程池: FutureTask详解