Я думаю, что лучший способ продемонстрировать это через простое приложение Windows Forms.
Создайте приложение Windows Forms по умолчанию и поместите на него 3 кнопки (называемые button1
, button2
и button3
.
Затем добавьте следующий код:
async void button1_Click(object sender, EventArgs e)
{
this.Text = "[button1_Click] About to await slowMethodAsync()";
int result = await slowMethodAsync();
this.Text = "[button1_Click] slowMethodAsync() returned " + result;
}
void button2_Click(object sender, EventArgs e)
{
this.Text = "[button2_Click] About to start task to call slowMethod()";
int result = 0;
Task.Run(() =>
{
result = slowMethod();
}).ContinueWith(_ =>
{
this.Invoke(new Action(() =>
{
this.Text = "[button2_Click] slowMethod() returned " + result;
}));
});
}
void button3_Click(object sender, EventArgs e)
{
this.Text = "[button3_Click] About to call slowMethod()";
int result = slowMethod();
this.Text = "[button3_Click] slowMethod() returned " + result;
}
static async Task<int> slowMethodAsync()
{
await Task.Delay(5000);
return 42;
}
static int slowMethod()
{
Thread.Sleep(5000);
return 42;
}
Если вы попробуете этот код, вы заметите следующее:
Нажатие кнопки 1 немедленно изменит название на [button1_Click] About to await Task.Delay(5000)
, и вы можете изменить размер диалогового окна, ожидая в течение 5 секунд, после чего заголовок изменится на [button1_Click] Awaited Task.Delay(5000)
.
Код для обработки кнопки 2 очень приблизительно эквивалентен конечному автомату, который генерируется изawait
код для кнопки 1. Если вы нажмете кнопку 2, вы увидите эффекты, аналогичные нажатию кнопки 1.
(Фактический код для await
в действительности совсем другой, но основной механизм использования продолжения -то есть ContinueWith()
и Invoke()
, чтобы продолжить выполнение кода после того, как await
в потоке пользовательского интерфейса иллюстрирует его подход.)
Код для button3 полностью блокирует duriНапример, Thread.Sleep()
, и если вы нажимаете кнопку 3, пользовательский интерфейс полностью блокируется на 5 секунд.
Чтобы проиллюстрировать, что происходит с примером без пользовательского интерфейса, рассмотрите следующее консольное приложение:
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Demo
{
static class Program
{
static async Task Main()
{
Console.WriteLine("Main thread ID = " + Thread.CurrentThread.ManagedThreadId);
int result = slowMethod();
Console.WriteLine("result = " + result);
Console.WriteLine("After calling slowMethod(), thread ID = " + Thread.CurrentThread.ManagedThreadId);
result = await slowMethodAsync();
Console.WriteLine("result = " + result);
Console.WriteLine("After calling slowMethodAsync(), thread ID = " + Thread.CurrentThread.ManagedThreadId);
}
static async Task<int> slowMethodAsync()
{
await Task.Delay(5000);
return 42;
}
static int slowMethod()
{
Thread.Sleep(5000);
return 42;
}
}
}
Если вы запустите это, вы увидите вывод, подобный следующему:
Main thread ID = 1
result = 42
After calling slowMethod(), thread ID = 1
result = 42
After calling slowMethodAsync(), thread ID = 4
Обратите внимание, как код возобновился в другом потоке после ожидания.
Ключевым моментом для понимания является то, что в отношении вызывающего кода y = await X();
не возвращается до тех пор, пока не получит возвращаемое значение, а код, который запускается впоследствии, может выполняться в другом потоке.
ЭффектЭто означает, что с точки зрения блокировки THREADS вызывающий поток освобождается, чтобы завершить работу и выполнить какой-то другой код, а другой поток требуется только при возврате метода async
.
Во многих случаях это означает, чточто дополнительный поток не требуется (для продолжения), и во всех случаях это означает, что исходный вызывающий поток не заблокирован и может быть освобожден для нас до пула потокове для другой задачи.
Это «не блокирующая» часть всего этого.
Для хорошего подробного объяснения того, почему иногда не требуется дополнительная нить, прочитайте превосходную статью Стивена Клири "Нет темы" .