Ví dụ về async / await gây ra bế tắc


94

Tôi đã xem qua một số phương pháp hay nhất để lập trình không đồng bộ bằng cách sử dụng c #'s async/ awaitkeywords (tôi mới sử dụng c # 5.0).

Một trong những lời khuyên được đưa ra là:

Tính ổn định: Biết bối cảnh đồng bộ hóa của bạn

... Một số ngữ cảnh đồng bộ hóa là không thể nhập lại và đơn luồng. Điều này có nghĩa là chỉ một đơn vị công việc có thể được thực thi trong ngữ cảnh tại một thời điểm nhất định. Ví dụ về điều này là chuỗi giao diện người dùng Windows hoặc ngữ cảnh yêu cầu ASP.NET. Trong các bối cảnh đồng bộ hóa đơn luồng này, bạn rất dễ tự bế tắc. Nếu bạn sinh ra một nhiệm vụ từ ngữ cảnh đơn luồng, sau đó đợi tác vụ đó trong ngữ cảnh, mã chờ của bạn có thể đang chặn tác vụ nền.

public ActionResult ActionAsync()
{
    // DEADLOCK: this blocks on the async task
    var data = GetDataAsync().Result;

    return View(data);
}

private async Task<string> GetDataAsync()
{
    // a very simple async method
    var result = await MyWebService.GetDataAsync();
    return result.ToString();
}

Nếu tôi cố gắng tự mình mổ xẻ nó, luồng chính sẽ sinh ra một luồng mới MyWebService.GetDataAsync();, nhưng vì luồng chính đang đợi ở đó, nó sẽ đợi kết quả trong GetDataAsync().Result. Trong khi đó, nói rằng dữ liệu đã sẵn sàng. Tại sao chuỗi chính không tiếp tục nó là logic tiếp tục và trả về kết quả chuỗi từ đó GetDataAsync()?

Ai đó có thể vui lòng giải thích cho tôi tại sao có một bế tắc trong ví dụ trên không? Tôi hoàn toàn không biết vấn đề là gì ...


Bạn có thực sự chắc chắn rằng GetDataAsync đã hoàn thành mọi thứ không? Hoặc nó bị kẹt gây ra chỉ khóa và không khóa?
Andrey

Đây là ví dụ đã được cung cấp. Theo sự hiểu biết của tôi, nó sẽ hoàn thành mọi thứ và có một kết quả của một số loại đã sẵn sàng ...
Dror Weiss

4
Tại sao bạn thậm chí còn chờ đợi nhiệm vụ? Thay vào đó, bạn nên chờ đợi vì về cơ bản bạn đã mất tất cả các lợi ích của mô hình không đồng bộ.
Toni Petrina

Để thêm vào quan điểm của @ ToniPetrina, ngay cả khi vấn đề bế tắc, var data = GetDataAsync().Result;là một dòng mã không bao giờ được thực hiện trong bối cảnh mà bạn không nên chặn (yêu cầu giao diện người dùng hoặc ASP.NET). Ngay cả khi nó không bị bế tắc, nó đang chặn luồng một khoảng thời gian không xác định. Vì vậy, về cơ bản nó là một ví dụ khủng khiếp. [Bạn cần phải nhận tắt của thread UI trước khi thực thi mã như vậy, hoặc sử dụng await. Cũng có, như Toni gợi ý]
ToolmakerSteve

Câu trả lời:


81

Hãy xem ví dụ này , Stephen có câu trả lời rõ ràng cho bạn:

Vì vậy, đây là những gì sẽ xảy ra, bắt đầu với phương thức cấp cao nhất ( Button1_Clickcho UI / MyController.Getcho ASP.NET):

  1. Phương thức cấp cao nhất gọi GetJsonAsync(trong ngữ cảnh UI / ASP.NET).

  2. GetJsonAsyncbắt đầu yêu cầu REST bằng cách gọi HttpClient.GetStringAsync(vẫn trong ngữ cảnh).

  3. GetStringAsynctrả về một kết quả chưa hoàn thành Task, cho biết yêu cầu REST chưa hoàn thành.

  4. GetJsonAsyncđang chờ Tasktrả lại bởi GetStringAsync. Bối cảnh được ghi lại và sẽ được sử dụng để tiếp tục chạy GetJsonAsyncphương thức sau này. GetJsonAsynctrả về một chưa hoàn thành Task, cho biết rằng GetJsonAsyncphương thức này chưa hoàn thành.

  5. Phương thức cấp cao nhất chặn đồng bộ trên Tasktrả về của GetJsonAsync. Điều này chặn chuỗi ngữ cảnh.

  6. ... Cuối cùng, yêu cầu REST sẽ hoàn tất. Điều này hoàn thành những Taskgì được trả lại bởi GetStringAsync.

  7. Phần tiếp tục cho GetJsonAsynchiện đã sẵn sàng để chạy và nó đợi ngữ cảnh có sẵn để có thể thực thi trong ngữ cảnh.

  8. Bế tắc . Phương thức cấp cao nhất đang chặn chuỗi ngữ cảnh, chờ GetJsonAsynchoàn thành và GetJsonAsyncđang đợi ngữ cảnh rảnh để nó có thể hoàn thành. Đối với ví dụ về giao diện người dùng, "ngữ cảnh" là bối cảnh giao diện người dùng; đối với ví dụ ASP.NET, "ngữ cảnh" là ngữ cảnh yêu cầu ASP.NET. Loại bế tắc này có thể được gây ra cho cả hai "ngữ cảnh".

Một liên kết khác mà bạn nên đọc: Await, UI, and deadlocks! Ôi trời!


20
  • Sự thật 1: GetDataAsync().Result;sẽ chạy khi nhiệm vụ được trả về GetDataAsync()hoàn thành, trong khi đó nó sẽ chặn chuỗi giao diện người dùng
  • Sự thật 2: Sự tiếp tục của await ( return result.ToString()) được xếp hàng đợi vào chuỗi giao diện người dùng để thực thi
  • Sự thật 3: Nhiệm vụ được trả về GetDataAsync()sẽ hoàn thành khi phần tiếp tục được xếp hàng đợi của nó được chạy
  • Sự thật 4: Sự tiếp tục trong hàng đợi không bao giờ được chạy, vì chuỗi giao diện người dùng bị chặn (Sự thật 1)

Bế tắc!

Bế tắc có thể được phá vỡ bằng các giải pháp thay thế được cung cấp để tránh Sự thật 1 hoặc Sự thật 2.

  • Tránh 1,4. Thay vì chặn chuỗi giao diện người dùng, hãy sử dụng var data = await GetDataAsync(), điều này cho phép chuỗi giao diện người dùng tiếp tục chạy
  • Tránh 2,3. Xếp hàng đợi sự tiếp tục của sự chờ đợi đến một luồng khác không bị chặn, ví dụ: sử dụng var data = Task.Run(GetDataAsync).Result, sẽ đăng phần tiếp theo vào ngữ cảnh đồng bộ của một luồng chia sẻ. Điều này cho phép hoàn thành nhiệm vụ được trả về bởi GetDataAsync().

Điều này được giải thích rất rõ trong một bài báo của Stephen Toub , khoảng nửa chặng đường mà anh ấy sử dụng ví dụ về DelayAsync().


Về vấn đề, var data = Task.Run(GetDataAsync).Resultđó là điều mới mẻ đối với tôi. Tôi luôn nghĩ rằng bên ngoài .Resultsẽ có sẵn ngay sau khi phần chờ đợi đầu tiên GetDataAsyncđược đánh trúng, vì vậy datasẽ luôn như vậy default. Hấp dẫn.
nawfal

19

Tôi vừa loay hoay với vấn đề này một lần nữa trong một dự án ASP.NET MVC. Khi bạn muốn gọi asynccác phương thức từ a PartialView, bạn không được phép thực hiện PartialView async. Bạn sẽ nhận được một ngoại lệ nếu bạn làm vậy.

