【C#】ThreadPool与Task

1. 线程池(ThreadPool)

  1. 为什么要使用线程池?
    主要原因是创建和销毁一个线程的代价是昂贵的,会消耗较多的系统资源;
  2. 线程池原理?
    每个CLR只有一个线程池,线程池线程不是在CLR初始化时自动创建的,而是向线程池派发(dispatch)异步操作时,如果线程池中没有线程,则会创建一个新新线程,不同的是,这个线程不会被销毁,执行完后进入空闲状态,等待响应新的异步请求。
    当然,如果一个线程池线程不做任何事情,也是一种资源浪费。所以,当一个线程空闲特定一段时间后,会自己醒来终止自己以释放资源。
    请注意线程池中的线程都是后台线程,在所有的前台线程运行结束后,所有的后台线程将停止工作。
    创建一个线程时,会将当前线程的上下文传递给新建线程,而收集和复制上下文信息会耗费一定的时间和性能,在不需要传递上下文的场景中,可以通过System.Threading.ExecutionContext类型中的SuppressFlow()方法和RestoreFlow()方法分别阻止和恢复上下文的传递或流动。
  3. 使用线程池时的注意项?
    1. 线程池不适合需要长时间运行的作业,或者处理需要与其它线程同步的作业;
    2. 避免在线程池中执行I/O首先的操作,这种任务应该使用TPL模型;
    3. 不要手动设置线程池的最小和最大线程数,CLR会自动执行线程池的扩张和收缩,手动干预会使性能更差(目前默认是1000个线程);
  4. 线程池的两种使用方式:
    1. 通过异步编程模型(Asynchronous Programming Model,简称APM)展示怎样在线程池中异步的执行委托
      下面的方式为异步编程模型(这是.net历史中第一个异步编程模式),这里使用委托的BeginInvoke()方法来来运行该委托,BeginInvoke接收一个回调函数,会在任务执行完后被调用;现在这种APM编程模型使用的越来越少了,更多的是使用任务并行库(Task Parallel Library, 简称TPL)。
	private delegate string RunOnThreadPool(out int threadId);//委托
	static void Main(string[] args)
	{
	    //使用APM方式 进行异步调用  异步调用会使用线程池中的线程
	    IAsyncResult r = poolDelegate.BeginInvoke(out threadId, Callback, "委托异步调用");
	    r.AsyncWaitHandle.WaitOne();
	    // 获取异步调用结果
	    string result = poolDelegate.EndInvoke(out threadId, r);
	}
  1. 通过ThreadPool.QueueUserWorkItem()向线程池中放入异步操作?
	static void Main(string[] args)
	{
	    //直接将方法传递给线程池,AsyncOperation为要异步执行的方法
	    ThreadPool.QueueUserWorkItem(AsyncOperation);
	
	    //直接将方法传递给线程池 并且通过state传递参数
	    ThreadPool.QueueUserWorkItem(AsyncOperation, "async state");
	
	    //使用Lambda表达式将任务传递给线程池 并且通过 state传递参数
	    ThreadPool.QueueUserWorkItem(state =>
	    {
	        WriteLine($"Operation state: {state}");
	        WriteLine($"工作线程 id: {CurrentThread.ManagedThreadId}");
	    }, "lambda state");
	}
	
	private static void AsyncOperation(object state)
	{
	    WriteLine($"Operation state: {state ?? "(null)"}");
	    WriteLine($"工作线程 id: {CurrentThread.ManagedThreadId}");
	}
  1. 使用普通创建线程方式和线程池方式有何区别?
    分别运行下面两个方法,其中普通线程执行了2s多,但是创建了500个线程,线程池执行了9s多,但是只创建了很少的线程,为操作系统节省了线程和内存空间,但是花费的时间较多;
	static void UseThreads()
	{
	    for (int i = 0; i < 500; i++)
	    {
	        var thread = new Thread(() =>
	        {
	            Sleep(TimeSpan.FromSeconds(0.1));
	        });
	        thread.Start();
	    }
	}
	
	static void UseThreadPool()
	{
		 for (int i = 0; i < 500; i++)
		 {
		     ThreadPool.QueueUserWorkItem(c =>
		     {
		         Sleep(TimeSpan.FromSeconds(0.1));
		     });
		 }
	}
  1. 如何通过CancellationTokenSource取消线程:
	private CancellationTokenSource cts = new CancellationTokenSource();
	private void StartThread(object sender, RoutedEventArgs e)
	{
	    ThreadPool.QueueUserWorkItem(o => Count(cts.Token, 100));
	}
	
	private void Count(CancellationToken token, Int32 countTo)
	{
	    for (Int32 count = 0; count < countTo; count++)
	    {
	        if (token.IsCancellationRequested)
	        {
	            this.Dispatcher.Invoke(() =>
	            {
	                this.AutoAddTextBox.Text += '\n' + "Count is canceled" + '\n';
	            });
	            break;
	        }
	        this.Dispatcher.Invoke(() =>
	        {
	            this.AutoAddTextBox.Text += count.ToString() + " ";
	        });
	        Thread.Sleep(100);
	    }
	    this.Dispatcher.Invoke(() =>
	    {
	        this.AutoAddTextBox.Text += "Count is Done!" + '\n';
	    });
	}
	
	private void CancelThread(object sender, RoutedEventArgs e)
	{
		//执行完Cancel()方法后,会将IsCancellationRequested置为true
	    cts.Cancel();
	}


