Cách gọi phương thức async một cách an toàn trong C # mà không phải chờ đợi


322

Tôi có một asyncphương thức không trả về dữ liệu:

public async Task MyAsyncMethod()
{
    // do some stuff async, don't return any data
}

Tôi đang gọi điều này từ một phương thức khác trả về một số dữ liệu:

public string GetStringData()
{
    MyAsyncMethod(); // this generates a warning and swallows exceptions
    return "hello world";
}

Gọi MyAsyncMethod()mà không chờ nó gây ra cảnh báo " Vì cuộc gọi này không được chờ, nên phương thức hiện tại tiếp tục chạy trước khi cuộc gọi được hoàn thành " trong phòng thu trực quan. Trên trang cảnh báo đó có ghi:

Bạn chỉ nên xem xét loại bỏ cảnh báo nếu bạn chắc chắn rằng bạn không muốn đợi cuộc gọi không đồng bộ hoàn tất và phương thức được gọi sẽ không đưa ra bất kỳ ngoại lệ nào .

Tôi chắc chắn rằng tôi không muốn đợi cuộc gọi hoàn tất; Tôi không cần hoặc có thời gian. Nhưng cuộc gọi có thể đưa ra ngoại lệ.

Tôi đã vấp phải vấn đề này một vài lần và tôi chắc chắn đó là một vấn đề phổ biến phải có giải pháp chung.

Làm cách nào để gọi phương thức async một cách an toàn mà không cần chờ kết quả?

Cập nhật:

Đối với những người đề xuất rằng tôi chỉ chờ kết quả, đây là mã đáp ứng yêu cầu web trên dịch vụ web của chúng tôi (ASP.NET Web API). Chờ đợi trong ngữ cảnh UI sẽ giữ luồng UI miễn phí, nhưng chờ trong cuộc gọi yêu cầu web sẽ đợi Tác vụ kết thúc trước khi trả lời yêu cầu, do đó tăng thời gian phản hồi mà không có lý do.


Tại sao không chỉ tạo một phương thức hoàn thành và chỉ cần bỏ qua nó ở đó? Bởi vì nếu nó đang chạy trên chủ đề nền. Sau đó, nó sẽ không dừng chương trình của bạn kết thúc.
Yahya

1
Nếu bạn không muốn đợi kết quả, lựa chọn duy nhất là bỏ qua / chặn cảnh báo. Nếu bạn làm muốn chờ kết quả / ngoại lệ sau đóMyAsyncMethod().Wait()
Peter Ritchie

1
Về chỉnh sửa của bạn: điều đó không có ý nghĩa với tôi. Giả sử phản hồi được gửi cho khách hàng 1 giây sau khi yêu cầu và 2 giây sau phương thức async của bạn đưa ra một ngoại lệ. Bạn sẽ làm gì với ngoại lệ đó? Bạn không thể gửi nó cho khách hàng, nếu phản hồi của bạn đã được gửi. Bạn sẽ làm gì khác với nó?

2
@Romoku Hội chợ đủ rồi. Giả sử ai đó nhìn vào nhật ký, dù sao đi nữa. :)

2
Một biến thể của kịch bản API Web ASP.NET là API Web tự lưu trữ trong một quy trình tồn tại lâu dài (như, ví dụ, một dịch vụ Windows), trong đó một yêu cầu tạo ra một tác vụ nền dài để làm một cái gì đó đắt tiền, nhưng vẫn muốn nhận được phản hồi nhanh chóng với HTTP 202 (Được chấp nhận).
David Rubin

Câu trả lời:


187

Nếu bạn muốn có ngoại lệ "không đồng bộ", bạn có thể làm:

  MyAsyncMethod().
    ContinueWith(t => Console.WriteLine(t.Exception),
        TaskContinuationOptions.OnlyOnFaulted);

Điều này sẽ cho phép bạn xử lý một ngoại lệ trên một luồng khác với luồng "chính". Điều này có nghĩa là bạn không phải "chờ" cuộc gọi đến MyAsyncMethod()từ chuỗi cuộc gọi MyAsyncMethod; nhưng, vẫn cho phép bạn làm một cái gì đó với một ngoại lệ - nhưng chỉ khi một ngoại lệ xảy ra.

