任务与线程的区别

433

在.NET中有两个可用的类:TaskThread

  • 这些类之间有什么区别?
  • 何时更好地使用Thread而不是Task(反之亦然)?

50
阅读 此网页 - MoonKnight
24
除非你需要线程,否则请优先使用任务(Task)。线程需要资源(1MB栈(在.NET中承诺),线程内核对象等)。任务也可以作为单独的线程并行运行,但它是系统线程池线程,由系统优化考虑CPU核心等因素,并用于在系统中运行许多任务。除此之外,当任务完成时,可以返回一个对象,因此有一种方便的方式来知道并行执行的结果。 - Abhijit-K
@AbhijitKadam 当您说“system”时,您是指.NET框架吗? - Panzercrisis
15
这篇文章很有趣,@MoonKnight,但为了SO的一个问题去读一本关于线程的书有点过头了。 - Tsahi Asher
2
@TsahiAsher 这是一本书中的单独一章。 - MoonKnight
我简直不敢相信最受赞的答案已经被删除了(因为我的声望很高,所以我可以看到它)。而且没有办法让版主知道他们犯了一个错误。 - Alex from Jitbit
3个回答

535

Thread 是一个较低级概念:如果你直接启动一个线程,你知道它将是一个单独的线程,而不是在线程池等其他地方执行。

Task 不仅仅是“在哪里运行一些代码”的抽象 - 它实际上只是“未来结果的承诺”。因此,以下是一些不同的例子:

  • Task.Delay 不需要任何实际的 CPU 时间;它就像在未来设置一个定时器。
  • WebClient.DownloadStringTaskAsync 返回的任务在本地不会占用太多 CPU 时间;它代表一个结果,该结果可能会在网络延迟或远程工作(在 web 服务器上)中花费大部分时间。
  • Task.Run() 返回的任务确实在说“我想让你单独执行这段代码”;该代码执行的确切线程取决于多种因素。

请注意,Task<T> 抽象是 C# 5 中异步支持的关键。

总的来说,我建议您在尽可能的情况下使用更高级别的抽象:在现代的 C# 代码中,您很少需要显式地启动自己的线程。


3
即使您正在运行类似消息循环的进程,您仍然应该使用任务而不是线程? - Ignacio Soler Garcia
7
可能可以把它创建为“长时间运行”的形式,这样就会有一个专门的线程,但这意味着要在整个过程中使用单一的抽象。 - Jon Skeet
22
同时值得一提的是,在ASP.NET中, new Thread() 不处理线程池线程,而 Task 则使用线程池线程。 - Royi Namir
5
这并不是特定于ASP.NET的事情,如果你指定任务将会是一个长时间运行的任务,在某些情况下启动任务时可能会使用非线程池线程。 - Jon Skeet
2
@LeonidVasilyev:虽然这不是直接与任务相关的(在引入任务之前就发生了),但我曾经因为进行一些图像分析工作而使线程池饥饿。我使用线程池进行对象检测(实际上使用了太多线程),但是我使用的SDK也使用了线程池来推送结果。这导致我的程序锁定,因为所有线程都忙碌。 - Ed S.
显示剩余5条评论

55

通常听到的是任务比线程更高级...这句话的意思是:

  1. 你不能使用 Abort/ThreadAbortedException,在你的“业务代码”中应该周期性地支持cancel event,测试 token.IsCancellationRequested 标志(还要避免长时间或无超时的连接,例如到数据库,否则你将永远没有机会测试此标志)。由于类似的原因,Thread.Sleep(delay) 调用应该被替换为 Task.Delay(delay, token) 调用(传递 token 内部以便能够中断延迟)。

  2. 任务没有线程的 SuspendResume 方法功能。也不能重复使用任务实例。

  3. 但你有两个新工具:

    a) continuations (续体)

// continuation with ContinueWhenAll - execute the delegate, when ALL
// tasks[] had been finished; other option is ContinueWhenAny

