C#“异步和等待”功能和线程

You*_*ark 5 c# multithreading asynchronous

请查看以下代码。

            class AddParams
            {
                public int a, b;

                public AddParams(int numb1, int numb2)
                {
                    a = numb1;
                    b = numb2;
                }
            }

            class Program
            {
                static void Main(string[] args)

                {
                    Console.WriteLine("ID of thread in 1: {0}",
                      Thread.CurrentThread.ManagedThreadId);

                    AddAsync();
                    Console.ReadLine();
                }

                private static async Task AddAsync()
                {
                    Console.WriteLine("***** Adding with Thread objects *****");
                    Console.WriteLine("ID of thread in Main(): {0}",
                      Thread.CurrentThread.ManagedThreadId);

                    AddParams ap = new AddParams(10, 10);
                    await Sum(ap);

                    Console.WriteLine("Other thread is done!");
                }

                static async Task Sum(object data)
                {
                    await Task.Run(() =>
                    {
                        if (data is AddParams)
                        {
                            Console.WriteLine("ID of thread in Add(): {0}",
                              Thread.CurrentThread.ManagedThreadId);

                            AddParams ap = (AddParams)data;
                            Console.WriteLine("{0} + {1} is {2}",
                              ap.a, ap.b, ap.a + ap.b);
                        }
                    });
                }
            }
Run Code Online (Sandbox Code Playgroud)

结果是这样的:

***** Adding with Thread objects *****
    ID of thread in Main(): 1
    ID of thread in Add(): 3
    10 + 10 is 20
    Other thread is done!
Run Code Online (Sandbox Code Playgroud)

我得到的结果与我预期的不同。请通过给出正确的概念来纠正我的假设。

1. 我假设 Main() 是在主线程上调用的,比如说 Thread1。

2. 调用 AddAsync() 但这个方法被标记为 async,所以这个方法可能是在辅助线程上调用的,比如 Thread2。但是 AddAsync() 方法中的以下代码的结果表明 Thread1 与我预期的 Thread2 不同:

Console.WriteLine("ID of thread in Main(): {0}",Thread.CurrentThread.ManagedThreadId);
Run Code Online (Sandbox Code Playgroud)

3. 调用 Sum() 方法。但是通过 await 关键字修饰,Sum() 方法在辅助线程上调用,比如 Thread3。

  1. 在 Sum() 方法内部,Run() 方法在辅助线程上调用,比如 Thread4。

我此时的理解是这样的:当我进行异步编程时,创建新线程依赖于 CLR。CLR 尽可能地创建多线程,但 CLR 也可以在单个线程上处理异步任务,只需在单个线程上异步处理多个任务即可。我只是假设一个简单的情况,每次我尝试异步任务时,CLR 都会创建一个新线程。

我知道这个话题很难描绘,所以如果能用图来解释会更好。

附加问题。

1. AddAsync() 方法里面的以下代码真的表示“Main()”中的线程ID?对我来说,使用“AddAsync() 中的线程 ID”会更合适。

Console.WriteLine("ID of thread in Main(): {0}", Thread.CurrentThread.ManagedThreadId);
Run Code Online (Sandbox Code Playgroud)

The*_*aot 4


原答案

  1. 我假设 Main() 在主线程(例如 Thread1)上调用。

是的,Main将在主线程上运行。

  1. 调用 AddAsync() 但此方法被标记为异步,因此此方法可能在辅助线程(例如 Thread2)上调用。但是 AddAsync() 方法中以下代码的结果表明 Thread1 与我预期的 Thread2 不同:
Console.WriteLine("ID of thread in Main(): {0}",Thread.CurrentThread.ManagedThreadId);
Run Code Online (Sandbox Code Playgroud)

不会。如果未达到 ,代码将不会切换线程await

static void Main(string[] args) // <- main thread
{
    Console.WriteLine("ID of thread in 1: {0}",
        Thread.CurrentThread.ManagedThreadId); // <- main thread

    AddAsync(); // <- main thread (note: you are not awaiting here)
    Console.ReadLine();
}

private static async Task AddAsync()
{
    Console.WriteLine("***** Adding with Thread objects *****"); // <- main thread
    Console.WriteLine("ID of thread in Main(): {0}", // <- main thread
        Thread.CurrentThread.ManagedThreadId);

    AddParams ap = new AddParams(10, 10); // <- main thread
    await Sum(ap); // <- ok, we cannot continue.
                   // Add `Sum(ap)` to pending stuff.
                   // When Sum(ap) is done we resume here, potentially in another thread.
                   // The main thread is now free to do pending stuff.
                   // Turns out `Sum(ap)` is pending, run it on the main thread.

    Console.WriteLine("Other thread is done!");
}
Run Code Online (Sandbox Code Playgroud)
  1. 调用 Sum() 方法。但是通过await关键字修饰,Sum()方法在辅助线程(例如Thread3)上调用。

