任务(System.Threading.Task)和线程(Thread)的区别

20

根据我对任务和线程之间的区别的理解,任务发生在线程池中,而线程则需要我自己管理。任务可以被取消并在完成其任务后返回到线程池中。

但是在某些博客中,我读到如果操作系统需要创建任务并创建线程,则创建(和销毁)任务将更容易。

有人可以解释一下为什么创建任务比线程简单吗?(或者我可能理解有误...)


18
“工作”和“工人”有什么区别?一个“工人”“做”一份“工作”,但“工人”本身不是“工作”。有些工作只需要一个工人完成,而有些工作则被分成多个小任务由多名工人协同完成。任务和线程也是如此;一个任务不是“某种类型的线程”,任务是一份工作,而线程是执行这份工作的工人。 - Eric Lippert
3
我听到的另一个类比是,处理器就像司机,线程就像卡车,任务就像要运送的货物。一个司机(处理器)一次只能操作一个卡车(线程),而一个卡车(线程)一次只能拉一种货物(任务)。你可以购买任意数量的卡车,但如果司机花费太多时间在不同卡车之间切换,他们就会少时间驾驶。货物可以堆放在仓库里等待运输,仓库可以根据适当的规则按优先顺序分配它们到卡车上。 - Chris Shain
5个回答

19
我认为您所说的Task是System.Threading.Task。如果是这样,您可以这样考虑:
- 一个程序可以有多个线程,但处理器核心一次只能运行一个线程。 - 线程非常昂贵,而在正在运行的线程之间切换也非常昂贵。 - 因此...拥有数千个线程执行任务是低效的。想象一下,如果你的老师让你做1万个任务。你会花费很多时间在它们之间循环,以至于永远无法完成任何事情。如果启动太多线程,CPU也可能发生同样的情况。
为了解决这个问题,.NET框架允许您创建任务。任务是一个封装在对象中的工作,它允许您做一些有趣的事情,比如捕获该工作的输出并链接工作的不同部分(先去商店,然后买杂志)。
任务被安排在线程池中。具体线程数取决于所使用的调度器,但默认调度器会尝试选择最适合你拥有的 CPU 核心数量和任务实际使用 CPU 时间的线程数。如果需要,你甚至可以编写自己的调度器,执行某些特定操作,例如确保该调度器的所有任务始终在单个线程上运行。

因此,请将任务视为待办事项清单中的项目。你可能能够同时完成5件事情,但如果你的老板给你10000件任务,它们就会堆积在收件箱里,直到你正在处理的前5件完成为止。任务与线程池之间的区别在于,任务(如我之前提到的)更好地控制了不同工作项之间的关系(想象一下多个指令钉在一起的待办事项),而线程池只允许你排队一堆单独的、单一阶段的工作项(函数)。


3
默认情况下,Task会在ThreadPool上进行调度,但并非必须如此。如果您使用TaskCompletionSource创建Task,甚至可能没有与之直接关联的任何代码。 - svick

13
您听到了两个不同的任务概念。第一个是工作的概念,第二个是进程的概念。 很久以前(在计算机术语中),没有线程。每个正在运行的程序实例都被称为进程,因为它只是一个接一个地执行步骤,直到退出。这与进程作为一系列步骤的直觉想法相匹配,例如工厂装配线的步骤。操作系统管理进程抽象。 然后,开发人员开始向工厂添加多条装配线。现在,程序可以同时做多件事情,其中一个库或(更常见的是)操作系统将管理每个线程内的步骤的调度。线程是一种轻量级进程,但线程属于进程,该进程中的所有线程共享内存。另一方面,多个进程不能干扰彼此的内存。因此,您的Web服务器中的多个线程可以各自访问有关连接的相同信息,但Word无法访问Excel的内存中数据结构,因为Word和Excel作为单独的进程运行。 将进程视为一系列步骤的想法实际上并不符合带有线程的进程模型,因此一些人开始将“以前称为进程的”抽象称为任务。这是您在博客文章中看到的第二个任务定义。请注意,仍有许多人使用“进程”一词来表示此事物。 那么,随着线程变得更加普遍,开发人员在其上添加了更多的抽象来使它们更易于使用。这导致线程池的崛起,它是一个由库管理的“池”线程。您将作业传递给库,库选择一个线程并在该线程上运行作业。.NET框架具有线程池实现,第一次听到“任务”时,文档确实是指要传递给线程池的作业。 因此,在某种意义上,文档和博客都是正确的。任务术语的重载是混淆的不幸源泉。

