多线程和异步的区别

3

注意:请在标记为重复之前阅读完整篇文章。我已经阅读了其他答案,它们似乎没有回答我的问题。

我看过各种图片和人们指出并说多线程与异步编程不同,通过给出各种类比,如餐厅工人等。但我还没有看到实际示例的区别。

我在C#中尝试了这个:

using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;

namespace AsyncTest
{
    class Program
    {
        static void RunSeconds(double seconds)
        {
            int ms = (int)(seconds * 1000);

            Stopwatch stopwatch = new Stopwatch();
            stopwatch.Start();
            Console.WriteLine($"Thread started to run for {seconds} seconds");
            Thread.Sleep(ms);
            stopwatch.Stop();

            Console.WriteLine($"Stopwatch passed {stopwatch.ElapsedMilliseconds} ms.");
        }

        static async Task RunSecondsAsync(double seconds)
        {
            int ms = (int)(seconds * 1000);

            Stopwatch stopwatch = new Stopwatch();
            stopwatch.Start();
            Console.WriteLine($"Thread started to run for {seconds} seconds");
            await Task.Run(() => Thread.Sleep(ms));
            stopwatch.Stop();

            Console.WriteLine($"Stopwatch passed {stopwatch.ElapsedMilliseconds} ms.");
        }

        static void RunSecondsThreaded(double seconds)
        {
            Thread th = new Thread(() => RunSeconds(seconds));
            th.Start();
        }

        static async Task Main()
        {
            Console.WriteLine("Synchronous:");
            RunSeconds(2.5); RunSeconds(2);

            Console.WriteLine("\nAsynchronous:");
            Task t1 = RunSecondsAsync(2.5); Task t2 = RunSecondsAsync(2);
            await t1; await t2;

            Console.WriteLine("\nMultithreading:");
            RunSecondsThreaded(2.5); RunSecondsThreaded(2);
        }
    }
}

结果:

Synchronous:
Thread started to run for 2.5 seconds
Stopwatch passed 2507 ms.
Thread started to run for 2 seconds
Stopwatch passed 2001 ms.

Asynchronous:
Thread started to run for 2.5 seconds
Thread started to run for 2 seconds
Stopwatch passed 2002 ms.
Stopwatch passed 2554 ms.

Multithreading:
Thread started to run for 2.5 seconds
Thread started to run for 2 seconds
Stopwatch passed 2000 ms.
Stopwatch passed 2501 ms.

他们在行为上本质上产生了相同的结果。因此,多线程程序与异步程序的行为有何不同,我会在何时发现这些差异?
我还有其他各种问题要解决:
例如,在这张图片中:

enter image description here

我不理解的是,当您运行异步程序时,它的行为几乎与多线程程序相同,因为它似乎花费了类似的时间。如上图所示,它正在“间歇性”地处理异步任务。如果是这样,那么异步任务完成的时间不应该更长吗?
比方说,运行一个原本需要3秒钟同步完成且锁定其他任务的异步任务,那么在从我的原始任务中休息并执行其他任务的同时,我不应该期望这些任务需要更长时间才能完成吗?
那么为什么通常异步情况下需要与同步情况下一样的时间(即通常的3秒)?而且为什么程序会变得“响应迅速”:如果任务没有在单独的线程上执行,为什么在同时处理其他任务时只需3秒钟就能完成任务?
我对在餐厅使用工人的示例(参见顶部答案)的问题是,在餐厅里,烹饪是由烤箱完成的。在计算机中,这个比喻并没有太多意义,因为不清楚为什么烤箱没有被视为单独的“线程”,而是人/工人。
此外,多线程应用程序是否会使用更多内存?如果确实如此,是否可能创建一个简单的应用程序(最好与上面的应用程序尽可能相似),以证明它确实会使用更多内存?
有点冗长的问题,但多线程和异步编程之间的区别远非清晰明了。

异步程序可以使用只有一个线程,但是异步多线程/并行程序需要有多个线程。 - Guru Stron
@maxspan 在任何给定的时刻,这些任务通常由不同的工人完成。如果不是工人在做,那么就是像烤箱这样的机器。提出的问题是为什么烤箱不能被视为链接类比中的另一个“工人”或线程。 - Osama Kawish
2个回答

4

在异步代码中不能使用 Thread.Sleep,请使用

await Task.Delay(1000); 

异步代码使用线程池,每当程序等待某个IO完成时,线程将返回到池中继续执行其他任务。一旦IO完成,异步方法将在它生成线程池的行处恢复,继续执行。
当您直接操作线程时,会阻塞并使您的代码不再是异步的,还会使线程池饱和,因为可用线程数有限。
另外,在异步方法的生命周期内,不能保证每一行都在同一个线程上执行。通常在每个await关键字后,线程都可能会更改。
在异步方法中,您永远不希望触及Thread类。
通过以下方式进行操作:
await Task.Run(() => Thread.Sleep(ms));

你迫使TPL分配一个线程到池中去阻塞它,使其饿死。 通过这样做,
await Task.Run(async () => await Task.Delay(ms));

