Стоит ли мне беспокоиться о предупреждении «В этом асинхронном методе отсутствуют операторы await и он будет работать синхронно»


93

У меня есть интерфейс, который предоставляет некоторые асинхронные методы. В частности, в нем определены методы, возвращающие либо Task, либо Task <T>. Я использую ключевые слова async / await.

Я в процессе реализации этого интерфейса. Однако в некоторых из этих методов этой реализации нечего ждать. По этой причине я получаю предупреждение компилятора: «В этом асинхронном методе отсутствуют операторы await, и он будет работать синхронно ...»

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

Я знаю, что могу исправить это, ожидая выполнения Task.Run, но это неправильно для метода, который выполняет всего несколько недорогих операций. Также похоже, что это добавит ненужные накладные расходы на выполнение, но я также не уверен, есть ли это уже, потому что присутствует ключевое слово async.

Должен ли я просто игнорировать предупреждения или есть способ обойти это, которого я не вижу?


2
Это будет зависеть от специфики. Вы действительно уверены, что хотите, чтобы эти операции выполнялись синхронно? Если вы действительно хотите, чтобы они выполнялись синхронно, почему метод отмечен как async?
— Servy

11
Просто удалите asyncключевое слово. Вы все еще можете вернуть Taskusing Task.FromResult.
— Майкл Лю

1
@BenVoigt Google полон информации об этом, если OP еще не знает.
— Servy

1
@BenVoigt Разве Майкл Лю уже не намекнул? Используйте Task.FromResult.

1
@hvd: Позже это было отредактировано в его комментарии.
— Бен Войт

Ответы:


144

Асинхронная ключевое слово является лишь деталью реализации способа; это не часть сигнатуры метода. Если при реализации или переопределении одного конкретного метода нечего ожидать, просто опустите ключевое слово async и верните завершенную задачу с помощью Task.FromResult <TResult> :

public Task<string> Foo()               //    public async Task<string> Foo()
{                                       //    {
    Baz();                              //        Baz();
    return Task.FromResult("Hello");    //        return "Hello";
}                                       //    }

Если ваш метод возвращает Task вместо Task <TResult> , вы можете вернуть выполненную задачу любого типа и значения. Task.FromResult(0)кажется популярным выбором:

public Task Bar()                       //    public async Task Bar()
{                                       //    {
    Baz();                              //        Baz();
    return Task.FromResult(0);          //
}                                       //    }

Или, начиная с .NET Framework 4.6, вы можете вернуть Task.CompletedTask :

public Task Bar()                       //    public async Task Bar()
{                                       //    {
    Baz();                              //        Baz();
    return Task.CompletedTask;          //
}                                       //    }

Спасибо. Я думаю, что то, что мне не хватало, - это концепция создания задачи, которая была завершена, а не возвращение фактической задачи, которая, как вы говорите, была бы такой же, как наличие ключевого слова async. Сейчас это кажется очевидным, но я просто этого не видел!
— dannykay1710

1
Для этой цели Task может использоваться статический член типа Task.Empty. Намерение было бы немного яснее, и мне больно думать обо всех этих послушных Задачах, которые возвращают ноль, который никогда не нужен.
— Руперт Ронсли, 08

await Task.FromResult(0)? Как насчет await Task.Yield()?
— Sushi271

1
@ Sushi271: Нет, без asyncметода вы возвращаетесь Task.FromResult(0) вместо того, чтобы ждать его.
— Майкл Лю,

1
На самом деле НЕТ, async - это не просто деталь реализации, есть много деталей, о которых нужно знать :). Нужно знать, какая часть выполняется синхронно, какая асинхронно, каков текущий контекст синхронизации и только для записи. Задачи всегда немного быстрее, так как за занавесками нет конечного автомата :).
— ipavlu 09

16

Совершенно разумно, что некоторые «асинхронные» операции выполняются синхронно, но все же соответствуют модели асинхронного вызова ради полиморфизма.

Реальный пример этого - API-интерфейсы ввода-вывода ОС. Асинхронные и перекрывающиеся вызовы на некоторых устройствах всегда завершаются встроенными (например, запись в конвейер, реализованный с использованием общей памяти). Но они реализуют тот же интерфейс, что и операции с несколькими частями, которые продолжаются в фоновом режиме.


