Tôi có nên lo lắng về cảnh báo "Phương thức không đồng bộ này thiếu các toán tử" chờ đợi "và sẽ chạy đồng bộ không"


93

Tôi có một giao diện hiển thị một số phương thức không đồng bộ. Cụ thể hơn, nó có các phương thức được định nghĩa trả về Task hoặc Task <T>. Tôi đang sử dụng từ khóa async / await.

Tôi đang trong quá trình thực hiện giao diện này. Tuy nhiên, trong một số phương pháp này, việc triển khai này không có gì để chờ đợi. Vì lý do đó, tôi nhận được cảnh báo trình biên dịch "Phương thức không đồng bộ này thiếu toán tử 'chờ đợi' và sẽ chạy đồng bộ ..."

Tôi hiểu lý do tại sao tôi nhận được lỗi nhưng đang tự hỏi liệu tôi có nên làm bất cứ điều gì với chúng trong bối cảnh này hay không. Thật sai lầm khi bỏ qua cảnh báo của trình biên dịch.

Tôi biết mình có thể sửa nó bằng cách đợi Task.Run nhưng điều đó cảm thấy không ổn đối với một phương pháp chỉ thực hiện một vài thao tác rẻ tiền. Nó cũng có vẻ như nó sẽ thêm chi phí không cần thiết vào quá trình thực thi nhưng sau đó tôi cũng không chắc liệu đã có chưa vì từ khóa async hiện diện.

Tôi có nên bỏ qua các cảnh báo hay có cách giải quyết vấn đề này mà tôi không thấy?


2
Nó sẽ phụ thuộc vào các chi tiết cụ thể. Bạn có thực sự chắc chắn muốn các thao tác này được thực hiện đồng bộ không? Nếu bạn muốn chúng được thực hiện đồng bộ, tại sao phương thức được đánh dấu là async?
Phục vụ

11
Chỉ cần xóa asynctừ khóa. Bạn vẫn có thể trả lại một Tasksử dụng Task.FromResult.
Michael Liu

1
@BenVoigt Google có đầy đủ thông tin về nó, trong trường hợp OP chưa biết.
Bữa tối

1
@BenVoigt Không phải Michael Liu đã cung cấp gợi ý đó sao? Sử dụng Task.FromResult.

1
@hvd: Điều đó đã được chỉnh sửa thành bình luận của anh ấy sau đó.
Ben Voigt

Câu trả lời:


144

Các async từ khóa chỉ là một chi tiết thi hành một phương pháp; nó không phải là một phần của chữ ký phương thức. Nếu việc triển khai hoặc ghi đè một phương pháp cụ thể không có gì để chờ đợi, thì chỉ cần bỏ qua từ khóa async và trả về một tác vụ đã hoàn thành bằng Task.FromResult <TResult> :

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

Nếu phương thức của bạn trả về Tác vụ thay vì Tác vụ <Kết quả> , thì bạn có thể trả về tác vụ đã hoàn thành thuộc bất kỳ loại và giá trị nào. Task.FromResult(0)dường như là một lựa chọn phổ biến:

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

Hoặc, kể từ .NET Framework 4.6, bạn có thể trả về Task.CompletedTask :

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

Cảm ơn Tôi nghĩ rằng một chút tôi còn thiếu là khái niệm tạo một Nhiệm vụ đã hoàn thành, chứ không phải trả lại một nhiệm vụ thực tế mà như bạn nói sẽ giống như có từ khóa async. Bây giờ có vẻ rõ ràng nhưng tôi đã không nhìn thấy nó!
dannykay1710

1
Tác vụ có thể thực hiện với một thành viên tĩnh dọc theo dòng Task.Empty cho mục đích này. Ý định sẽ rõ ràng hơn một chút và tôi thật đau lòng khi nghĩ về tất cả những Nhiệm vụ nghiêm túc này trả về một con số 0 không bao giờ cần thiết.
Rupert Rawnsley

await Task.FromResult(0)? Làm thế nào về await Task.Yield()?
Sushi 271

1
@ Sushi271: Không, trong một asyncphương pháp không phải , bạn quay lại Task.FromResult(0) thay vì đợi nó.
Michael Liu

1
Trên thực tế KHÔNG, async không chỉ là một chi tiết triển khai, có rất nhiều chi tiết xung quanh người ta phải biết :). Người ta phải biết, phần nào chạy đồng bộ, phần nào không đồng bộ, bối cảnh đồng bộ hóa hiện tại là gì và chỉ đối với bản ghi, Nhiệm vụ luôn nhanh hơn một chút, vì không có máy trạng thái phía sau rèm :).
ipavlu

16

Hoàn toàn hợp lý khi một số hoạt động "không đồng bộ" hoàn thành đồng bộ, nhưng vẫn tuân theo mô hình gọi không đồng bộ vì lợi ích của tính đa hình.

Một ví dụ thực tế về điều này là với các API I / O của hệ điều hành. Các cuộc gọi không đồng bộ và chồng chéo trên một số thiết bị luôn hoàn thành nội tuyến (ví dụ: ghi vào đường ống được triển khai bằng bộ nhớ dùng chung). Nhưng chúng thực hiện cùng một giao diện như các hoạt động nhiều phần vẫn tiếp tục trong nền.


4

Michael Liu đã trả lời tốt câu hỏi của bạn về cách bạn có thể tránh cảnh báo: bằng cách trả lại Task.FromResult.

Tôi sẽ trả lời phần "Tôi có nên lo lắng về cảnh báo" trong câu hỏi của bạn.

Câu trả lời là Có!