即使您多次启动,也将从池中运行一个或两个线程。

在同步代码上运行Task.Run(),大多用于不支持内部异步的遗留调用,TPL只会在一个池线程中包装同步调用。要获取异步代码的全部优势,您需要等待一个仅内部运行异步代码的调用。


我的 Thread.Sleep 代码运行得很完美,就像多线程代码一样,所以我不确定你说的代码不再是异步的意思是什么。(请注意结果:异步线程一个接一个地启动,但同步代码没有)。为了澄清你的解释,当需要时,异步代码变成多线程的吗?(这就是我根据“异步代码使用线程池...”所理解的)。 - Osama Kawish
是的,使用 Task.Run(sync code) 的唯一优点是可以自动运行一个池线程,这个线程可能已经在连续运行中创建。 - NIKER
真正的优势在于进行IO操作时,Thread.Sleep并不是IO。当您在同步线程中等待IO时,需要循环和自旋,并检查IO是否完成 - 阻塞线程以达到100%利用率。在异步代码中,IO任务被生成并且线程返回到池中,在等待IO完成时使用0%的CPU。 - NIKER

2

让我尝试将您的程序与现实世界的例子联系起来,然后解释它。

将您的程序视为IT办公室,您是其中的老板。老板意味着启动程序执行的主线程。控制台可以被视为您的日记。

程序执行开始:

static async Task Main()
{
    Process process = Process.GetCurrentProcess();
    Console.WriteLine("Synchronous:");

你从主门进入办公室,并在日记中记录“同步:”。

Synchronous:

调用方法 'RunSeconds()'
RunSeconds(2.5); RunSeconds(2);

假设 'RunSeconds()' 相当于您项目客户的一个呼叫,但没有人来接听。因此,您将接听两个呼叫。要记住的是,由于您只是一个人,所以您会依次接听呼叫,并且总共花费的时间接近4.5秒。同时,您收到了家里的电话,但由于忙于接听客户电话,无法接听。现在来看通话记录。您接到电话后进行记录。完成通话后记录通话所花费的时间。对于这两个呼叫,您都需要进行两次记录。

Thread started to run for 2.5 seconds
Stopwatch passed 2507 ms.
Thread started to run for 2 seconds
Stopwatch passed 2001 ms.

Console.WriteLine("\nAsynchronous:");

接着你需要将"Asynchronous:"记录在日志中

调用方法 'RunSecondsAsync()'

Task t1 = RunSecondsAsync(2.5); Task t2 = RunSecondsAsync(2);
await t1; await t2;

让我们假设 'RunSecondsAsync()' 再次等同于从您的项目客户端之一的调用,但是这次您有一个经理和一个拥有10个接待员的团队来接听电话。在这里,经理相当于任务(Task),每个接待员都是一个线程,共同组成线程池。请记住,经理本身不会接任何电话,他只是将电话委派给接待员并管理他们。
当第一个呼叫 'RunSecondsAsync(2.5)' 到达时,经理立即将其分配给一个接待员,并通过任务对象返回通知您已处理该呼叫。接着您会立即收到第二个呼叫 'RunSecondsAsync(2)',经理立即将其分配给另一个接待员,并且两个呼叫同时被处理。
然而,您想要记录通话所花费的时间,因此您使用 await 关键字等待这些呼叫完成。这次等待的关键区别是,您仍然可以自由地做任何您想做的事情,因为电话是由接待员接听的。因此,如果您这次接到家中的电话,您将能够接听它(类比应用程序响应)。
一旦呼叫完成,经理会告诉您呼叫已完成,然后您就可以记录在日志中了。现在来谈谈呼叫的日志记录,首先记录接收到的两个呼叫,然后一旦它们完成,记录每个呼叫所花费的总时间。在本例中,您花费的总时间接近2.5秒,这是两个呼叫中最长的时间,因为呼叫是并行处理的,并且与经理之间有一些通信开销。
Thread started to run for 2.5 seconds
Thread started to run for 2 seconds
Stopwatch passed 2002 ms.
Stopwatch passed 2554 ms.

Console.WriteLine("\Multithreading:");

然后你将"多线程:"记录在日记中

调用方法'RunSecondsThreaded()'

RunSecondsThreaded(2.5); RunSecondsThreaded(2);

最后,你和经理吵了一架,并且他离开了这家组织。然而,你不想接电话,因为你有其他重要的任务要处理。所以当电话打来时,你雇了一个接线员来帮助你处理工作。这样做了两次,因为有两个电话打来了。与此同时,你再次有空去完成其他任务,比如如果你接到家里的电话,你可以接听它。 现在说到通话记录。这一次你不需要把通话记录在日记里,接线员替你记录。你所做的工作仅仅是雇佣接线员。由于电话几乎同时打进来,总共花费的时间是2.5秒加上雇佣的额外时间。
Thread started to run for 2.5 seconds
Thread started to run for 2 seconds
Stopwatch passed 2000 ms.
Stopwatch passed 2501 ms.

希望这能帮助您解决困惑。

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