Tại sao hành động không đồng bộ này bị treo?


102

Tôi có một ứng dụng .Net 4.5 nhiều tầng gọi một phương thức bằng C # mới của asyncawait từ khóa và chỉ bị treo và tôi không thể hiểu tại sao.

Ở phía dưới, tôi có một phương thức không đồng bộ mở rộng tiện ích cơ sở dữ liệu của chúng tôi OurDBConn(về cơ bản là một trình bao bọc cho cơ sở DBConnectionDBCommandcác đối tượng):

public static async Task<T> ExecuteAsync<T>(this OurDBConn dataSource, Func<OurDBConn, T> function)
{
    string connectionString = dataSource.ConnectionString;

    // Start the SQL and pass back to the caller until finished
    T result = await Task.Run(
        () =>
        {
            // Copy the SQL connection so that we don't get two commands running at the same time on the same open connection
            using (var ds = new OurDBConn(connectionString))
            {
                return function(ds);
            }
        });

    return result;
}

Sau đó, tôi có một phương thức không đồng bộ mức độ trung bình gọi điều này để nhận một số tổng số chạy chậm:

public static async Task<ResultClass> GetTotalAsync( ... )
{
    var result = await this.DBConnection.ExecuteAsync<ResultClass>(
        ds => ds.Execute("select slow running data into result"));

    return result;
}

Cuối cùng, tôi có một phương thức giao diện người dùng (một hành động MVC) chạy đồng bộ:

Task<ResultClass> asyncTask = midLevelClass.GetTotalAsync(...);

// do other stuff that takes a few seconds

ResultClass slowTotal = asyncTask.Result;

Vấn đề là nó bị treo ở dòng cuối cùng đó mãi mãi. Nó làm điều tương tự nếu tôi gọi asyncTask.Wait(). Nếu tôi chạy trực tiếp phương pháp SQL chậm thì mất khoảng 4 giây.

Hành vi mà tôi mong đợi là khi nó đến asyncTask.Result, nếu nó chưa kết thúc, nó nên đợi cho đến khi nó đạt được, và khi nó đã xong nó sẽ trả về kết quả.

Nếu tôi bước qua với trình gỡ lỗi, câu lệnh SQL hoàn thành và hàm lambda kết thúc, nhưng return result;dòng của GetTotalAsynckhông bao giờ đạt được.

Bất kỳ ý tưởng những gì tôi đang làm sai?

Bất kỳ đề xuất nào về nơi tôi cần điều tra để khắc phục điều này?

Đây có thể là một điểm bế tắc ở đâu đó, và nếu có thì có cách nào trực tiếp để tìm ra nó không?

Câu trả lời:


150

Đúng, đó là một bế tắc. Và một lỗi phổ biến với TPL, vì vậy đừng cảm thấy tồi tệ.

Khi bạn viết await foo, thời gian chạy, theo mặc định, lên lịch cho sự tiếp tục của hàm trên cùng SynchronizationContext mà phương thức đã bắt đầu. Trong tiếng Anh, giả sử bạn đã gọi của bạn ExecuteAsynctừ chuỗi giao diện người dùng. Truy vấn của bạn chạy trên chuỗi threadpool (vì bạn đã gọi Task.Run), nhưng sau đó bạn chờ kết quả. Điều này có nghĩa là thời gian chạy sẽ lên lịch cho return result;dòng " " của bạn chạy lại trên chuỗi giao diện người dùng, thay vì lên lịch cho nó trở lại luồng luồng.

Vậy làm thế nào để bế tắc này? Hãy tưởng tượng bạn chỉ có mã này:

var task = dataSource.ExecuteAsync(_ => 42);
var result = task.Result;

Vì vậy, dòng đầu tiên bắt đầu công việc không đồng bộ. Dòng thứ hai sau đó chặn chuỗi giao diện người dùng . Vì vậy, khi thời gian chạy muốn chạy lại dòng "kết quả trả về" trên chuỗi giao diện người dùng, nó không thể làm điều đó cho đến khiResult hoàn tất. Nhưng tất nhiên, Kết quả không thể được đưa ra cho đến khi sự trở lại xảy ra. Bế tắc.