它可能会也可能不会在同一个线程中运行。它很可能Sum会在主线程中运行,因为当主线程正在等待Sum并且我们需要一个线程来运行时,Sum主线程可用。

如果您在 的开头添加以下行Sum,我希望它显示与主线程相同的 id:

Console.WriteLine("ID of thread in Sum(): {0}", Thread.CurrentThread.ManagedThreadId);
Run Code Online (Sandbox Code Playgroud)
  1. 在 Sum() 方法内部,在辅助线程(例如 Thread4)上调用 Run() 方法

是的,Task.Run将使用另一个线程,默认情况下来自ThreadPool. 注意:我说默认,因为这取决于TaskScheduler,默认的将使用ThreadPool.


我现在的理解是这样的:当我进行异步编程时,创建新线程取决于CLR。CLR 尽可能创建多线程,但是 CLR 也可以在单个线程上处理异步任务,只需在单个线程上同时异步处理多个任务即可。我只是假设简单的情况,每次尝试异步任务时,CLR 都会创建一个新线程。

它不会每次都启动新线程。async/await不是 around 的语法糖Thread,而是 aroundTask和 延续。Task已经设计为避免在不必要的情况下使用新线程,例如aTask可以内联运行,或者使用ThreadPool.


  1. AddAsync() 方法中的以下代码真的表示“Main()”中线程的 ID 吗?对我来说,“AddAsync() 中的线程 ID”更合适。
Console.WriteLine("ID of thread in Main(): {0}",  Thread.CurrentThread.ManagedThreadId);
Run Code Online (Sandbox Code Playgroud)

正如上面代码中的注释所示,是的,这就是主线程。


到达await Task.Run...后主线程将处于空闲状态,因为它必须等待任务完成。当它恢复时,它返回到AddAsync,运行Console.WriteLine("Other thread is done!");然后返回到Main它运行的地方Console.ReadLine();Main如果在调用之前添加以下行Console.ReadLine,您将看到主线程的 id:

Console.WriteLine("ID of thread before ReadLine: {0}",
    Thread.CurrentThread.ManagedThreadId);
Run Code Online (Sandbox Code Playgroud)

如您所见,您的代码不需要并行性。除了使用它之外Task.Run,它还可以在单​​线程中运行。 勘误表:经过进一步检查,存在并行性,只是不那么明显......请参阅扩展答案。


扩展答案

经过第二次阅读后,我怀疑您期望该调用AddAsync并行运行。正如我上面所说,您没有等待它,在这种情况下,它像常规同步调用一样运行。

如果你想AddAsync并行运行,我建议使用Task.Run,例如:

Task.Run((Func<Task>)AddAsync);
Run Code Online (Sandbox Code Playgroud)

这样做,AddAsync将不再在主线程中运行。主线程将前进到Console.ReadLine甚至可能先于AddASync它结束。注意,主线程结束后执行也会结束。

当然,AddAsync是快,我建议给你await一些Task.Delay时间来击中那个键。


在你提问之前,让我先提出一个问题:它是如何Task.Delay工作的?- 内部结构的简单解释(至少在 Windows 上)是它会向操作系统请求超时。当操作系统看到时间到了,就会调用程序通知超时结束。那种方式Task.Delay不需要使用线程来运行。

这是一种不同的类型Task,它不必运行代码,因此不需要占用线程。我们可以将这种 Taks 称为 Promise。另一个例子是从文件中读取,例如:

using (var reader = File.OpenText("example.txt"))
{
    var fileText = await reader.ReadToEndAsync();
    // ...
}
Run Code Online (Sandbox Code Playgroud)

在这种情况下,读取文件的行为将不需要您的线程之一。在内部,操作系统会要求驱动程序将数据复制到 RAM 缓冲区,并在完成时发出通知(这在现代硬件中会发生,因为DMA需要 CPU 的干预最少),因此那里不使用任何线程。

同时,调用线程可以自由地执行其他操作。如果您有多个类似的操作(例如,您可能正在从文件中读取、将数据发送到网络等),它们可以并行发生,而不使用您的线程,当其中一个操作完成时,您的执行代码将在您的可用线程之一中恢复。


另一件需要考虑的事情是,如果您在 UI 线程上工作,事情的工作方式会略有不同。

在窗口中,很多操作都是从消息队列开始的。无需担心它是如何工作的,但足以说明主线程将花费大量时间等待输入事件(单击、按键等)。

正如您将在扩展答案的末尾看到的那样,方法有可能会在不同的线程中继续。但是,UI 线程只是一个可以与 UI 交互的线程。因此,让 UI 代码在不同的线程中运行并不好。