Cập nhật:

về mặt kỹ thuật, bạn có thể làm một cái gì đó tương tự với await:

try
{
    await MyAsyncMethod().ConfigureAwait(false);
}
catch (Exception ex)
{
    Trace.WriteLine(ex);
}

... Sẽ hữu ích nếu bạn cần sử dụng cụ thể try/ catch(hoặc using) nhưng tôi thấy điều này ContinueWithrõ ràng hơn một chút vì bạn phải biết ý ConfigureAwait(false)nghĩa của nó.


7
Tôi đã biến phương thức này thành một phương thức mở rộng trên Task: lớp tĩnh công khai AsyncUtility {public static void PerformanceAsyncTaskWithoutAwait (tác vụ tác vụ này, Hành động <Nhiệm vụ> ngoại lệ) {var dummy = task.CContueWith (t => exHandler (t), TaskContininatingOp }} Cách sử dụng: MyAsyncMethod (). PerformanceAsyncTaskWithoutAwait (t => log.ErrorFormat ("Đã xảy ra lỗi khi gọi MyAsyncMethod: \ n {0}", t.Exception));
Đánh dấu vào

2
downvoter. Bình luận? Nếu có gì sai trong câu trả lời, tôi rất muốn biết và / hoặc sửa chữa.
Peter Ritchie

2
Xin chào - Tôi không downvote, nhưng ... Bạn có thể giải thích cập nhật của mình không? Cuộc gọi không được chờ đợi, vì vậy nếu bạn thực hiện như vậy, thì bạn chờ cuộc gọi, bạn không tiếp tục trong bối cảnh đã bắt ...
Bartosz

1
"Chờ đợi" không phải là mô tả chính xác trong bối cảnh này. Dòng sau khi ConfiguratAwait(false)không thực thi cho đến khi tác vụ hoàn thành, nhưng luồng hiện tại không "chờ" (tức là khối) cho điều đó, dòng tiếp theo được gọi không đồng bộ với lệnh gọi chờ. Nếu không có ConfigureAwait(false)dòng tiếp theo sẽ được thực thi trên bối cảnh yêu cầu web ban đầu. Với ConfigurateAwait(false)nó được thực thi trong cùng bối cảnh với phương thức (nhiệm vụ) không đồng bộ, giải phóng bối cảnh / luồng gốc để tiếp tục ...
Peter Ritchie

15
Phiên bản ContinWith không giống với phiên bản thử {await} Catch {}. Trong phiên bản đầu tiên, mọi thứ sau ContinWith () sẽ thực thi ngay lập tức. Nhiệm vụ ban đầu bị sa thải và bị lãng quên. Trong phiên bản thứ hai, mọi thứ sau khi bắt {} sẽ chỉ thực hiện sau khi hoàn thành nhiệm vụ ban đầu. Phiên bản thứ hai tương đương với "đang chờ MyAsyncMethod (). ContinWith (t => Console.WriteLine (t.Exception), TaskContininatingOptions.OnlyOnFaulted) .ConfigureAwait (fals);
Thanocation Ioannidis

67

Trước tiên bạn nên xem xét thực hiện GetStringDatamột asyncphương thức và yêu cầu nó awaitđược trả về MyAsyncMethod.

Nếu bạn hoàn toàn chắc chắn rằng bạn không cần xử lý ngoại lệ từ MyAsyncMethod hoặc biết khi nào nó hoàn thành, thì bạn có thể làm điều này:

public string GetStringData()
{
  var _ = MyAsyncMethod();
  return "hello world";
}

BTW, đây không phải là "vấn đề chung". Rất hiếm khi muốn thực thi một số mã và không quan tâm liệu nó có hoàn thành hay không không quan tâm liệu nó có hoàn thành thành công hay không.

Cập nhật:

Vì bạn đang sử dụng ASP.NET và muốn quay lại sớm, bạn có thể thấy bài đăng trên blog của tôi về chủ đề này hữu ích . Tuy nhiên, ASP.NET không được thiết kế cho việc này và không có gì đảm bảo rằng mã của bạn sẽ chạy sau khi phản hồi được trả về. ASP.NET sẽ cố hết sức để cho nó chạy, nhưng nó không thể đảm bảo điều đó.

Vì vậy, đây là một giải pháp tốt cho một cái gì đó đơn giản như ném một sự kiện vào nhật ký, nơi nó không thực sự quan trọng nếu bạn mất một vài thứ ở đây và ở đó. Nó không phải là một giải pháp tốt cho bất kỳ loại hoạt động kinh doanh quan trọng nào. Trong các tình huống đó, bạn phải áp dụng một kiến ​​trúc phức tạp hơn, với một cách liên tục để lưu các hoạt động (ví dụ: Hàng đợi Azure, MSMQ) và một quy trình nền riêng (ví dụ: Vai trò công nhân Azure, Dịch vụ Win32) để xử lý chúng.


2
Tôi nghĩ rằng bạn có thể đã hiểu lầm. Tôi làm chăm sóc nếu nó ném ngoại lệ và thất bại, nhưng tôi không muốn phải chờ đợi phương pháp này trước khi trở về dữ liệu của tôi. Cũng xem bản chỉnh sửa của tôi về bối cảnh tôi đang làm việc nếu điều đó tạo ra sự khác biệt.
George Powell

5
@GeorgePowell: Sẽ rất nguy hiểm khi có mã chạy trong ngữ cảnh ASP.NET mà không có yêu cầu hoạt động. Tôi có một bài đăng blog có thể giúp bạn hiểu , nhưng không biết thêm về vấn đề của bạn, tôi không thể nói liệu tôi có đề xuất phương pháp đó hay không.
Stephen Cleary

@StephenCleary Tôi có một nhu cầu tương tự. Trong ví dụ của tôi, tôi có / cần một công cụ xử lý hàng loạt để chạy trong đám mây, tôi sẽ "ping" điểm cuối để khởi động quá trình xử lý hàng loạt, nhưng tôi muốn quay lại ngay lập tức. Kể từ khi ping nó bắt đầu, nó có thể xử lý mọi thứ từ đó. Nếu có trường hợp ngoại lệ bị ném, thì chúng sẽ được ghi vào bảng "BatchProcessLog / Error" của tôi ...
ganders

8
Trong C # 7, bạn có thể thay thế var _ = MyAsyncMethod();bằng _ = MyAsyncMethod();. Điều này vẫn tránh cảnh báo CS4014, nhưng nó làm cho rõ ràng hơn một chút rằng bạn không sử dụng biến.
Brian

48

Câu trả lời của Peter Ritchie là những gì tôi muốn, và bài viết của Stephen Cleary về việc trở lại sớm trong ASP.NET rất hữu ích.

Tuy nhiên, là một vấn đề chung hơn (không cụ thể đối với bối cảnh ASP.NET), ứng dụng Console sau đây thể hiện cách sử dụng và hành vi trả lời của Peter's bằng cách sử dụng Task.ContinueWith(...)

static void Main(string[] args)
{
  try
  {
    // output "hello world" as method returns early
    Console.WriteLine(GetStringData());
  }
  catch
  {
    // Exception is NOT caught here
  }
  Console.ReadLine();
}

public static string GetStringData()
{
  MyAsyncMethod().ContinueWith(OnMyAsyncMethodFailed, TaskContinuationOptions.OnlyOnFaulted);
  return "hello world";
}

public static async Task MyAsyncMethod()
{
  await Task.Run(() => { throw new Exception("thrown on background thread"); });
}

public static void OnMyAsyncMethodFailed(Task task)
{
  Exception ex = task.Exception;
  // Deal with exceptions here however you want
}

GetStringData()trở về sớm mà không cần chờ đợi MyAsyncMethod()và các ngoại lệ được đưa vào MyAsyncMethod()được xử lý trong OnMyAsyncMethodFailed(Task task)khôngtry/ catchxung quanhGetStringData()


9
Xóa Console.ReadLine();và thêm một chút ngủ / trì hoãn MyAsyncMethodvà bạn sẽ không bao giờ thấy ngoại lệ.
tymtam

17

Tôi kết thúc với giải pháp này:

public async Task MyAsyncMethod()
{
    // do some stuff async, don't return any data
}

public string GetStringData()
{
    // Run async, no warning, exception are catched
    RunAsync(MyAsyncMethod()); 
    return "hello world";
}

private void RunAsync(Task task)
{
    task.ContinueWith(t =>
    {
        ILog log = ServiceLocator.Current.GetInstance<ILog>();
        log.Error("Unexpected Error", t.Exception);

    }, TaskContinuationOptions.OnlyOnFaulted);
}

8

Điều này được gọi là lửa và quên, và có một phần mở rộng cho điều đó.

Tiêu thụ một nhiệm vụ và không làm gì với nó. Hữu ích cho các cuộc gọi cháy và quên đối với các phương thức async trong các phương thức async.

Cài đặt gói nuget .

Sử dụng:

MyAsyncMethod().Forget();

12
Nó không làm gì cả, nó chỉ là một mẹo để loại bỏ cảnh báo. Xem github.com/microsoft/vs-threading/blob/master/src/ Khăn
Paolo Fulgoni

2

Tôi đoán câu hỏi phát sinh, tại sao bạn cần phải làm điều này? Lý do asynctrong C # 5.0 là vì vậy bạn có thể chờ kết quả. Phương thức này không thực sự không đồng bộ, nhưng chỉ đơn giản được gọi tại một thời điểm để không can thiệp quá nhiều vào luồng hiện tại.

Có lẽ tốt hơn là bắt đầu một chủ đề và để nó tự hoàn thành.


2
asynclà một chút nhiều hơn chỉ là "chờ đợi" một kết quả. "Await" ngụ ý rằng các dòng sau "await" được thực thi không đồng bộ trên cùng một luồng đã gọi "await". Điều này có thể được thực hiện mà không cần "chờ đợi", tất nhiên, nhưng cuối cùng bạn có một loạt các đại biểu và mất giao diện liên tiếp của mã (cũng như khả năng sử dụng usingtry/catch...
Peter Ritchie

1
@PeterRitchie Bạn có thể có một phương thức không đồng bộ về mặt chức năng, không sử dụng awaittừ khóa và không sử dụng asynctừ khóa, nhưng không có lý do gì khi sử dụng asynctừ khóa mà không sử dụng awaitđịnh nghĩa của phương pháp đó.
Phục vụ

1
@PeterRitchie Từ tuyên bố: " asyncnhiều hơn một chút so với chỉ" chờ đợi "một kết quả." Các asynctừ khóa (bạn ngụ ý đó là từ khóa bằng cách bao quanh nó trong backticks) có nghĩa là không có gì hơn đang chờ kết quả. Đó là sự không đồng bộ, như các khái niệm CS chung, điều đó có nghĩa nhiều hơn là chỉ chờ kết quả.
Phục vụ

1
@Servy asynctạo ra một máy trạng thái quản lý mọi sự chờ đợi trong phương thức async. Nếu không có awaits trong phương thức, nó vẫn tạo ra máy trạng thái đó - nhưng phương thức này không đồng bộ. Và nếu asyncphương thức trở lại void, không có gì để chờ đợi. Vì vậy, nó là nhiều hơn chỉ là chờ đợi một kết quả.
Peter Ritchie

1
@PeterRitchie Miễn là phương thức trả về một nhiệm vụ bạn có thể chờ đợi nó. Không cần cho máy trạng thái, hoặc asynctừ khóa. Tất cả những gì bạn cần làm (và tất cả những gì thực sự xảy ra cuối cùng với một máy trạng thái trong trường hợp đặc biệt đó) là phương thức được chạy đồng bộ và sau đó được bọc trong một tác vụ hoàn thành. Tôi cho rằng về mặt kỹ thuật bạn không chỉ cần loại bỏ máy trạng thái; bạn gỡ máy trạng thái rồi gọi Task.FromResult. Tôi giả sử bạn (và cả người viết trình biên dịch) có thể tự thêm phần phụ lục.
Phục vụ

0

Trên các công nghệ có các vòng lặp thông báo (không chắc chắn nếu ASP là một trong số chúng), bạn có thể chặn vòng lặp và xử lý tin nhắn cho đến khi tác vụ kết thúc và sử dụng ContinWith để bỏ chặn mã:

public void WaitForTask(Task task)
{
    DispatcherFrame frame = new DispatcherFrame();
    task.ContinueWith(t => frame.Continue = false));
    Dispatcher.PushFrame(frame);
}

Cách tiếp cận này tương tự như chặn trên ShowDialog và vẫn giữ cho UI phản hồi.


0

Tôi đến bữa tiệc muộn ở đây, nhưng có một thư viện tuyệt vời mà tôi đang sử dụng mà tôi chưa thấy được tham khảo trong các câu trả lời khác

https://github.com/brminnick/AsyncAwaitBestPractices

Nếu bạn cần "Bắn và quên", bạn gọi phương thức mở rộng trên tác vụ.

Truyền hành động onException vào cuộc gọi đảm bảo rằng bạn có được cả hai thế giới tốt nhất - không cần chờ đợi thực thi và làm chậm người dùng của bạn, trong khi vẫn giữ được khả năng xử lý ngoại lệ một cách duyên dáng.

Trong ví dụ của bạn, bạn sẽ sử dụng nó như thế này:

   public string GetStringData()
    {
        MyAsyncMethod().SafeFireAndForget(onException: (exception) =>
                    {
                      //DO STUFF WITH THE EXCEPTION                    
                    }); 
        return "hello world";
    }

Nó cũng cung cấp cho AsyncCommands đang chờ triển khai ICommand và đây là giải pháp tuyệt vời cho giải pháp MVVM Xamarin của tôi


-1

Giải pháp là khởi động HTTPClient vào một tác vụ thực thi khác mà không cần bối cảnh hóa.

var submit = httpClient.PostAsync(uri, new StringContent(body, Encoding.UTF8,"application/json"));
var t = Task.Run(() => submit.ConfigureAwait(false));
await t.ConfigureAwait(false);

-1

Nếu bạn thực sự muốn làm điều này. Chỉ cần giải quyết "Gọi một phương thức async trong C # mà không phải chờ đợi", bạn có thể thực thi phương thức async bên trong a Task.Run. Cách tiếp cận này sẽ đợi cho đến khi MyAsyncMethodkết thúc.

public string GetStringData()
{
    Task.Run(()=> MyAsyncMethod()).Result;
    return "hello world";
}

awaitkhông đồng bộ hủy bỏ Resultnhiệm vụ của bạn, trong khi chỉ cần sử dụng Kết quả sẽ chặn cho đến khi nhiệm vụ hoàn thành.


-1

Thông thường phương thức async trả về lớp Nhiệm vụ. Nếu bạn sử dụng Wait()phương thức hoặc thuộc Resulttính và mã ném ngoại lệ - loại ngoại lệ được gói vào AggregateException- thì bạn cần truy vấn Exception.InnerExceptionđể xác định ngoại lệ chính xác.

Nhưng .GetAwaiter().GetResult()thay vào đó , nó cũng có thể được sử dụng - nó cũng sẽ chờ tác vụ không đồng bộ, nhưng sẽ không bao gồm ngoại lệ.

Vì vậy, đây là ví dụ ngắn:

public async Task MyMethodAsync()
{
}

public string GetStringData()
{
    MyMethodAsync().GetAwaiter().GetResult();
    return "test";
}

Bạn cũng có thể muốn có thể trả về một số tham số từ chức năng async - có thể đạt được bằng cách cung cấp thêm Action<return type>vào chức năng async, ví dụ như sau:

public string GetStringData()
{
    return MyMethodWithReturnParameterAsync().GetAwaiter().GetResult();
}

public async Task<String> MyMethodWithReturnParameterAsync()
{
    return "test";
}

Xin lưu ý rằng các phương thức async thường có ASyncđặt tên hậu tố, để có thể tránh xung đột giữa các chức năng đồng bộ hóa có cùng tên. (Ví dụ FileStream.ReadAsync) - Tôi đã cập nhật tên hàm để làm theo khuyến nghị này.

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.