async / await - khi nào trả lại một Nhiệm vụ so với void?


503

Theo kịch bản nào người ta sẽ muốn sử dụng

public async Task AsyncMethod(int num)

thay vì

public async void AsyncMethod(int num)

Kịch bản duy nhất mà tôi có thể nghĩ đến là nếu bạn cần nhiệm vụ để có thể theo dõi tiến trình của nó.

Ngoài ra, trong phương pháp sau, có phải là async và đang chờ các từ khóa không cần thiết không?

public static async void AsyncMethod2(int num)
{
    await Task.Factory.StartNew(() => Thread.Sleep(num));
}

20
Lưu ý rằng các phương thức async phải luôn luôn được gắn với tên Async.Example Foo()sẽ trở thành FooAsync().
Fred

30
@Fred Chủ yếu, nhưng không phải lúc nào. Đây chỉ là quy ước và các ngoại lệ được chấp nhận cho quy ước này là với các lớp dựa trên sự kiện hoặc hợp đồng giao diện, xem MSDN . Ví dụ: bạn không nên đổi tên các trình xử lý sự kiện phổ biến, chẳng hạn như Nút1_Click.
Ben

14
Thay vào đó chỉ là một lưu ý bạn không nên sử dụng Thread.Sleepvới các nhiệm vụ của mình mà bạn nên sử dụngawait Task.Delay(num)
Bob Vale

45
@fred Tôi không đồng ý với điều này, IMO thêm hậu tố async chỉ nên được sử dụng khi bạn đang cung cấp giao diện với cả tùy chọn đồng bộ hóa và không đồng bộ. Smurf đặt tên mọi thứ với async khi chỉ có một ý định là vô nghĩa. Trường hợp cụ Task.Delaythể không phải Task.AsyncDelaylà tất cả các phương thức trong nhiệm vụ là Async
Không được yêu thích vào

11
Tôi đã có một vấn đề thú vị sáng nay với phương pháp điều khiển webapi 2, nó đã được khai báo async voidthay thế async Task. Phương thức bị lỗi vì nó đang sử dụng một đối tượng bối cảnh Entity Framework được khai báo là thành viên của bộ điều khiển đã bị loại bỏ trước khi phương thức kết thúc để thực thi. Khung xử lý bộ điều khiển trước khi phương thức của nó kết thúc để thực thi. Tôi đã thay đổi phương thức thành Nhiệm vụ không đồng bộ và nó đã hoạt động.
costa

Câu trả lời:


417

1) Thông thường, bạn sẽ muốn trả lại a Task. Ngoại lệ chính phải là khi bạn cầnvoidkiểu trả về (cho các sự kiện). Nếu không có lý do gì để không cho phép người gọi awaitthực hiện nhiệm vụ của bạn, tại sao lại không cho phép?

2) asynccác phương thức trả về voidlà đặc biệt ở một khía cạnh khác: chúng đại diện cho các hoạt động không đồng bộ cấp cao nhất và có các quy tắc bổ sung có hiệu lực khi tác vụ của bạn trả về một ngoại lệ. Cách dễ nhất là thể hiện sự khác biệt bằng một ví dụ:

static async void f()
{
    await h();
}

static async Task g()
{
    await h();
}

static async Task h()
{
    throw new NotImplementedException();
}

private void button1_Click(object sender, EventArgs e)
{
    f();
}

private void button2_Click(object sender, EventArgs e)
{
    g();
}

private void button3_Click(object sender, EventArgs e)
{
    GC.Collect();
}

fNgoại lệ luôn được "quan sát". Một ngoại lệ để lại một phương thức không đồng bộ cấp cao nhất được xử lý đơn giản như bất kỳ ngoại lệ chưa được xử lý nào khác. gNgoại lệ không bao giờ được quan sát. Khi trình thu gom rác đến để dọn dẹp tác vụ, nó thấy rằng tác vụ dẫn đến một ngoại lệ và không ai xử lý ngoại lệ đó. Khi điều đó xảy ra, TaskScheduler.UnobservedTaskExceptionxử lý chạy. Bạn không bao giờ nên để điều này xảy ra. Để sử dụng ví dụ của bạn,