当运行到第32次时取消线程。

  1. BackgroundWorker组件介绍
    BackgroundWorker是基于事件的异步编程模式( Event-based Asynchronous Pattern,简称EAP),是.net历史上第二种用来构造异步程序的方式,现在推荐使用TPL;该组件被应用于WPF中,通过它实现的代码可以直接与UI控制器交互;

2、任务(Task)

  1. 什么是任务并行库(TPL)
    为了实现线程的同步、异步、异常传递等问题,需要编写较多的代码,来达到正确性和健壮性,而且最大的问题是没有内建的机制让你知道操作在什么时候完成,也没有机制在操作完成时获取返回值。为此,.NET 4.0引入了一个关于异步操作的API——任务并行库(TPL)。TPL的内部使用了线程池,而且效率更高。在把线程归还到线程池之前,它会在同一个线程中顺序执行多个任务,减少任务上下文切换带来的时间浪费问题。其中,任务(Task)是对象,封装了要异步执行的操作。
    TPL被认为是线程池之上的又一抽象层,其对程序员隐藏了与线程池交互的底层代码,并提供了更方便的细粒度API,TPL的核心概念是任务Task。
  2. Task创建的是线程池任务,Thread默认创建的是前台线程;
  3. 线程池一般只运行执行时间较短的异步操作;
  4. 新建Task的方法有3种,示例如下:
    其中Task2中使用的是Run()静态方法,Task4中设置了LongRunning,表明需要长时间运行,因此不是线程池线程;注意,Task.Run方法只是Task.Factory.StartNew的一个快捷方式,但是后者有附加的选项;
   public Main()
   {
       var task1 = new Task(() => TaskMethod("Task 1"));
       task1.Start();
       Task.Run(() => TaskMethod("Task 2"));
       Task.Factory.StartNew(() => TaskMethod("Task 3"));
       Task.Factory.StartNew(() => TaskMethod("Task 4"), TaskCreationOptions.LongRunning);
   }
   
   private void TaskMethod(string name)
   {
       string str = string.Format("{0} 线程id:{1},线程池中线程:{2}",
           name, Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread);
       this.Dispatcher.BeginInvoke(new Action(delegate
       {
           this.textbox.Text += str + "\n";
       }));
   }

输出结果:

	Task 1 线程id:10,线程池中线程:True
	Task 2 线程id:12,线程池中线程:True
	Task 3 线程id:13,线程池中线程:True
	Task 4 线程id:12,线程池中线程:False
  1. Task的基本操作:
    // 主线程直接执行操作
    TaskMethod("主线程任务");

    // 访问 Result属性,得到运行结果
    Task<int> task = CreateTask("Task 1");
    task.Start();
    task.Wait();//显式等待任务完成后,才执行后面的代码
    int result = task.Result;//task.Result是隐式等待任务完成后,才执行后面的代码,即在任务尚未完成时查询任务的Result
    //这里Wait和Result是等待单个任务完成,会阻塞当前线程,如果要等待一个Task对象数据,则采用WaitAll()或WaitAny()
    WriteLine($"运算结果: {result}");

    // 使用当前线程,同步执行任务
    task = CreateTask("Task 2");
    task.RunSynchronously();//这里是运行在主线程上,非线程池线程
    result = task.Result;
    WriteLine($"运算结果:{result}");

