关于c#:任务与线程差异

Task vs Thread differences

本问题已经有最佳答案,请猛点这里访问。

我不熟悉并行编程。在.NET中有两个类:TaskThread

所以,我的问题是:

  • 这些课程有什么区别?
  • 何时使用Thread更好?何时使用Task


Thread是一个较低级的概念:如果您直接启动一个线程,那么您知道它将是一个单独的线程,而不是在线程池中执行。

不过,Task不仅仅是"在哪里运行某些代码"的抽象概念,它实际上只是"对未来结果的承诺"。因此,作为一些不同的例子:

  • Task.Delay不需要任何实际的CPU时间;这就像设置一个计时器在将来关闭一样。
  • WebClient.DownloadStringTaskAsync返回的任务在本地不会占用太多的CPU时间;它代表的结果很可能将大部分时间花在网络延迟或远程工作上(在Web服务器上)。
  • Task.Run()返回的一个任务实际上是说"我希望您单独执行此代码";该代码执行所依赖的确切线程取决于许多因素。

注意,Task抽象对于C_5中的异步支持至关重要。

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


来源

螺纹

线程表示一个实际的操作系统级线程,具有自己的堆栈和内核资源。(从技术上讲,clr实现可以使用fibers,但没有现有的clr可以这样做)线程允许最高程度的控制;可以中止()或挂起()或恢复()线程(尽管这是一个非常糟糕的主意),可以观察其状态,并且可以设置线程级属性,如堆栈大小、单元状态或区域性。

线程的问题是操作系统线程的开销很高。您拥有的每个线程都会为其堆栈消耗大量的内存,并随着线程之间的处理器上下文切换增加额外的CPU开销。相反,当工作可用时,最好让一个小的线程池执行代码。

有时没有备用线程。如果需要指定名称(用于调试)或单元状态(用于显示UI),则必须创建自己的线程(请注意,拥有多个UI线程通常是一个坏主意)。此外,如果要维护一个由单个线程拥有且只能由该线程使用的对象,则更容易显式地为其创建线程实例,以便轻松检查尝试使用该对象的代码是否在正确的线程上运行。

线程池

threadpool是由clr维护的线程池的包装器。threadpool根本不给您任何控制权;您可以在某个时刻提交要执行的工作,并且可以控制池的大小,但不能设置任何其他内容。您甚至无法知道池何时开始运行您提交给它的工作。

使用threadpool可以避免创建过多线程的开销。但是,如果向线程池提交的长时间运行的任务太多,则线程池可能会满,提交的后续工作可能会等待较早的长时间运行项目完成。此外,threadpool不提供查找工作项何时完成的方法(与thread.join()不同),也不提供获取结果的方法。因此,threadpool最好用于调用方不需要结果的短操作。

任务

最后,来自任务并行库的任务类提供了两个世界中最好的。与threadpool一样,任务不会创建自己的OS线程。相反,任务由taskscheduler执行;默认的scheduler只在threadpool上运行。

与threadpool不同,task还允许您查明它何时完成,并(通过一般任务)返回结果。可以对现有任务调用ContinueWith(),使其在任务完成后运行更多代码(如果任务已经完成,则会立即运行回调)。如果任务是通用的,continueWith()将向您传递任务的结果,从而允许您运行更多使用它的代码。

您还可以通过调用wait()(或者,对于一般任务,通过获取result属性)同步等待任务完成。与thread.join()类似,这将阻塞调用线程,直到任务完成。同步等待任务通常是个坏主意;它会阻止调用线程执行任何其他工作,并且如果任务最终等待(甚至异步)当前线程,也会导致死锁。

由于任务仍在线程池上运行,因此不应将它们用于长时间运行的操作,因为它们仍然可以填满线程池并阻止新的工作。相反,task提供了一个longrunning选项,它将告诉taskscheduler启动一个新线程,而不是在threadpool上运行。

所有新的高级并发API,包括parallel.for*()方法、plinq、c_5 await和bcl中的现代异步方法,都是基于任务构建的。

结论

归根结底,任务几乎总是最好的选择;它提供了更强大的API,并避免浪费OS线程。

在现代代码中显式地创建自己的线程的唯一原因是设置每个线程选项,或者维护需要维护自己身份的持久线程。


通常你会听到任务是一个比线程更高层次的概念…这句话的意思是:

  • 不能使用abort/threadabortedException,您应该支持在您的"业务代码"中取消事件,定期测试token.IsCancellationRequested标志(也要避免长时间或超时连接,例如到db,否则您将无法测试此标志)。由于类似的原因,Thread.Sleep(delay)呼叫应替换为Task.Delay(delay, token)呼叫。

  • 没有线程的SuspendResume方法的任务功能。任务的实例也不能重用。

  • 但是你有两个新的工具:

    a)延续

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 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)嵌套/子任务

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    //StartNew - starts task immediately, parent ends whith child
    var parent = Task.Factory.StartNew
    (() => {
              var child = Task.Factory.StartNew(() =>
             {
             //...
             });
          },  
          TaskCreationOptions.AttachedToParent
    );
  • 所以系统线程完全隐藏在任务中,但任务的代码仍然在具体的系统线程中执行。系统线程是任务的资源,当然,在任务并行执行的框架下仍然存在线程池。线程如何执行新任务可能有不同的策略。另一个共享资源任务调度程序关心它。任务调度器解决的一些问题1)更喜欢在同一线程中执行任务及其二次执行,从而最大限度地降低切换成本(即内联执行)2)更喜欢按启动顺序执行任务(即首选公平性)3)更有效地在非活动线程之间分配任务,这取决于"任务活动的先验知识"—a你在偷东西。重要提示:一般来说,"异步"与"并行"不同。使用TaskScheduler选项,可以在一个线程中同步执行异步任务。为了表示并行代码执行,可以使用更高的抽象(而不是任务):Parallel.ForEachPLINQDataflow

  • 任务与c异步/等待功能(也称为Promise Model)集成,例如在requestButton.Clicked += async (o, e) => ProcessResponce(await client.RequestAsync(e.ResourceName));中,执行client.RequestAsync不会阻塞UI线程。重要提示:在引擎盖下,Clicked委托调用是绝对常规的(所有线程都由编译器完成)。

  • 这足以做出选择。如果您需要支持取消调用倾向于挂起(例如超时连接)的旧API的功能,并且在这种情况下支持thread.abort(),或者如果您正在创建多线程后台计算并希望使用suspend/resume优化线程之间的切换,这意味着手动管理并行执行-请使用读。否则,转到任务,因为它们可以让您轻松地对任务组进行操作,集成到语言中,并使开发人员更有效率-任务并行库(TPL)。


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

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

    在旧的时代(即在tpl之前),使用Thread类是在后台或并行运行代码的标准方法之一(更好的选择通常是使用ThreadPool,但是这很麻烦,有几个缺点,尤其是创建一个全新的t的性能开销。hread在后台执行任务。

    如今,90%的时间里,使用任务和TPL是一个更好的解决方案,因为它提供了抽象,从而可以更有效地使用系统资源。我想在一些场景中,您希望对运行代码的线程进行显式控制,但是一般来说,如果您希望异步运行某个东西,那么您的第一个调用端口应该是TPL。