Điều này minh họa một quy tắc chính của việc sử dụng TPL: khi bạn sử dụng .Resulttrên chuỗi giao diện người dùng (hoặc một số ngữ cảnh đồng bộ ưa thích khác), bạn phải cẩn thận để đảm bảo rằng không có gì mà Tác vụ phụ thuộc được lên lịch cho chuỗi giao diện người dùng. Nếu không thì sự dữ sẽ xảy ra.

Vậy bạn làm gì? Tùy chọn số 1 là sử dụng đang chờ đợi ở mọi nơi, nhưng như bạn đã nói đó không phải là một tùy chọn. Tùy chọn thứ hai có sẵn cho bạn là chỉ cần ngừng sử dụng await. Bạn có thể viết lại hai hàm của mình thành:

public static Task<T> ExecuteAsync<T>(this OurDBConn dataSource, Func<OurDBConn, T> function)
{
    string connectionString = dataSource.ConnectionString;

    // Start the SQL and pass back to the caller until finished
    return Task.Run(
        () =>
        {
            // Copy the SQL connection so that we don't get two commands running at the same time on the same open connection
            using (var ds = new OurDBConn(connectionString))
            {
                return function(ds);
            }
        });
}

public static Task<ResultClass> GetTotalAsync( ... )
{
    return this.DBConnection.ExecuteAsync<ResultClass>(
        ds => ds.Execute("select slow running data into result"));
}

Có gì khác biệt? Bây giờ không có chờ đợi ở bất cứ đâu, vì vậy không có gì được lên lịch ngầm cho chuỗi giao diện người dùng. Đối với các phương thức đơn giản như thế này mà chỉ có một lần trả lại, không có ích gì khi thực hiện một "var result = await...; return result mẫu ""; chỉ cần loại bỏ công cụ sửa đổi không đồng bộ và chuyển trực tiếp đối tượng tác vụ xung quanh. Nó ít chi phí hơn, nếu không có gì khác.

Tùy chọn số 3 là chỉ định rằng bạn không muốn thời gian chờ của mình lên lịch trở lại chuỗi giao diện người dùng mà chỉ cần lên lịch cho nhóm chuỗi. Bạn làm điều này với ConfigureAwaitphương pháp, như sau:

