Почему я получаю предупреждение при попытке запустить асинхронный метод в другом потоке? - PullRequest
0 голосов
/ 25 апреля 2019

У меня есть асинхронный метод, который вызывает другой асинхронный метод, однако я хочу, чтобы он выполнялся в отдельном потоке параллельно:

public async Task<Page> ServePage() {
  Task.Run(() => DoThings(10));   // warning here

  // ... other code
  return new Page();
}

public async Task DoThings(int foo) {
  // stuff
}

Предупреждение гласит:

Поскольку этот вызов не ожидается, выполнение текущего метода продолжается до завершения вызова. Попробуйте применить оператор 'await' к результату вызова.

На самом деле это то, что я пытаюсь сделать. Почему я получаю предупреждение компилятора? Неверный ли синтаксис Task.Run?

Ответы [ 4 ]

3 голосов
/ 25 апреля 2019

TL; DR

Причина, по которой вы получаете предупреждение, заключается в том, что

 Task.Run(() => DoThings(10));   // warning here

возвращает задание, и поскольку ваш метод ServePage помечен как асинхронный, компилятор считает, что вам следует дождаться результата Task

Detail

Вы смешиваете две очень разные парадигмы, которые по совпадению включают Task, а именно:

  • Task.Run(), что обычно полезно для распараллеливания работы, связанной с процессором, с использованием нескольких ядер, которые могут быть доступны
  • async / await, что полезно для ожидания завершения операций, связанных с вводом / выводом, без блокировки (траты) потока.

Так, например, если вы хотите выполнить 3 операции с привязкой к ЦП одновременно, а поскольку Task.Run возвращает Task, вы можете сделать следующее:

public Page ServePage() // If we are CPU bound, there's no point decorating this as async
{
    var taskX = Task.Run(() => CalculateMeaningOfLife()); // Start taskX
    var taskY = Task.Run(() => CalculateJonSkeetsIQ()); // Start taskY
    var z = DoMoreHeavyLiftingOnCurrentThread();
    Task.WaitAll(taskX, taskY); // Wait for X and Y - the Task equivalent of `Thread.Join`

    // Return a final object comprising data from the work done on all three tasks
    return new Page(taskX.Result, taskY.Result, z);
}

Выше, вероятно, потребуются три потока, которые могли бы одновременно выполнять работу с процессором, если для этого достаточно ядер.

Это в отличие от async / await, который обычно используется для освобождения потоков при ожидании завершения вызовов, связанных с вводом / выводом. Предполагая, что DoThings действительно связан с вводом / выводом и выглядит примерно так:

public async Task<string> DoThings(int foo) {
   var result = await SomeAsyncIo(foo);
   return "done!";
}

Параллельно, но все же, асинхронно, вы можете сделать:

public async Task<Page> ServePage() {
   var task1 = DoThings(123); // Kick off Task 1
   var task2 = DoThings(234); // Kick off Task 2 in parallel with task 1
   await Task.WhenAll(task1, task2); // Wait for both tasks to finish, while releasing this thread
   return new Page(task1.Result, task2.Result); // Return a result with data from both tasks
}

Если работа, связанная с вводом / выводом, занимает разумное количество времени, есть большая вероятность, что во время await Task.WhenAll, когда потоки ZERO действительно работают, есть момент - См. Статья Стивена Клири .

Есть 3-й, но очень опасный вариант - это огонь и забыть. Поскольку метод DoThings уже помечен как async, он уже возвращает Task, поэтому вообще не нужно использовать Task.Run. Огонь и забыл бы выглядеть следующим образом:

public Page ServePage() // No async
{
  #pragma warning disable 4014 //   warning is suppresed by the Pragma
  DoThings(10);   // Kick off DoThings but don't wait for it to complete.
  #pragma warning enable 4014

  // ... other code
  return new Page();
}

Согласно комментарию @ JohnWu, подход «забей и забудь» опасен и обычно указывает на запах дизайна. Подробнее об этом здесь и здесь

Редактировать

Re:

есть нюанс, который ускользает от меня снова и снова, например, вызов асинхронного метода, который возвращает Task из синхронного метода, запускает и забывает выполнение метода. (Это самый последний пример кода.) Я правильно понимаю?

Немного сложно объяснить, но независимо от того, вызывается ли с или без ключевого слова await какой-либо синхронный код в вызываемом методе async 1072 * до того, как первое ожидание будет выполнено на Тема звонящего, если только мы не прибегнем к молоткам типа Task.Run.

Возможно, этот пример может помочь пониманию (обратите внимание, что мы сознательно используем синхронный Thread.Sleep, а не await Task.Delay для имитации работы с привязкой к процессору и введения задержки, которая может наблюдаться)