Bạn có thể sử dụng cách giải quyết đơn giản sau trong trường hợp bạn muốn gọi một asyncphương thức từ phương thức đồng bộ hóa:

  1. Trước cuộc gọi, hãy xóa SynchronizationContext
  2. Thực hiện cuộc gọi, sẽ không còn bế tắc ở đây, hãy đợi nó kết thúc
  3. Khôi phục lại SynchronizationContext

Thí dụ:

public ActionResult DisplayUserInfo(string userName)
{
    // trick to prevent deadlocks of calling async method 
    // and waiting for on a sync UI thread.
    var syncContext = SynchronizationContext.Current;
    SynchronizationContext.SetSynchronizationContext(null);

    //  this is the async call, wait for the result (!)
    var model = _asyncService.GetUserInfo(Username).Result;

    // restore the context
    SynchronizationContext.SetSynchronizationContext(syncContext);

    return PartialView("_UserInfo", model);
}

3

Một điểm chính khác là bạn không nên chặn trên Nhiệm vụ và sử dụng không đồng bộ hóa tất cả các cách để ngăn chặn bế tắc. Sau đó, tất cả sẽ là chặn không đồng bộ không đồng bộ.

public async Task<ActionResult> ActionAsync()
{

    var data = await GetDataAsync();

    return View(data);
}

private async Task<string> GetDataAsync()
{
    // a very simple async method
    var result = await MyWebService.GetDataAsync();
    return result.ToString();
}

6
Điều gì sẽ xảy ra nếu tôi muốn luồng chính (UI) bị chặn cho đến khi tác vụ kết thúc? Hoặc trong một ứng dụng Console chẳng hạn? Giả sử tôi muốn sử dụng HttpClient, chỉ hỗ trợ không đồng bộ ... Làm cách nào để sử dụng nó một cách đồng bộ mà không có nguy cơ bị bế tắc ? Điều này phải có thể. Nếu WebClient có thể được sử dụng theo cách đó (vì có các phương thức đồng bộ) và hoạt động hoàn hảo, thì tại sao nó lại không thể được thực hiện với HttpClient?
Dexter

Xem câu trả lời của Philip Ngan ở trên (tôi biết câu này được đăng sau khi nhận xét này): Xếp hàng đợi sự tiếp tục chờ đợi đến một chuỗi khác không bị chặn, ví dụ: sử dụng var data = Task.Run (GetDataAsync) .Result
Jeroen

@Dexter - re " Điều gì xảy ra nếu tôi muốn chuỗi chính (UI) bị chặn cho đến khi tác vụ kết thúc? " - bạn có thực sự muốn chuỗi UI bị chặn, nghĩa là người dùng không thể làm gì, thậm chí không thể hủy - hoặc là bạn không muốn tiếp tục phương pháp bạn đang sử dụng? "await" hoặc "Task.ContinueWith" xử lý trường hợp sau.
ToolmakerSteve

@ToolmakerSteve tất nhiên tôi không muốn tiếp tục phương pháp này. Nhưng tôi chỉ đơn giản là không thể sử dụng await vì tôi cũng không thể sử dụng async bằng mọi cách - HttpClient được gọi trong main , tất nhiên không thể async. Và sau đó tôi đã đề cập đến việc làm tất cả những điều này trong một ứng dụng Console - trong trường hợp này, tôi muốn chính xác cái trước - tôi không muốn ứng dụng của mình thậm chí đa luồng. Chặn mọi thứ .
Dexter

-1

Một công việc xung quanh tôi đến là sử dụng Join phương pháp mở rộng cho nhiệm vụ trước khi yêu cầu kết quả.

Mã trông như thế này:

public ActionResult ActionAsync()
{
  var task = GetDataAsync();
  task.Join();
  var data = task.Result;

  return View(data);
}

Phương thức nối ở đâu:

public static class TaskExtensions
{
    public static void Join(this Task task)
    {
        var currentDispatcher = Dispatcher.CurrentDispatcher;
        while (!task.IsCompleted)
        {
            // Make the dispatcher allow this thread to work on other things
            currentDispatcher.Invoke(delegate { }, DispatcherPriority.SystemIdle);
        }
    }
}

Tôi không đủ hiểu biết về miền để thấy những hạn chế của giải pháp này (nếu có)

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.