Task.Factory.ContinueWhenAll( 
   tasks,
   () => {
       int answer = tasks[0].Result + tasks[1].Result;
       Console.WriteLine("The answer is {0}", answer);
   }
);
b) 嵌套/子任务

//StartNew - starts task immediately, parent ends whith child
var parent = Task.Factory.StartNew
(() => {
          var child = Task.Factory.StartNew(() =>
         {
         //...
         });
      },  
      TaskCreationOptions.AttachedToParent
);
  • 系统线程完全隐藏在任务之外,但任务的代码仍在具体的系统线程中执行。系统线程是任务的资源,当然,在任务的并行执行下,线程池仍然存在。可以有不同的策略来获取新的任务以执行。另一个共享资源TaskScheduler会关心这个问题。一些TaskScheduler解决的问题:1)优先在同一线程中执行任务及其继续,从而最小化切换成本--即内联执行 2)按启动顺序执行任务--也就是 PreferFairness 3)根据“任务活动的先前知识”更有效地分配任务到闲置线程之间--即Work Stealing 。重要提示:一般来说,“async”不等同于“parallel”。通过操作TaskScheduler选项,可以将异步任务设置为在一个线程中同步执行。为了表达并行代码执行,可以使用比Tasks更高级的抽象:Parallel.ForEachPLINQ Dataflow

  • 任务与C#的异步/等待特性即Promise模型集成在一起,例如requestButton.Clicked += async (o, e) => ProcessResponce(await client.RequestAsync(e.ResourceName));执行client.RequestAsync的时候将不会阻塞UI线程。重要提示:在幕后,Clicked委托调用是绝对常规的(所有线程都由编译器完成)。

  • 以上就足以做出选择了。如果需要支持调用传统API的取消功能(例如没有超时连接),并且为此目的支持Thread.Abort(),或者如果您正在创建多线程后台计算并希望通过Suspend/Resume优化线程之间的切换,则意味着手动管理并行执行--请使用Thread。否则,请使用Tasks,因为它们将使您轻松操作它们的组,已经集成到语言中,并使开发人员更具生产力-任务并行库(TPL)


    3
    注意 - Thread.Abort 对于大多数长时间操作,如数据库连接等,并不起作用。它只能在托管代码中进行中止,而大多数长时间等待的操作都被卡在本地代码中(例如等待句柄、I/O 操作等)。唯一的好处是,它可以相对安全地在托管代码中的任何位置中止(例如不在 finally 子句中等),因此它可以防止出现无限循环等错误。但在生产级别的代码中使用它并没有太多意义。 - Luaan
    3
    Thread.Abort 是有害的,应尽可能避免使用。在可能发生线程中止的情况下运行的代码极难正确推理。这不值得,只需检查某种标志即可。(我建议使用 CancellationToken API,即使您没有使用 Tasks)。 - Joren
    这是一个错误的做法,应该用Task.Delay(delay, token);替换Thread.Sleep(delay)。如果需要,则应该用Task.Delay(delay, token).Wait();替换,它与Thread.Sleep(delay)相同,或者如果可能的话,用await Task.Delay(delay, token)替换。 - Rekshino

    43

    Thread类用于在Windows中创建和操作线程

    Task表示某些异步操作的一部分,并且是任务并行库(TPL)的一部分,这是一组API,可用于异步运行和并行运行任务。

    早期(即TPL之前),使用Thread类是在后台或并行运行代码的标准方式之一(更好的替代方案通常是使用ThreadPool),但这很麻烦,并且有几个缺点,其中最大的是创建一个全新线程来执行后台任务的性能开销。

    现在使用任务和TPL通常是更好的解决方案,因为它提供了抽象,使得系统资源的使用更加高效。我想你可能会遇到一些情况,需要显式控制正在运行代码的线程,但一般来说,如果想要异步运行某些内容,应首先考虑TPL。


    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接