public static async Task<ResultClass> GetTotalAsync( ... )
{
    var resultTask = this.DBConnection.ExecuteAsync<ResultClass>(
        ds => return ds.Execute("select slow running data into result");

    return await resultTask.ConfigureAwait(false);
}

Việc chờ đợi một tác vụ thông thường sẽ lên lịch cho chuỗi giao diện người dùng nếu bạn đang ở đó; chờ đợi kết quả ContinueAwaitsẽ bỏ qua bất kỳ ngữ cảnh nào bạn đang ở và luôn lên lịch cho threadpool. Nhược điểm của điều này là bạn phải rắc điều này ở mọi nơi trong tất cả các chức năng .Result của bạn phụ thuộc vào, bởi vì bất kỳ sự thiếu sót nào cũng .ConfigureAwaitcó thể là nguyên nhân của một bế tắc khác.


6
BTW, câu hỏi là về ASP.NET, vì vậy không có chuỗi giao diện người dùng. Nhưng vấn đề với deadlock hoàn toàn giống nhau, do ASP.NET SynchronizationContext.
svick

Điều đó giải thích rất nhiều, vì tôi đã có mã .Net 4 tương tự không có vấn đề nhưng sử dụng TPL mà không có async/ awaittừ khóa.
Keith

2
TPL = Thư viện song song tác vụ msdn.microsoft.com/en-us/library/dd460717(v=vs.110).aspx
Jamie Ide

Nếu bất cứ ai đang tìm kiếm các mã VB.net (như tôi) nó được giải thích ở đây: docs.microsoft.com/en-us/dotnet/visual-basic/programming-guide/...
MichaelDarkBlue

Bạn có thể xin vui lòng giúp đỡ tôi trong stackoverflow.com/questions/54360300/...
Jitendra Pancholi

36

Đây là asynckịch bản bế tắc hỗn hợp cổ điển , như tôi mô tả trên blog của mình . Jason mô tả nó tốt: theo mặc định, một "ngữ cảnh" được lưu ở mỗi awaitvà được sử dụng để tiếp tục asyncphương thức. "Bối cảnh" này là hiện tại SynchronizationContexttrừ khi nó null, trong trường hợp đó nó là hiện tại TaskScheduler. Khi asyncphương thức cố gắng tiếp tục, trước tiên nó sẽ nhập lại "ngữ cảnh" đã được chụp (trong trường hợp này là ASP.NET SynchronizationContext). ASP.NET SynchronizationContextchỉ cho phép một luồng trong ngữ cảnh tại một thời điểm và đã có một luồng trong ngữ cảnh - luồng bị chặn trên Task.Result.

Có hai hướng dẫn sẽ tránh được tình trạng bế tắc này:

  1. Sử dụng asynctất cả các cách xuống. Bạn đề cập rằng bạn "không thể" làm điều này, nhưng tôi không chắc tại sao lại không. ASP.NET MVC trên .NET 4.5 chắc chắn có thể hỗ trợ asynccác hành động và đây không phải là một thay đổi khó thực hiện.
  2. Sử dụng ConfigureAwait(continueOnCapturedContext: false)càng nhiều càng tốt. Điều này ghi đè hành vi mặc định tiếp tục trên ngữ cảnh đã chụp.

ConfigureAwait(false)đảm bảo rằng chức năng hiện tại tiếp tục lại trên một ngữ cảnh khác không?
chue x

Khung MVC hỗ trợ nó, nhưng đây là một phần của ứng dụng MVC hiện có với rất nhiều JS phía máy khách đã có mặt. Tôi không thể dễ dàng chuyển sang một asynchành động mà không phá vỡ cách hoạt động của phía khách hàng. Tôi chắc chắn có kế hoạch điều tra tùy chọn đó lâu dài hơn.
Keith

Chỉ để làm rõ nhận xét của tôi - tôi tò mò liệu việc sử dụng ConfigureAwait(false)cây gọi có giải quyết được vấn đề của OP hay không.
chue x

3
@Keith: Thực hiện một hành động MVC asynchoàn toàn không ảnh hưởng đến phía khách hàng. Tôi giải thích điều này trong một bài đăng blog khác, asyncKhông thay đổi giao thức HTTP .
Stephen Cleary

1
@Keith: Việc async"phát triển" thông qua codebase là điều bình thường . Nếu phương thức bộ điều khiển của bạn có thể phụ thuộc vào các hoạt động không đồng bộ, thì phương thức lớp cơ sở sẽ trả về Task<ActionResult>. Việc chuyển đổi một dự án lớn sang asyncluôn khó khăn vì việc trộn asyncvà đồng bộ mã rất khó và phức tạp. asyncMã thuần đơn giản hơn nhiều.
Stephen Cleary

12

Tôi cũng ở trong tình huống bế tắc tương tự nhưng trong trường hợp của tôi gọi một phương thức không đồng bộ từ một phương thức đồng bộ, những gì phù hợp với tôi là:

private static SiteMetadataCacheItem GetCachedItem()
{
      TenantService TS = new TenantService(); // my service datacontext
      var CachedItem = Task.Run(async ()=> 
               await TS.GetTenantDataAsync(TenantIdValue)
      ).Result; // dont deadlock anymore
}

đây có phải là một cách tiếp cận tốt, bất kỳ ý tưởng?


Giải pháp này cũng đang hoạt động với tôi, nhưng tôi không chắc đó là giải pháp tốt hoặc nó có thể bị hỏng ở đâu đó. Bất kỳ ai có thể giải thích rằng
Konstantin Vdovkin

cũng cuối cùng tôi đã đi với giải pháp này và nó đang làm việc trong một môi trường sản xuất mà không rắc rối .....
Danilow

1
Tôi nghĩ rằng bạn đang đạt được hiệu suất khi sử dụng Task.Run. Trong thử nghiệm của tôi Task.Run gần như tăng gấp đôi thời gian thực thi cho một yêu cầu http 100ms.
Timothy Gonzalez

1
điều đó có ý nghĩa, bạn đang tạo một nhiệm vụ mới để gói một cuộc gọi không đồng bộ, hiệu suất là sự đánh đổi
Danilow

Tuyệt vời, điều này cũng làm việc cho tôi, trường hợp của tôi cũng do một phương thức đồng bộ gọi một phương thức không đồng bộ gây ra. Cảm ơn bạn!
Leonardo Spina

4

Chỉ để thêm vào câu trả lời được chấp nhận (không đủ đại diện để nhận xét), tôi đã gặp sự cố này phát sinh khi chặn sử dụng task.Result , mặc dù mọi sự kiện awaitbên dưới đều có ConfigureAwait(false), như trong ví dụ này:

public Foo GetFooSynchronous()
{
    var foo = new Foo();
    foo.Info = GetInfoAsync.Result;  // often deadlocks in ASP.NET
    return foo;
}

private async Task<string> GetInfoAsync()
{ 
    return await ExternalLibraryStringAsync().ConfigureAwait(false);
}

Vấn đề thực sự nằm ở mã thư viện bên ngoài. Phương thức thư viện không đồng bộ đã cố gắng tiếp tục trong ngữ cảnh đồng bộ hóa đang gọi, bất kể tôi định cấu hình thời gian chờ như thế nào, dẫn đến bế tắc.

Do đó, câu trả lời là cuộn phiên bản mã thư viện bên ngoài của riêng tôi ExternalLibraryStringAsync , để nó có các thuộc tính tiếp tục mong muốn.


câu trả lời sai cho mục đích lịch sử

Sau nhiều đau đớn và thống khổ, tôi đã tìm ra giải pháp được chôn vùi trong bài đăng trên blog này (Ctrl-f cho 'bế tắc'). Nó xoay quanh việc sử dụngtask.ContinueWith , thay vì trần task.Result.

Ví dụ về deadlock trước đây:

public Foo GetFooSynchronous()
{
    var foo = new Foo();
    foo.Info = GetInfoAsync.Result;  // often deadlocks in ASP.NET
    return foo;
}

private async Task<string> GetInfoAsync()
{ 
    return await ExternalLibraryStringAsync().ConfigureAwait(false);
}

Tránh tình trạng bế tắc như thế này:

public Foo GetFooSynchronous
{
    var foo = new Foo();
    GetInfoAsync()  // ContinueWith doesn't run until the task is complete
        .ContinueWith(task => foo.Info = task.Result);
    return foo;
}

private async Task<string> GetInfoAsync
{
    return await ExternalLibraryStringAsync().ConfigureAwait(false);
}

Downvote để làm gì? Giải pháp này đang làm việc cho tôi.
Cameron Jeffers

Bạn đang trả lại đối tượng trước khi Taskhoàn thành và không cung cấp cho người gọi phương tiện xác định khi nào đột biến của đối tượng được trả về thực sự xảy ra.
Phục vụ

hmm, tôi hiểu rồi. Vì vậy, tôi có nên để lộ một số loại phương thức "đợi cho đến khi tác vụ hoàn thành" sử dụng vòng lặp while chặn theo cách thủ công (hoặc một cái gì đó tương tự)? Hoặc đóng gói một khối như vậy vào GetFooSynchronousphương thức?
Cameron Jeffers

1
Nếu bạn làm vậy, nó sẽ bế tắc. Bạn cần phải đồng bộ hóa tất cả bằng cách trả về một Taskthay vì chặn.
Servy

Thật không may, đó không phải là một tùy chọn, lớp thực hiện một giao diện đồng bộ mà tôi không thể thay đổi.
Cameron Jeffers

0

câu trả lời nhanh: thay đổi dòng này

ResultClass slowTotal = asyncTask.Result;

đến

ResultClass slowTotal = await asyncTask;

tại sao? bạn không nên sử dụng .result để nhận kết quả của các tác vụ bên trong hầu hết các ứng dụng ngoại trừ các ứng dụng bảng điều khiển nếu bạn làm như vậy chương trình của bạn sẽ bị treo khi đến đó

bạn cũng có thể thử mã dưới đây nếu bạn muốn sử dụng.

ResultClass slowTotal = Task.Run(async ()=>await asyncTask).Result;
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.