public static async void AsyncMethod2(int num)
{
    await Task.Factory.StartNew(() => Thread.Sleep(num));
}

Có, sử dụng asyncawaitở đây, họ đảm bảo phương pháp của bạn vẫn hoạt động chính xác nếu ném ngoại lệ.

để biết thêm thông tin, hãy xem: http://msdn.microsoft.com/en-us/magazine/jj991977.aspx


10
Ý tôi là fthay vì gtrong bình luận của tôi. Ngoại lệ từ fđược truyền cho SynchronizationContext. gsẽ nâng lên UnobservedTaskException, nhưng UTEkhông còn làm hỏng quá trình nếu nó không được xử lý. Có một số tình huống có thể chấp nhận "ngoại lệ không đồng bộ" như thế này bị bỏ qua.
Stephen Cleary

3
Nếu bạn có WhenAnynhiều Tasks dẫn đến ngoại lệ. Bạn thường chỉ phải xử lý cái đầu tiên, và bạn thường muốn bỏ qua những cái khác.
Stephen Cleary

1
@StephenCleary Cảm ơn, tôi đoán đó là một ví dụ hay, mặc dù điều này phụ thuộc vào lý do bạn gọi WhenAnyngay từ đầu liệu có ổn không nếu bỏ qua các trường hợp ngoại lệ khác: trường hợp sử dụng chính tôi có cho nó vẫn kết thúc chờ các nhiệm vụ còn lại khi kết thúc , có hoặc không có ngoại lệ.

10
Tôi hơi bối rối về lý do tại sao bạn khuyên bạn nên trả lại một Nhiệm vụ thay vì khoảng trống. Giống như bạn đã nói, f () sẽ ném và ngoại lệ nhưng g () sẽ không. Không phải là tốt nhất để được nhận thức về các ngoại lệ chủ đề nền?
dùng981225

2
@ user981225 Thật vậy, nhưng điều đó trở thành trách nhiệm của người gọi g: mọi phương thức gọi g nên không đồng bộ và sử dụng cũng đang chờ. Đây là một hướng dẫn, không phải là một quy tắc cứng, bạn có thể quyết định rằng trong chương trình cụ thể của mình, việc g trở lại khoảng trống sẽ dễ dàng hơn.

40

Tôi đã xem qua bài viết rất hữu ích này về asyncvoidđược viết bởi Jérôme Laban: https://jaylee.org/archive/2012/07/08/c-sharp-async-tips-and-tricks-part-2-async-void .html

Điểm mấu chốt là async+voidcó thể làm sập hệ thống và thường chỉ nên được sử dụng trên các trình xử lý sự kiện phía UI.

Lý do đằng sau điều này là Bối cảnh đồng bộ hóa được sử dụng bởi AsyncVoidMethodBuilder, không có trong ví dụ này. Khi không có Bối cảnh đồng bộ hóa xung quanh, bất kỳ ngoại lệ nào được xử lý bằng phần thân của phương thức void async sẽ được lưu lại trên ThreadPool. Mặc dù dường như không có nơi logic nào khác có thể ném loại ngoại lệ chưa được xử lý đó, nhưng hậu quả đáng tiếc là quá trình này đang bị chấm dứt, bởi vì các ngoại lệ chưa được xử lý trên ThreadPool chấm dứt hiệu quả quá trình kể từ .NET 2.0. Bạn có thể chặn tất cả ngoại lệ chưa được xử lý bằng cách sử dụng sự kiện AppDomain.UnhandledException, nhưng không có cách nào để khôi phục quy trình từ sự kiện này.

Khi viết trình xử lý sự kiện UI, các phương thức void async bằng cách nào đó không gây đau đớn vì các ngoại lệ được xử lý theo cách tương tự được tìm thấy trong các phương thức không đồng bộ; họ bị ném vào người điều phối. Có khả năng phục hồi từ các trường hợp ngoại lệ như vậy, với nhiều hơn là chính xác cho hầu hết các trường hợp. Tuy nhiên, bên ngoài các trình xử lý sự kiện UI, các phương thức void async bằng cách nào đó rất nguy hiểm khi sử dụng và có thể không dễ tìm thấy.