4

Майкл Лю хорошо ответил на ваш вопрос о том, как можно избежать предупреждения: вернув Task.FromResult.

Я собираюсь ответить на часть вашего вопроса «Стоит ли мне беспокоиться о предупреждении».

Ответ - да!

Причина этого в том, что предупреждение часто возникает, когда вы вызываете метод, который возвращается Taskвнутри асинхронного метода без awaitоператора. Я только что исправил ошибку параллелизма, возникшую из-за того, что я вызвал операцию в Entity Framework, не дожидаясь предыдущей операции.

Если вы можете тщательно написать свой код, чтобы избежать предупреждений компилятора, то при появлении предупреждения оно будет выделяться, как больной палец. Я мог бы избежать нескольких часов отладки.


5
Это неверный ответ. И вот почему: awaitвнутри метода может быть хотя бы один в одном месте (CS1998 не будет), но это не значит, что не будет другого вызова метода asnyc, в котором не будет синхронизации (using awaitили любой другой). Теперь, если кто-то хочет знать, как убедиться, что вы случайно не пропустите синхронизацию, просто убедитесь, что вы не проигнорируете другое предупреждение - CS4014. Я бы даже рекомендовал угрожать этому как ошибкой.
— Виктор Ярема

3

Может быть, уже слишком поздно, но расследование может оказаться полезным:

О внутренней структуре скомпилированного кода ( IL ):

 public static async Task<int> GetTestData()
    {
        return 12;
    }

это делается в IL:

.method private hidebysig static class [mscorlib]System.Threading.Tasks.Task`1<int32> 
        GetTestData() cil managed
{
  .custom instance void [mscorlib]System.Runtime.CompilerServices.AsyncStateMachineAttribute::.ctor(class [mscorlib]System.Type) = ( 01 00 28 55 73 61 67 65 4C 69 62 72 61 72 79 2E   // ..(UsageLibrary.
                                                                                                                                     53 74 61 72 74 54 79 70 65 2B 3C 47 65 74 54 65   // StartType+<GetTe
                                                                                                                                     73 74 44 61 74 61 3E 64 5F 5F 31 00 00 )          // stData>d__1..
  .custom instance void [mscorlib]System.Diagnostics.DebuggerStepThroughAttribute::.ctor() = ( 01 00 00 00 ) 
  // Code size       52 (0x34)
  .maxstack  2
  .locals init ([0] class UsageLibrary.StartType/'<GetTestData>d__1' V_0,
           [1] valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32> V_1)
  IL_0000:  newobj     instance void UsageLibrary.StartType/'<GetTestData>d__1'::.ctor()
  IL_0005:  stloc.0
  IL_0006:  ldloc.0
  IL_0007:  call       valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<!0> valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32>::Create()
  IL_000c:  stfld      valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32> UsageLibrary.StartType/'<GetTestData>d__1'::'<>t__builder'
  IL_0011:  ldloc.0
  IL_0012:  ldc.i4.m1
  IL_0013:  stfld      int32 UsageLibrary.StartType/'<GetTestData>d__1'::'<>1__state'
  IL_0018:  ldloc.0
  IL_0019:  ldfld      valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32> UsageLibrary.StartType/'<GetTestData>d__1'::'<>t__builder'
  IL_001e:  stloc.1
  IL_001f:  ldloca.s   V_1
  IL_0021:  ldloca.s   V_0
  IL_0023:  call       instance void valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32>::Start<class UsageLibrary.StartType/'<GetTestData>d__1'>(!!0&)
  IL_0028:  ldloc.0
  IL_0029:  ldflda     valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32> UsageLibrary.StartType/'<GetTestData>d__1'::'<>t__builder'
  IL_002e:  call       instance class [mscorlib]System.Threading.Tasks.Task`1<!0> valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32>::get_Task()
  IL_0033:  ret
} // end of method StartType::GetTestData

И без асинхронного метода и метода задач:

 public static int GetTestData()
        {
            return 12;
        }

становится:

.method private hidebysig static int32  GetTestData() cil managed
{
  // Code size       8 (0x8)
  .maxstack  1
  .locals init ([0] int32 V_0)
  IL_0000:  nop
  IL_0001:  ldc.i4.s   12
  IL_0003:  stloc.0
  IL_0004:  br.s       IL_0006
  IL_0006:  ldloc.0
  IL_0007:  ret
} // end of method StartType::GetTestData

Как вы могли заметить, большая разница между этими методами. Если вы не используете await внутри асинхронного метода и не заботитесь об использовании асинхронного метода (например, вызов API или обработчик событий), хорошая идея преобразует его в обычный метод синхронизации (это сохраняет производительность вашего приложения).

Обновлено:

Также есть дополнительная информация из документов microsoft https://docs.microsoft.com/en-us/dotnet/standard/async-in-depth :

async-методы должны иметь ключевое слово await в своем теле, иначе они никогда не сработают! Об этом важно помнить. Если ожидание не используется в теле асинхронного метода, компилятор C # выдаст предупреждение, но код будет компилироваться и запускаться, как если бы это был обычный метод. Обратите внимание, что это также было бы невероятно неэффективным, поскольку конечный автомат, сгенерированный компилятором C # для метода async, ничего не будет выполнять.


2
Кроме того, ваш окончательный вывод об использовании async/awaitзначительно упрощен, поскольку вы основываете его на своем нереалистичном примере единственной операции, связанной с процессором. Tasks при правильном использовании позволяет повысить производительность и
— скорость

Это всего лишь упрощенный тестовый пример, как я сказал в этом посте. Также я упомянул о запросах к api и хендлерам событий, где возможно использование обеих версий методов (асинхронных и обычных). Также ПО сказал об использовании асинхронных методов без ожидания внутри. Мой пост был об этом, но не о правильном использовании Tasks. Печальная история, что вы не читаете весь текст сообщения и не делаете быстрых выводов.
— Олег Бондаренко

1
Существует разница между методом, который возвращает int(как в вашем случае), и методом, который возвращает, Taskнапример, как обсуждалось OP. Прочтите его сообщение и принятый ответ еще раз, вместо того, чтобы принимать вещи лично. Ваш ответ в этом случае бесполезен. Вы даже не удосужитесь показать разницу между методом, имеющимся awaitвнутри, или нет. Если бы вы это сделали, это было бы очень хорошо, и за это стоит проголосовать
— MickyD

Я думаю, вы действительно не понимаете разницы между асинхронным методом и обычными методами, которые вызываются с помощью api или обработчиков событий. Это было специально упомянуто в моем посте. Извините за то, что вам снова не хватает этого .
— Олег Бондаренко

1

Обратите внимание на поведение исключения при возврате Task.FromResult

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

public Task<string> GetToken1WithoutAsync() => throw new Exception("Ex1!");

// Warning: This async method lacks 'await' operators and will run synchronously. Consider ...
public async Task<string> GetToken2WithAsync() => throw new Exception("Ex2!");  

public string GetToken3Throws() => throw new Exception("Ex3!");
public async Task<string> GetToken3WithAsync() => await Task.Run(GetToken3Throws);

public async Task<string> GetToken4WithAsync() { throw new Exception("Ex4!"); return await Task.FromResult("X");} 


public static async Task Main(string[] args)
{
    var p = new Program();

    try { var task1 = p.GetToken1WithoutAsync(); } 
    catch( Exception ) { Console.WriteLine("Throws before await.");};

    var task2 = p.GetToken2WithAsync(); // Does not throw;
    try { var token2 = await task2; } 
    catch( Exception ) { Console.WriteLine("Throws on await.");};

    var task3 = p.GetToken3WithAsync(); // Does not throw;
    try { var token3 = await task3; } 
    catch( Exception ) { Console.WriteLine("Throws on await.");};

    var task4 = p.GetToken4WithAsync(); // Does not throw;
    try { var token4 = await task4; } 
    catch( Exception ) { Console.WriteLine("Throws on await.");};
}
// .NETCoreApp,Version=v3.0
Throws before await.
Throws on await.
Throws on await.
Throws on await.

(Перекрестное сообщение моего ответа на вопрос, когда async Task <T> требуется интерфейсом, как получить возвращаемую переменную без предупреждения компилятора )

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.