public async Task<Page> ServePage()
{
  // Launched from this same thread, 
  // returns after ~2 seconds (i.e. hits both sleeps) 
  // continuation printed.
  await DoThings(10);

  #pragma warning disable 4014
  // Launched from this same thread, 
  // returns after ~1 second (i.e. hits first sleep only) 
  // continuation not yet printed
  DoThings(10);   

  // Task likely to be scheduled on a second thread
  // will return within few milliseconds (i.e. not blocked by any sleeps)
  Task.Run(() => DoThings(10));   

  // Task likely to be scheduled on a second thread
  // will return after 2 seconds, although caller's thread will be released during the await
  // Generally a waste of a thread unless also doing CPU bound work on current thread, or unless we want to release the calling thread.
  await Task.Run(() => DoThings());   

  // Redundant state machine, returns after 2 seconds
  // see return Task vs async return await Task https://stackoverflow.com/questions/19098143
  await Task.Run(async () => await DoThings());   
}

public async Task<string> DoThings(int foo) {
   Thread.Sleep(1000); 
   var result = await SomeAsyncIo(foo);
   Trace.WriteLine("Continuation!"); 
   Thread.Sleep(1000); 
   return "done!";
}

Следует отметить еще один важный момент - в большинстве случаев нет никаких гарантий, что код продолжения ПОСЛЕ ожидания будет выполнен в том же потоке, что и до await. Код продолжения переписывается компилятором в Задачу, и задача продолжения будет запланирована в пуле потоков.

1 голос
/ 25 апреля 2019

Когда вы вызываете асинхронный метод, который возвращает задачу, вы можете сделать две вещи:

  1. Ожидание задачи, которая немедленно возвращает управление вызывающей стороне и возобновляет работу по завершении задачи (с возвращаемым значением или без него). Это также место, где вы можете перехватывать любые исключения, которые могли произойти при выполнении Задачи.

  2. Огонь и забудь. Это когда вы запускаете задачу и полностью ее освобождаете, включая возможность ловить исключения. Самое главное, что, если не ожидается, выполнение контроля будет продолжаться после вызова и может вызвать непреднамеренные проблемы с повреждением состояния.

Вы сделали # 2 в своем коде. Хотя это технически разрешено, по причинам, указанным выше, компилятор предупредил вас.

Вопрос к вам, хотя - если вы действительно просто хотели выстрелить и забыть, зачем вам нужно было создать эту явную задачу? Вы могли бы просто позвонить DoThings (10) напрямую, не так ли? Если я не пропущу что-то в коде, что мне не видно. Итак - ты не мог сделать это?

public async Task<Page> ServePage() {
  DoThings(10); 
}
1 голос
/ 25 апреля 2019

Это

Task.Run(() => DoThings(10));

запустит ваше отдельное задание «параллельно», что означает, что другой поток запустит это задание. Поток, который вошел в этот метод, затем продолжит выполнение следующего оператора.

То, что вы здесь делаете, разрешено. Вот почему это предупреждение. (Я предполагаю, что остальная часть метода, не показанного, возвращает Page.)

Сообщение предупреждает вас, что, поскольку другая задача выполняется в другом потоке, она может выполняться до, после или одновременно с другим кодом в методе. Этот метод больше не «знает», что делает задача или когда она завершена.

Это говорит, другими словами:

Не думайте, что эта строка кода появляется перед другими строками кода в методе, который он собирается выполнить до того, как они это сделают. Если что-то должно быть выполнено после завершения этой задачи, дождитесь этого, прежде чем делать следующее.

Как пример:

public int DoSomething()
{
    var x = 1;
    Task.Run(() => x++);
    return x;        
}

Что это возвращает? Это зависит. Может возвращать 1 или 2. Может возвращаться либо до, либо после увеличения x. Если вас волнует, был ли увеличен x, это плохо. Если ваша задача делает что-то, что не имеет значения для остальной части метода, и вам все равно, завершится ли задача до или после остальной части метода, то это предупреждение не будет иметь для вас значения. Если вам все равно, это важно, и вы захотите дождаться выполнения задачи.

0 голосов
/ 25 апреля 2019

Все версии ниже действительны, без предупреждений и более или менее эквивалентны:

public Task ServePage1()
{
    return Task.Run(async () => await DoThings(10));
}

public async Task ServePage2()
{
    await Task.Run(async () => await DoThings(10));
}

public Task ServePage3()
{
    return Task.Run(() => DoThings(10));
}

public async Task ServePage4()
{
    await Task.Run(() => DoThings(10));
}

Как правило, вы не должны запускать и забывать задачи, игнорируя возвращаемое значение Task.Run. Если вы сделаете это, компилятор выдаст предупреждение, потому что оно редко является преднамеренным.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...