为了解决这个问题,在UI线程中将await让该线程继续在消息队列上工作。此外,它还会将消息发布到队列中以供继续,从而允许 UI 线程拾取它们。存档的方式是使用不同的TaskSchedulerUI 线程。

这也意味着,如果您在 UI 环境中,并且用于await承诺任务,它将允许它保持对输入事件的响应。这可能会节省你的使用BackgroundWorker......好吧,除非你需要一些需要大量CPU时间的东西,那么你将需要使用Task.Run,调用ThreadPool,使用BackgroundWorker或启动Thread


你的问题

那么,我可以说在这段代码中,一个新线程仅由 Task.Run() 创建吗?async 和await 关键字不会创建新线程?

不,Task.Run正在使用另一个线程,但没有创建它。默认情况下它会回落到ThreadPool.

其作用是什么ThreadPool嗯,它保留了一小组线程,可用于按需运行操作(例如运行 a Task),一旦操作完成,线程将返回到ThreadPool保持空闲状态的位置,直到您再次需要它。摘要:ThreadPool回收线程。


在 AddAsync() 内部调用“await Sum(ap)”时,主线程仍在调用 Sum(ap),对吧

是的,它仍然是主线程。下面将对此进行更详细的介绍。


然后转到 Sum() 方法,代码仍在主线程上处理,突然被 Task.Run() 遇到。此时“Task.Run()”创建了一个新线程并在新线程上执行lambda表达式代码?

正如我上面所说,Task.Run如果不需要,不会创建新线程。它将要求ThreadPool在其线程之一上运行该操作。这些ThreadPool线程用于运行一次性操作,因此您最终不会创建大量线程Thread,而只会回收一些线程。

所以,是的,lambda 中的代码将在不同的 中运行Thread,但它并不是专门为此创建的。


当“Task.Run()”正在处理时,等待Task.Run()方法结果的调用线程(主线程)的状态是什么?是被屏蔽还是不被屏蔽?

首先请注意,您有两个等待选项Task.Run。您可以使用await,也可以使用Task.Wait

  • await将要:

    • “暂停”该方法的执行。
    • Task获取或创建方法的不完整。
    • 添加一个延续来Task“恢复”该方法。或者,如果没有其他东西要运行,则延续会将不完整的设置设置Task为完成。
    • 返回一个不完整的信息Task给调用者。
  • Task.Wait将要:

    • 阻塞线程直到Task完成。

现在,我将更慢地浏览代码......

首先,再次同步部分:

static void Main(string[] args) // <-- entry point, main thread
{
    Console.WriteLine("ID of thread in 1: {0}",
        Thread.CurrentThread.ManagedThreadId); // <-- main thread

    AddAsync(); // <-- main thread. You are not awaiting, this is a sync call.
    Console.ReadLine();
}
Run Code Online (Sandbox Code Playgroud)

此时,我们将创建一个不完整的Task,我将其称为AddAsync Task. 这将是AddAsync返回的内容(并不是说您将使用它,您只是忽略它)。

然后主线程进入AddAsync

private static async Task AddAsync() // <-- called from `AddAsync()`
{
    Console.WriteLine("***** Adding with Thread objects *****"); // <-- main thread
    Console.WriteLine("ID of thread in Main(): {0}",
        Thread.CurrentThread.ManagedThreadId); // <-- main thread

    AddParams ap = new AddParams(10, 10); // <-- main thread
    await Sum(ap); // <-- shenanigans!!!

    Console.WriteLine("Other thread is done!");
}
Run Code Online (Sandbox Code Playgroud)

让我稍微重构一下,很快......

private static async Task AddAsync() // <-- called from `AddAsync()`
{
    Console.WriteLine("***** Adding with Thread objects *****"); // <-- main thread
    Console.WriteLine("ID of thread in Main(): {0}",
        Thread.CurrentThread.ManagedThreadId); // <-- main thread

    AddParams ap = new AddParams(10, 10); // <-- main thread
    var z = Sum(ap); // <-- shenanigans!!!
    await z;

    Console.WriteLine("Other thread is done!");
}
Run Code Online (Sandbox Code Playgroud)

接下来发生的事情是调用Sum. 此时,Task将创建一个新的不完整的Sum。我将把它称为Sum Task.

接下来,主线程进入Sum

static async Task Sum(object data) // <-- called from `await Sum(ap)`
{
    await Task.Run(() =>
    {
        if (data is AddParams)
        {
            Console.WriteLine("ID of thread in Add(): {0}",
                Thread.CurrentThread.ManagedThreadId);

            AddParams ap = (AddParams)data;
            Console.WriteLine("{0} + {1} is {2}",
                ap.a, ap.b, ap.a + ap.b);
        }
    });
}
Run Code Online (Sandbox Code Playgroud)