Lý do cho điều này là cảnh báo thường xuất hiện khi bạn gọi một phương thức trả về Taskbên trong một phương thức không đồng bộ mà không có awaittoán tử. Tôi vừa sửa một lỗi đồng thời đã xảy ra vì tôi đã gọi một thao tác trong Entity Framework mà không đợi thao tác trước đó.

Nếu bạn có thể viết mã một cách tỉ mỉ để tránh các cảnh báo của trình biên dịch, thì khi có cảnh báo, nó sẽ nổi bật lên như một ngón tay cái đau. Tôi có thể tránh được vài giờ gỡ lỗi.


5
Câu trả lời này chỉ là sai. Đây là lý do tại sao: có thể có ít nhất một awaitbên trong phương thức ở một nơi (sẽ không có CS1998) nhưng nó không có nghĩa là sẽ không có lệnh gọi nào khác của phương thức asnyc sẽ thiếu đồng bộ hóa (sử dụng awaithoặc bất kỳ phương thức nào khác). Bây giờ nếu ai đó muốn biết cách đảm bảo bạn không vô tình bỏ lỡ quá trình đồng bộ hóa thì chỉ cần đảm bảo rằng bạn không bỏ qua một cảnh báo khác - CS4014. Tôi thậm chí muốn khuyên bạn nên đe dọa rằng một trong những lỗi.
Victor Yarema

3

Có thể đã quá muộn nhưng nó có thể là một cuộc điều tra hữu ích:

Có về cấu trúc bên trong của mã đã biên dịch ( IL ):

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

nó trở thành trong 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

Và không có phương thức không đồng bộ và tác vụ:

 public static int GetTestData()
        {
            return 12;
        }

trở thành:

.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

Như bạn có thể thấy sự khác biệt lớn giữa các phương pháp này. Nếu bạn không sử dụng phương thức await bên trong async và không quan tâm đến việc sử dụng phương thức async (ví dụ: lệnh gọi API hoặc trình xử lý sự kiện), thì ý kiến ​​hay là chuyển nó sang phương thức đồng bộ thông thường (nó tiết kiệm hiệu suất ứng dụng của bạn).

Đã cập nhật:

Ngoài ra còn có thông tin bổ sung từ tài liệu microsoft https://docs.microsoft.com/en-us/dotnet/standard/async-in-depth :

các phương thức async cần phải có một từ khóa đang chờ trong nội dung của chúng nếu không chúng sẽ không bao giờ mang lại kết quả! Đây là điều quan trọng cần ghi nhớ. Nếu await không được sử dụng trong phần thân của một phương thức không đồng bộ, trình biên dịch C # sẽ tạo ra một cảnh báo, nhưng mã sẽ biên dịch và chạy như thể nó là một phương thức bình thường. Lưu ý rằng điều này cũng sẽ cực kỳ kém hiệu quả, vì máy trạng thái được tạo bởi trình biên dịch C # cho phương thức không đồng bộ sẽ không hoàn thành bất cứ điều gì.


2
Ngoài ra, kết luận cuối cùng của bạn về việc sử dụng async/awaitđược đơn giản hóa quá mức vì bạn đang dựa trên ví dụ không thực tế của mình về một hoạt động duy nhất bị ràng buộc bởi CPU. Tasks khi được sử dụng đúng cách cho phép cải thiện hiệu suất và khả năng phản hồi của ứng dụng do các tác vụ đồng thời (tức là song song) và quản lý và sử dụng các luồng tốt hơn
MickyD

Đó chỉ là ví dụ đơn giản thử nghiệm như tôi đã nói trong bài đăng này. Ngoài ra, tôi đã đề cập đến các yêu cầu đối với api và trình xử lý sự kiện khi có thể sử dụng cả hai phiên bản phương thức (không đồng bộ và thông thường). PO cũng nói về việc sử dụng các phương thức không đồng bộ mà không cần chờ đợi bên trong. Bài viết của tôi là về nó nhưng không phải về cách sử dụng đúng cách Tasks. Đó là một câu chuyện đáng buồn khi bạn không đọc toàn bộ nội dung của bài đăng và kết luận nhanh chóng.
Oleg Bondarenko

1
Có sự khác biệt giữa một phương thức trả về int(như trong trường hợp của bạn) và một phương thức trả về Taskchẳng hạn như đã được OP thảo luận. Đọc của ông bưu chính, câu trả lời được chấp nhận một lần nữa thay vì dùng những thứ cá nhân. Câu trả lời của bạn không hữu ích trong trường hợp này. Bạn thậm chí không bận tâm đến sự khác biệt giữa một phương pháp có awaitbên trong hay không. Bây giờ bạn đã làm được điều đó sẽ rất tốt rất đáng được
ủng hộ

Tôi đoán bạn thực sự không hiểu sự khác biệt giữa phương thức async và phương thức thông thường được gọi với api hoặc trình xử lý sự kiện. Nó đã được đề cập đặc biệt trong bài đăng của tôi. Xin lỗi vì bạn đang thiếu đó một lần nữa .
Oleg Bondarenko

1

Lưu ý về hành vi ngoại lệ khi quay lại Task.FromResult

Đây là một bản demo nhỏ cho thấy sự khác biệt trong việc xử lý ngoại lệ giữa các phương pháp được đánh dấu và không được đánh dấu bằng 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.

(Bài đăng chéo câu trả lời của tôi cho Khi không đồng bộ Tác vụ <T> theo yêu cầu của giao diện, cách lấy biến trả về mà không có cảnh báo trình biên dịch )

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.