使用Tasks (TPL)库是否使应用程序成为多线程的?

本文关键字:应用程序 多线程 是否 Tasks TPL 使用 | 更新日期: 2023-09-27 18:05:03

最近在一次面试中,我被问到这个问题。

问:你写过多线程应用程序吗?

:是的

问:介意解释更多吗?

A:我使用Tasks (Task Parallel library)来执行waiting for some info from internet while loading UI等任务。这提高了应用程序的可用性。

问:但是,只要你用了TPL就意味着你写了一个multithreaded应用程序?

我:(不知道该说什么)

那么,到底什么是多线程应用程序呢?它与使用Tasks有什么不同吗?

使用Tasks (TPL)库是否使应用程序成为多线程的?

任务可以用来表示在多个线程中发生的操作,但是它们不这样做。一个可以编写只在单个线程中执行的复杂TPL应用程序。例如,当您有一个任务表示对某些数据的网络请求时,该任务是而不是将创建额外的线程来完成该目标。这样的程序(希望)是异步的,但不一定是多读的。

并行是指同时做多件事。这可能是也可能不是多个线程的结果。

让我们打个比方。


Bob是怎么做晚饭的:

  1. 他装满一壶水,然后把它煮沸。
  2. 然后他把意大利面放进水里。
  3. 意大利面煮好后,他把水沥干。
  4. 他在准备他的酱料。
  5. 他把做酱汁的所有原料都放在平底锅里。
  6. 他在煮他的酱。
  7. 他把酱汁放在意大利面上。
  8. 他吃晚饭了。

Bob在烹饪晚餐时完全是同步的,没有多线程、异步或并行。


Jane是这样做晚餐的:

  1. 她装满一壶水,开始煮。
  2. 她在准备她的酱料。
  3. 她把意大利面放进沸水里。
  4. 她把食材放进平底锅。
  5. 她喝光了她的面食。
  6. 她把酱汁放在意大利面上。
  7. 她吃她的晚餐。

Jane利用异步烹饪(没有任何多线程)在烹饪晚餐时实现并行。


Servy是这样做晚餐的:

  1. 他告诉Bob烧一壶水,准备好了就把意大利面放进去,然后端上意大利面。
  2. 他告诉Jane准备做酱汁的原料,煮熟,然后煮好后放在意大利面上。
  3. 他等待Bob和Jane完成。
  4. 他吃晚饭了。

Servy利用了多个线程(worker),每个线程都同步地完成自己的工作,但它们彼此异步地工作,以实现并行性。

当然,如果我们考虑,例如,我们的炉子是有两个燃烧器还是只有一个燃烧器,这就变得更有趣了。如果我们的炉子有两个燃烧器,那么我们的两条线,鲍勃和简,都能在不互相妨碍的情况下完成自己的工作。他们可能会撞一下肩膀,或者每个人时不时地试图从同一个柜子里抓东西,所以他们每个人都会减慢一个,但不会太多。如果他们每个人都需要共用一个炉子,那么当另一个人在工作时,他们实际上根本无法完成很多工作。在这种情况下,工作实际上不会比让一个人完全同步烹饪更快,就像鲍勃独自一人时所做的那样。在这种情况下,我们正在处理多个线程,,但我们的处理不是并行的并非所有多线程工作实际上都是并行工作。当您在一台具有一个CPU的机器上运行多个线程时,就会发生这种情况。实际上,你并没有比使用一个线程更快地完成工作,因为每个线程只是轮流做功。(这并不意味着多线程程序在单核cpu上是毫无意义的,它们不是,只是使用它们的原因不是为了提高速度。)


我们甚至可以考虑这些厨师如何使用任务并行库来完成他们的工作,以查看TPL的哪些用途对应于每种类型的厨师:

首先我们有bob,只是写普通的非tpl代码并同步执行所有操作:

public class Bob : ICook
{
    public IMeal Cook()
    {
        Pasta pasta = PastaCookingOperations.MakePasta();
        Sauce sauce = PastaCookingOperations.MakeSauce();
        return PastaCookingOperations.Combine(pasta, sauce);
    }
}