1
+1 这是一个很好的解释。我认为 OP 在谈论 System.Threading.Task。如果您能稍微扩展一下讨论,包括 TPL 的细微差别,那就太好了。 - Chris Shain

8

从v1.0开始,线程就是.Net的一部分,任务(Tasks)则是在发布于.Net 4.0的任务并行库(Task Parallel Library TPL)中引入的。

你可以将任务视为线程的更高级版本。它们非常易于使用,并具有以下优点:

  1. 你可以创建返回类型为任务的方法。
  2. 你可以使用“ContinueWith”方法,它将等待前一个任务完成后再开始执行。(抽象等待)
  3. 抽象锁,应遵循公司指南。
  4. 你可以使用Task.WaitAll并传递任务数组,以便等待所有任务完成。
  5. 你可以将任务附加到父任务上,因此可以决定父任务或子任务哪个先存在。
  6. 你可以通过LINQ查询实现数据并行性。
  7. 你可以创建并行for和foreach循环。
  8. 使用任务处理异常非常容易。
  9. *最重要的是,如果在单核计算机上运行相同的代码,它将只充当单个进程,没有线程的开销。

任务相对于线程的缺点:

  1. 需要使用.Net 4.0
  2. 学习过操作系统的新手更容易理解线程。
  3. 对框架不熟悉,无法获得太多帮助。

一些提示:

始终使用Task.Factory.StartNew方法,这是语义上完美且标准的。

查看任务并行库以获取更多信息 http://msdn.microsoft.com/en-us/library/dd460717.aspx


3

延伸Eric Lippert的评论:

Thread是一种允许应用程序并行执行多个任务的方式,例如,应用程序可能拥有一个处理用户事件(如按钮点击)的线程,以及另一个执行某些长时间计算的线程。这样,您就可以“同时”做两件不同的事情。如果没有这样做,用户就必须等到计算完成才能点击按钮。因此,Thread是可以执行您编写的代码的东西。

Task代表了一项抽象的工作,该工作可以有一个结果,并且您可以等待工作结束(通过调用Wait())或在工作结束后执行其他操作(通过调用ContinueWith())。

最常见的工作是并行执行某些计算,而Task提供了一种简单的方法来实现。代码实际运行的时间和方式由TaskScheduler定义。默认的调度器使用ThreadPool:一组可以运行任何代码的线程。这样做是因为创建和切换线程是低效的。

但是Task不一定要直接与某些代码相关联。您可以使用TaskCompletionSource来创建Task,并在任何时候设置其结果。例如,您可以创建一个Task,并在用户单击按钮时标记它已完成。一些其他代码可以等待该Task,而在等待期间,该Task没有执行任何代码。

如果您想知道何时使用Task和何时使用Thread:使用Task比创建自己的Thread更简单、更高效。但有时,您需要比Task提供的控制更多。在这些情况下,使用Thread直接更有意义。


1

任务实际上只是手动启动线程的样板代码的包装器。从根本上讲,它们没有区别。任务使线程管理更加容易,并且由于减少了样板噪音,它们通常更具表现力。


Chris,请问你能解释一下任务调度吗? - Yanshof
1
我会添加一个答案,但我也建议您查看@AdamMihalcin的答案。 - Chris Shain
@ChrisShain 我同意任务是在线程上安排的,并且并没有试图暗示线程与任务相同,但是相同的开发目标得以实现。也许我的解释有点太笼统了... - Justin Pihony
@JustinPihony 是的,它只是让不了解情况的读者得出了错误的结论。我认为修改后更准确。+1 - Chris Shain

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