Đúng không? Tôi nghĩ rằng các ngoại lệ bên trong các phương thức async được ghi lại trong Nhiệm vụ. Và cũng luôn luôn có Đồng bộ hóa điếc liên kết
NM

30

Tôi có ý tưởng rõ ràng từ tuyên bố này.

  1. Các phương thức void Async có ngữ nghĩa xử lý lỗi khác nhau. Khi một ngoại lệ được đưa ra khỏi phương thức Tác vụ không đồng bộ hoặc Tác vụ không đồng bộ, ngoại lệ đó được ghi lại và đặt vào đối tượng Tác vụ. Với các phương thức void async, không có đối tượng Nhiệm vụ, do đó, bất kỳ trường hợp ngoại lệ nào được đưa ra khỏi phương thức void async sẽ được đưa ra trực tiếp trên SyncizationContext (SyncizationContext đại diện cho một vị trí "trong đó" mã có thể được thực thi khi phương thức void async đã bắt đầu

Các ngoại lệ từ một phương pháp không đồng bộ Async không thể bị bắt với

private async void ThrowExceptionAsync()
{
  throw new InvalidOperationException();
}
public void AsyncVoidExceptions_CannotBeCaughtByCatch()
{
  try
  {
    ThrowExceptionAsync();
  }
  catch (Exception)
  {
    // The exception is never caught here!
    throw;
  }
}

Có thể quan sát các ngoại lệ này bằng AppDomain.UnhandledException hoặc một sự kiện bắt tất cả tương tự cho các ứng dụng GUI / ASP.NET, nhưng sử dụng các sự kiện đó để xử lý ngoại lệ thông thường là một công thức cho tính không thể nhầm lẫn (nó làm hỏng ứng dụng).

  1. Các phương thức void Async có ngữ nghĩa sáng tác khác nhau. Các phương thức Async trả về Nhiệm vụ hoặc Tác vụ có thể được soạn thảo dễ dàng bằng cách sử dụng await, Task.WhenAny, Task.When ALL, v.v. Các phương thức Async trả về void không cung cấp một cách dễ dàng để thông báo mã cuộc gọi mà họ đã hoàn thành. Thật dễ dàng để bắt đầu một số phương thức void async, nhưng không dễ để xác định khi nào chúng kết thúc. Các phương thức void Async sẽ thông báo cho SyncizationContext của chúng khi chúng bắt đầu và kết thúc, nhưng SyncizationContext tùy chỉnh là một giải pháp phức tạp cho mã ứng dụng thông thường.

  2. Phương thức Async Void hữu ích khi sử dụng trình xử lý sự kiện đồng bộ vì chúng đưa ra các ngoại lệ của chúng trực tiếp trên SyncizationContext, tương tự như cách xử lý sự kiện đồng bộ hóa xử lý

Để biết thêm chi tiết, hãy kiểm tra liên kết này https://msdn.microsoft.com/en-us/magazine/jj991977.aspx


21

Vấn đề với việc gọi async void là bạn thậm chí không nhận lại được nhiệm vụ, bạn không có cách nào để biết khi nào nhiệm vụ của chức năng đã hoàn thành (xem https://bloss.msdn.microsoft.com/oldnewthing/20170720-00/ ? p = 96655 )

Dưới đây là ba cách để gọi hàm async:

async Task<T> SomethingAsync() { ... return t; }
async Task SomethingAsync() { ... }
async void SomethingAsync() { ... }

Trong tất cả các trường hợp, hàm được chuyển thành một chuỗi các tác vụ. Sự khác biệt là những gì hàm trả về.

Trong trường hợp đầu tiên, hàm trả về một tác vụ cuối cùng tạo ra t.

Trong trường hợp thứ hai, hàm trả về một tác vụ không có sản phẩm, nhưng bạn vẫn có thể chờ đợi để biết khi nào nó chạy đến khi hoàn thành.

Trường hợp thứ ba là trường hợp khó chịu. Trường hợp thứ ba giống như trường hợp thứ hai, ngoại trừ việc bạn thậm chí không nhận lại nhiệm vụ. Bạn không có cách nào để biết khi nào nhiệm vụ của chức năng đã hoàn thành.

Trường hợp khoảng trống async là "lửa và quên": Bạn bắt đầu chuỗi nhiệm vụ, nhưng bạn không quan tâm đến khi nào nó kết thúc. Khi hàm trả về, tất cả những gì bạn biết là mọi thứ cho đến khi chờ đợi đầu tiên đã được thực thi. Tất cả mọi thứ sau lần chờ đợi đầu tiên sẽ chạy ở một số điểm không xác định trong tương lai mà bạn không có quyền truy cập.


6

Tôi nghĩ bạn cũng có thể sử dụng async voidđể khởi động các hoạt động nền, miễn là bạn cẩn thận để bắt ngoại lệ. Suy nghĩ?

class Program {

    static bool isFinished = false;

    static void Main(string[] args) {

        // Kick off the background operation and don't care about when it completes
        BackgroundWork();

        Console.WriteLine("Press enter when you're ready to stop the background operation.");
        Console.ReadLine();
        isFinished = true;
    }

    // Using async void to kickoff a background operation that nobody wants to be notified about when it completes.
    static async void BackgroundWork() {
        // It's important to catch exceptions so we don't crash the appliation.
        try {
            // This operation will end after ten interations or when the app closes. Whichever happens first.
            for (var count = 1; count <= 10 && !isFinished; count++) {
                await Task.Delay(1000);
                Console.WriteLine($"{count} seconds of work elapsed.");
            }
            Console.WriteLine("Background operation came to an end.");
        } catch (Exception x) {
            Console.WriteLine("Caught exception:");
            Console.WriteLine(x.ToString());
        }
    }
}

1
Vấn đề với việc gọi async void là bạn thậm chí không nhận lại được nhiệm vụ, bạn không có cách nào để biết khi nào nhiệm vụ của chức năng đã hoàn thành
user8128167

Điều này có ý nghĩa đối với các hoạt động nền (hiếm) mà bạn không quan tâm đến kết quả và bạn không muốn có ngoại lệ ảnh hưởng đến các hoạt động khác. Một trường hợp sử dụng thông thường sẽ gửi một sự kiện nhật ký đến một máy chủ nhật ký. Bạn muốn điều này xảy ra trong nền và bạn không muốn trình xử lý dịch vụ / yêu cầu của bạn bị lỗi nếu máy chủ đăng nhập bị hỏng. Tất nhiên, bạn phải nắm bắt tất cả các ngoại lệ, nếu không quy trình của bạn sẽ bị chấm dứt. Hoặc có một cách tốt hơn để đạt được điều này?
Mùa đông

Các ngoại lệ từ Phương pháp không đồng bộ không đồng bộ không thể bị bắt. msdn.microsoft.com/en-us/magazine/jj991977.aspx
Si Zi

3
@SiZi, phần bắt được ẩn bên trong phương thức void async như trong ví dụ và nó được bắt.
bboyle1234

Điều gì sẽ xảy ra khi yêu cầu của bạn thay đổi thành yêu cầu không thực hiện công việc nếu bạn không thể đăng nhập nó - sau đó bạn phải thay đổi chữ ký trong toàn bộ API của mình, thay vì chỉ thay đổi logic hiện có // Bỏ qua Ngoại lệ
Milney

0

Câu trả lời của tôi rất đơn giản, bạn không thể chờ đợi phương thức void

Error   CS4008  Cannot await 'void' TestAsync   e:\test\TestAsync\TestAsyncProgram.cs

Vì vậy, nếu phương thức không đồng bộ, tốt hơn là nên chờ đợi, bởi vì bạn có thể mất lợi thế không đồng bộ.


-2

Theo tài liệu của Microsoft , KHÔNG BAO GIỜ nên sử dụngasync void

Không làm điều này: Ví dụ sau sử dụng async voidlàm cho yêu cầu HTTP hoàn thành khi đạt được sự chờ đợi đầu tiên:

  • Đó là LUÔN là một thực tiễn xấu trong các ứng dụng ASP.NET Core.

  • Truy cập httpResponse sau khi yêu cầu HTTP hoàn tất.

  • Sự cố quá trình.

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.