然后我们有Jane,她启动了两个不同的异步操作,然后在启动每个操作后等待这两个操作来计算她的结果。

public class Jane : ICook
{
    public IMeal Cook()
    {
        Task<Pasta> pastaTask = PastaCookingOperations.MakePastaAsync();
        Task<Sauce> sauceTask = PastaCookingOperations.MakeSauceAsync();
        return PastaCookingOperations.Combine(pastaTask.Result, sauceTask.Result);
    }
}

在这里提醒一下,Jane正在使用TPL,并且她正在并行地做她的大部分工作,但是她只使用一个线程来完成她的工作。

然后我们有Servy,它使用Task.Run创建一个任务,表示在另一个线程中执行工作。他启动两个不同的工人,让他们每个人同步做一些工作,然后等待两个工人完成。

public class Servy : ICook
{
    public IMeal Cook()
    {
        var bobsWork = Task.Run(() => PastaCookingOperations.MakePasta());
        var janesWork = Task.Run(() => PastaCookingOperations.MakeSauce());
        return PastaCookingOperations.Combine(bobsWork.Result, janesWork.Result);
    }
}

Task是对未来工作完成的承诺。当使用它时,您可以将其用于I/O based工作,其中不要求使用多个线程来执行代码。一个很好的例子是使用c# 5的async/awaitHttpClient的特性来完成基于网络的I/O工作。

但是,您可以利用TPL来执行多线程工作。例如,当使用Task.RunTask.Factory.Startnew启动一个新任务时,幕后的工作在ThreadPool上为你排队,TPL为我们抽象出来,允许你使用多个线程。

使用多线程的常见场景是当您有CPU限制的工作可以同时(并行)完成时。使用多线程应用程序会带来很大的责任。

所以我们看到使用TPL并不一定意味着使用多个线程,但你绝对可以利用它来做多线程。

问:但是,只要你使用过TPL就意味着你写了一个多线程应用程序?

聪明的问题,任务!=多线程,使用TaskCompletionSource你可以创建一个Task,它可以在单线程中执行(可能是UI线程)。

Task只是对一个可能在未来完成的操作的抽象。这并不意味着代码是多线程的。通常Task涉及多线程,但不一定总是这样。

记住,只知道TPL,你不能说你知道多线程。有很多概念你需要涵盖。

    线程
  • 同步原语
  • 线程安全
  • 异步编程
  • 并行编程是TPL的一部分。
  • 和更多…

当然还有Task并行库。

注意:这不是完整的列表,这些只是我的想法。

人力资源>

<学习资源:

  • Eric的旧博客
  • Eric的新博客
  • Stephen toub的博客

对于单独线程,我建议http://www.albahari.com/threading/

对于视频教程,我建议使用Pluralsight。这是值得的,但值得的。

最后但并非最不重要:是的,当然,它是Stackoverflow。

我的5美分:您不必显式地使用Task.RunTask.Factory.StartNew来使您的TPL应用程序多线程。想想看:

async static Task TestAsync()
{
    Func<Task> doAsync = async () =>
    {
        await Task.Delay(1).ConfigureAwait(false);
        Console.WriteLine(new { Thread.CurrentThread.ManagedThreadId });
    };
    var tasks = Enumerable.Range(0, 10).Select(i => doAsync());
    await Task.WhenAll(tasks);
}
// ...
TestAsync().Wait();

doAsync内部await之后的代码在不同线程上并发执行。以类似的方式,并发可以通过异步套接字API, HttpClient, Stream.ReadAsync或任何其他使用线程池(包括IOCP池线程)引入。

形象地说,每个。net应用程序都是多线程的,因为框架广泛地使用了ThreadPool。即使是一个简单的控制台应用程序也会显示多个System.Diagnostics.Process.GetCurrentProcess().Threads.Count线程。面试官应该问你是否写过并发(或并行)代码。