Допустим, у меня есть этот код:
public async void Run()
{
TaskCompletionSource t = new TaskCompletionSource();
Prepare(t.Task);
await Task.Delay(1000);
t.SetResult();
Console.WriteLine("End");
}
public async void Prepare(Task task)
{
await Run(task, "A");
await Run(task, "B");
await Run(task, "C");
await Run(task, "D");
}
public async Task Run(Task requisite, string text)
{
await requisite;
Console.WriteLine(text);
}
Что происходит, когда вызывается t.SetResult();
?
Это многопоточность?
Есть ли гарантия порядка элементов в Консоли?
Если у меня есть List<>
, и метод Run
меняет его, нужно ли мне беспокоиться о многопоточности?
Это упрощение реального вопроса... Буду признателен, если мы сосредоточимся на ответе, а не на обходных путях...
SetResult()
первая задача («A») возобновляет выполнение после await
внутри функции Run(Task, string)
и печатает «A». Затем возвращается от Prepare()
к await Run(task, "B")
, что печатает «B». И так далее. Наконец, «ABCD» будет напечатан по порядку.TaskScheduler
используется в текущей среде и какие еще задачи выполняются в этом планировщике. По умолчанию у вас будет пул потоков, содержащий несколько потоков. Скорее всего (но без каких-либо гарантий) ваши задачи будут выполняться в разных потоках, а не параллельно (если мы говорим о задачах ABCD).await
внутри List<>
- скорее всего, это сработает, потому что одновременных изменений нет. В то же время это определенно хрупкий код, который можно легко взломать во время будущих модификаций кода. Я постараюсь избегать такого кода, насколько это возможно, и использовать Run
или какую-нибудь параллельную коллекцию. Или хотя бы напишите длинный комментарий, описывающий ограничения текущей реализации.Спасибо! Я думаю, что действительно не могу использовать задачи для своей проблемы... У меня есть некоторая параллельная задача, требующая быстрой синхронизации... Я думаю, мне нужно вручную контролировать фрагментацию метода внутри множества более мелких методов и вручную контролировать их выполнение. ..
@Everton Вот почему лучше объяснить, чего вы пытаетесь достичь, а не спрашивать подробности, связанные с вашей попыткой решения. Конечно, вы можете показать это как попытку, но если бы мы точно знали, что вы делаете, мы потенциально могли бы предложить решение.
Технически, если не обязательно запускать все задачи параллельно (что в любом случае практически невозможно из-за аппаратных ограничений), то можно добиться некоторого параллелизма с Task.WhenAll()
и друзьями вместо последовательных await
. Итак, вы запускаете кучу задач и ждете их всех. В этом случае вы получите максимально возможный параллелизм (управляемый пулом потоков в зависимости от ресурсов системы).
@EvertonElvioKoser рассмотрите возможность принятия этого ответа и в следующий раз задайте более конкретный вопрос...
Пока вы написали простое заявление await
;
await requisite;
Компилятор C# перенес ваш метод в отдельный класс и перевел это «простое» await
во что-то эквивалентное;
private Task requisite;
private int state = 0;
private void MoveNext()
{
switch (state)
{
case 0:
var awaiter = requisite.GetAwaiter();
if (!awaiter.IsCompleted)
{
state = 1;
awaiter.OnCompleted(MoveNext);
return;
}
goto case 1;
case 1:
// resume execution here when the task is complete
break;
}
}
Как видите, если задача уже завершена, ваш код продолжится синхронно. Если задача была незавершенной, будет зарегистрирован обратный вызов, чтобы продолжить выполнение позже.
Точная реализация каждого awaiter может сильно различаться. Например, Task.Yield
вернет ожидающее сообщение, которое никогда не будет завершено. Принудительное выполнение продолжения в пуле потоков.
Большинство типов задач и все задачи из вашего примера будут вызывать каждый метод продолжения синхронно, пока вызывается .SetResult
.
Вернемся к вашему конкретному примеру;
public async void Prepare(Task task)
{
await Run(task, "A");
await Run(task, "B");
await Run(task, "C");
await Run(task, "D");
}
При первом вызове Run
задача не завершена, поэтому Run
зарегистрирует продолжение Action
и вернет неполную задачу. Первый await
в методе Prepare
также увидит, что возвращенная задача является неполной. Хотя метод void
, поэтому еще одна незавершенная задача не возвращается.
Когда вы возобновите работу после задержки и вызовете t.SetResult();
, сначала будет вызвано зарегистрированное продолжение для возобновления метода Run
. Когда метод Run
завершается, а не возвращается, .SetResult
будет вызываться для незавершенной задачи, что приведет к немедленному вызову продолжения Prepare
. Все это произойдет в одном потоке до того, как t.SetResult
вернется.
Если бы вы поместили точку останова в Run
или Prepare
и проверили стек вызовов, вы бы увидели, как стек потоков инвертируется по сравнению с тем, что вы обычно ожидаете. Каждая возвращающая функция async
будет там с вызовом SetResult
в стеке.
«Task.Yield
вернет ожидающее событие, которое никогда не завершится. Принудительное выполнение продолжения в пуле потоков». -- На самом деле await Task.Yield() продолжается на захваченном SynchronizationContext
. Это его документированное поведение. ИМХО, это не пример, подходящий для этого вопроса.
Дело было лишь в том, что официанты могут вести себя по-разному. например, дескрипторы ввода-вывода и другие дескрипторы и таймеры ОС. Но большинство других задач просто немедленно выполняют обратные вызовы.
Это во многом зависит от пропускной способности, которую обрабатывает приложение: при высоком трафике может быть задействовано несколько потоков.
Но, как правило, Task
и использование async
/await
не гарантирует многопоточность, новые потоки используются (из пула потоков), когда они необходимы - текущий поток занят какой-то работой и не может взять на себя новую работу.
Идея асинхронного выполнения заключается в том, что когда есть работа, которую нужно дождаться (наш процессор должен чего-то ждать, например операцию ввода-вывода), он освобождает текущий поток и позволяет ему выполнить другую работу.
Таким образом, может случиться так, что один поток выполняет две задачи «параллельно». Но это многозадачность, а не многопоточность.
Обладая этими знаниями, давайте проанализируем ваш код.
Во-первых, вы создаете TaskCompletionSource
, который имеет Task
, который находится в "действии" и его можно ожидать, но он завершится, как только мы вызовем SetResult
на TaskCompletionSource
.
Затем мы вызываем Prepare
, который мы не ждем, но он ожидает прохождения Task
, поэтому этот путь выполнения остановится на await Run(task, "A")
и будет ждать завершения задачи, что произойдет, когда мы вызовем SetResult
(как объяснялось ранее). После Prepare
(значит задание уже ожидается) звоним await Task.Delay
— у нас тут ещё один await
, на этот раз за задержку. Поскольку мы используем Task
, новый поток не будет создан, поскольку await Run
просто ожидает, поток может продолжить другую работу и обработать выполнение вашего кода. Так что ждем задержки и задача будет выполняться параллельно, асинхронно.
Затем, когда задержка завершится, он установит результат задачи, то есть await Run(task, "A")
будет «разблокирован», распечатает результат и продолжит await Run(task, "B")
, но при этом будет предпринята попытка дождаться уже выполненной задачи, и поэтому она будет выполняться синхронно. (задание уже выполнено, ждать нечего), сразу печатаем «Б», затем «С», а затем «Д» (последовательно).
Вы можете столкнуться с одной проблемой: ваша программа может завершиться до завершения всех задач в методе Prepare
, потому что это async void
, и у нас нет возможности ее дождаться. Я бы предложил небольшое улучшение:
public async void Run()
{
TaskCompletionSource t = new TaskCompletionSource();
var prepareTask = Prepare(t.Task);
await Task.Delay(1000);
t.SetResult();
await prepareTask;
Console.WriteLine("End");
}
public async Task Prepare(Task task)
{
await Run(task, "A");
await Run(task, "B");
await Run(task, "C");
await Run(task, "D");
}
public async Task Run(Task requisite, string text)
{
await requisite;
Console.WriteLine(text);
}
Перестаньте использовать
async void
и дождитесь выполнения всех своих задач, и эти вопросы отпадут.