还有更多恶作剧......让我重构该代码......

static async Task Sum(object data) // <-- called from `await Sum(ap)`
{
    Action y = () =>
    {
        if (data is AddParams)
        {
            Console.WriteLine("ID of thread in Add(): {0}",
                Thread.CurrentThread.ManagedThreadId);

            AddParams ap = (AddParams)data;
            Console.WriteLine("{0} + {1} is {2}",
                ap.a, ap.b, ap.a + ap.b);
        }
    };
    var x = Task.Run(y);
    await x;
}
Run Code Online (Sandbox Code Playgroud)

上面的代码与我们的代码等效。请注意,您可以使用x.Wait()这会阻塞主线程。我们不这样做...

static async Task Sum(object data) // <-- called from `await Sum(ap)`
{
    Action y = () =>
    {
        if (data is AddParams)
        {
            Console.WriteLine("ID of thread in Add(): {0}",
                Thread.CurrentThread.ManagedThreadId);

            AddParams ap = (AddParams)data;
            Console.WriteLine("{0} + {1} is {2}",
                ap.a, ap.b, ap.a + ap.b);
        }
    }; // <-- Action created in main thread
    var x = Task.Run(y); // <-- main threat: create a new Task x with the action y
                         //    start the new Task in a thread from the thread pool
    await x;
}
Run Code Online (Sandbox Code Playgroud)

现在,有趣的部分......

static async Task Sum(object data)
{
    Action y = () =>
    {
        if (data is AddParams) // <-- second thread
        {
            Console.WriteLine("ID of thread in Add(): {0}",
                Thread.CurrentThread.ManagedThreadId);

            AddParams ap = (AddParams)data;
            Console.WriteLine("{0} + {1} is {2}",
                ap.a, ap.b, ap.a + ap.b);
        }
    };
    var x = Task.Run(y);
    await x; // <-- Add a continuation to x
             //    so that when it finished, it will set the Sum Task to completed
}
Run Code Online (Sandbox Code Playgroud)

现在该方法Sum返回(不完整Sum Task

private static async Task AddAsync()
{
    Console.WriteLine("***** Adding with Thread objects *****");
    Console.WriteLine("ID of thread in Main(): {0}",
        Thread.CurrentThread.ManagedThreadId);

    AddParams ap = new AddParams(10, 10);
    var z = Sum(ap); // <-- main thread, z is now the incomplete Sum Task
    await z; // <-- Add a continuation to z
             //    so that when it finished, it will resume `AddAsync`
             //    `AddAsync` is "paused" now.
             //    main thread returns the incomplete Async Task

    Console.WriteLine("Other thread is done!");
}
Run Code Online (Sandbox Code Playgroud)

现在该方法AddAsync返回(不完整AddAsync Task)。我想在这里强调一下:该方法AddSync还没有完成,但它正在以不完整的状态返回。

static void Main(string[] args)
{
    Console.WriteLine("ID of thread in 1: {0}",
        Thread.CurrentThread.ManagedThreadId);

    AddAsync();
    Console.ReadLine(); // <-- main thread
}
Run Code Online (Sandbox Code Playgroud)

与此同时,第二个线程完成......

static async Task Sum(object data)
{
    Action y = () =>
    {
        if (data is AddParams) // <-- second thread
        {
            Console.WriteLine("ID of thread in Add(): {0}",
                Thread.CurrentThread.ManagedThreadId); // <-- second thread

            AddParams ap = (AddParams)data; // <-- second thread
            Console.WriteLine("{0} + {1} is {2}",
                ap.a, ap.b, ap.a + ap.b); // <-- second thread
        }
    };
    var x = Task.Run(y);
    await x;
}
Run Code Online (Sandbox Code Playgroud)

并触发我们添加到的延续x

该延续将 Sum Task ( z) 设置为已完成。这将恢复AddAsync

private static async Task AddAsync()
{
    Console.WriteLine("***** Adding with Thread objects *****");
    Console.WriteLine("ID of thread in Main(): {0}",
        Thread.CurrentThread.ManagedThreadId);

    AddParams ap = new AddParams(10, 10);
    var z = Sum(ap);
    await z;

    Console.WriteLine("Other thread is done!"); // <-- second thread
}
Run Code Online (Sandbox Code Playgroud)

现在,AddAsync结束了。然而,正如我上面所说,你只是忽略了AddAsync返回的内容。您没有Wait或 或await向其添加延续...第二个线程没有其他事情可做,现在第二个线程死亡。

注意:需要明确的是,第二个线程来自ThreadPool. 您可以通过阅读来检查自己Thread.CurrentThread.IsThreadPoolThread