一个伸缩性好的程序不应该使线程阻塞,当调用Wait,查询任务的Result属性时,极有可能造成线程池创建新线程,增大的资源的消耗。ContinueWith方法可以在任务完成时执行另一个任务,避免线程的阻塞。

	private void StartThread(object sender, RoutedEventArgs e)
	{
	    Task<int> t = new Task<int>(n => Sum((int)n), 1000);
	    t.Start();
	    Console.WriteLine("before result out");
	    Task cwt = t.ContinueWith(task => Console.WriteLine("result is : " + t.Result));
	    //ContinueWith()方法返回一个Task对象,但该对象一般不用,可直接忽略掉
	    Console.WriteLine("after result out");
	}
	
	private int Sum(int countTo)
	{
	    int sum = 0;
	    for (int i = 0; i < countTo; i++)
	    {
	        sum += i;
	    }
	    return sum;
	}
	
	输出结果:
	before result out
	after result out
	result is : 499500

任务可以启动子任务,且只有当各子任务全部执行结束,父任务才结束:

	Task<int[]> parent = new Task<int[]>(() =>
	{
	    var results = new int[3];
	    new Task(() => results[0] = Sum(500), TaskCreationOptions.AttachedToParent).Start();
	    new Task(() => results[1] = Sum(1000), TaskCreationOptions.AttachedToParent).Start();
	    new Task(() => results[2] = Sum(1500), TaskCreationOptions.AttachedToParent).Start();
	    return results;
	});
	//parent.ContinueWith(parentTask => Array.ForEach(parentTask.Result, Console.WriteLine));
	parent.ContinueWith(parentTask =>
	{
	    foreach (var item in parent.Result)
	    {
	        Console.WriteLine(item.ToString());
	    }
	});
	parent.Start();
	
	输出结果:
	124750
	499500
	1124250

Task相对于ThreadPool.QueueUserWorkItem具有很多附加属性,如任务状态,父任务的引用,TaskScheduler的引用,回调方法的引用等等,但会增加代价,因为需要为所有这些属性分配内存,所以在不需要这些附加功能时,采用ThreadPool.QueueUserWorkItem能获得更好的资源利用率。

  1. System.Threading的Timer类
    该类使一个线程池线线程定时调用一个方法。在内部,线程池为所有的Timer对象只使用了一个线程,即单独有一个线程控件所有Timer对象中回调方法的调用,当一个Timer对象到期时,该线程会在内部调用ThreadPool.QueueUserWorkItem,将回调任务添加到线程池队列中。当回调方法执行时间很长,而Timer间隔时间又很短时,会存在线程池多个线程同时执行一个回调方法
    所以要在一个线程池线程上执行定期性发生的后台任务时,采用Timer定时器。
    System.Windows.Forms的Timer类及System.Windows.Threading的Dispatcher类的功能相同,与上面定时器的区别是:回调方法只由一个线程完成,就是设置定时器的线程,即在一个线程中设置了DispatcherTimer,那么其回调方法也只在该线程中执行
  2. Task类中几个常用方法:
    public static bool WaitAll(Task[] tasks):判断tasks是否全部执行完毕;
    public static bool WaitAny(Task[] tasks):判断tasks中是否存在执行完毕的task;
    public static Task WhenAll(Task[] tasks):当所有tasks执行完毕后,创建并返回一个新的Task;
    public static Task WhenAll(Task[] tasks):当tasks中存在已执行完毕的task,创建并返回一个新的Task;
  3. TaskScheduler是一个非常重要的抽象,该组件实际上负责如何执行任务,默认的任务调试器将任务放置到线程池的工作线程中,这是TPL的默认选项;
  4. . C#5.0引入了新的语言特性,称为异步函数(asynchronous function),它是TPL之上更高级别的抽象,真正简化了异步编程,主要依靠async和await关键字实现;

参考:C#多线程编程系列
参考:C#多线程总结(纯干货)

  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

woniudaidai

你的鼓励将是我创作的最大动力!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值