线程与线程池以及任务和协商式取消

一、线程

使用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");
}

image

设置t.IsBackground = false

image

有时我们想让执行的工作取消,可以使用协商式取消的方式,为什么叫协商式,是因为要双方都要遵守一个约定。我们使用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();
}

提前按下回车键

image

正常完成

image

原文地址:https://www.cnblogs.com/hkfyf/p/12093878.html