Làm cách nào tôi có thể chẩn đoán async / đang chờ bế tắc?


24

Tôi đang làm việc với một cơ sở mã mới, sử dụng async / await rất nhiều. Hầu hết những người trong nhóm của tôi cũng khá mới với async / await. Chúng tôi thường có xu hướng tuân thủ các Thực tiễn Tốt nhất như Được chỉ định bởi Microsoft , nhưng thường cần bối cảnh của chúng tôi để chuyển qua cuộc gọi không đồng bộ và đang làm việc với các thư viện không ConfigureAwait(false).

Kết hợp tất cả những điều đó và chúng tôi gặp phải những bế tắc không đồng bộ được mô tả trong bài viết ... hàng tuần. Chúng không xuất hiện trong quá trình thử nghiệm đơn vị, vì các nguồn dữ liệu bị chế giễu của chúng tôi (thường thông qua Task.FromResult) không đủ để kích hoạt bế tắc. Vì vậy, trong thời gian chạy hoặc kiểm tra tích hợp, một số cuộc gọi dịch vụ chỉ đi ra ngoài ăn trưa và không bao giờ trở lại. Điều đó giết chết các máy chủ, và nói chung làm cho mọi thứ trở nên lộn xộn.

Vấn đề là việc theo dõi nơi xảy ra lỗi (thường không đồng bộ hóa mọi lúc) thường liên quan đến việc kiểm tra mã thủ công, việc này tốn thời gian và không thể tự động hóa.

Cách tốt hơn để chẩn đoán những gì gây ra bế tắc?


1
Câu hỏi hay; Tôi đã tự hỏi điều này bản thân mình. Bạn đã đọc bộ sưu tập các asyncbài viết của anh chàng này ?
Robert Harvey

@RobertHarvey - có thể không phải tất cả, nhưng tôi đã đọc một số. Thêm "Đảm bảo thực hiện hai hoặc ba điều này ở mọi nơi nếu không mã của bạn sẽ chết một cách khủng khiếp khi chạy".
Telastyn

Bạn có sẵn sàng bỏ async hoặc giảm việc sử dụng nó đến những điểm có lợi nhất không? Async IO không phải là tất cả hoặc không có gì.
usr

1
Nếu bạn có thể tái tạo bế tắc, bạn có thể chỉ nhìn vào dấu vết ngăn xếp để xem cuộc gọi chặn không?
Svick

2
Nếu vấn đề là "không đồng bộ mọi cách", thì điều đó có nghĩa là một nửa của bế tắc là một bế tắc truyền thống và sẽ hiển thị trong dấu vết ngăn xếp của luồng ngữ cảnh đồng bộ hóa.
Svick

Câu trả lời:


4

Ok - Tôi không chắc liệu những điều sau đây có giúp ích gì cho bạn không, vì tôi đã đưa ra một số giả định trong việc phát triển một giải pháp có thể đúng hoặc không đúng trong trường hợp của bạn. Có lẽ "giải pháp" của tôi quá lý thuyết và chỉ hoạt động đối với các ví dụ nhân tạo - Tôi chưa thực hiện bất kỳ thử nghiệm nào ngoài những thứ bên dưới.
Ngoài ra, tôi sẽ thấy một cách giải quyết sau đây hơn là một giải pháp thực sự nhưng xem xét việc thiếu phản hồi Tôi nghĩ rằng nó vẫn có thể tốt hơn không có gì (Tôi cứ xem câu hỏi của bạn chờ đợi một giải pháp, nhưng không thấy một câu hỏi nào được đăng lên tôi bắt đầu chơi xung quanh với vấn đề).

Nhưng đủ để nói: giả sử chúng ta có một dịch vụ dữ liệu đơn giản có thể được sử dụng để truy xuất một số nguyên:

public interface IDataService
{
    Task<int> LoadMagicInteger();
}

Một triển khai đơn giản sử dụng mã không đồng bộ:

public sealed class CustomDataService
    : IDataService
{
    public async Task<int> LoadMagicInteger()
    {
        Console.WriteLine("LoadMagicInteger - 1");
        await Task.Delay(100);
        Console.WriteLine("LoadMagicInteger - 2");
        var result = 42;
        Console.WriteLine("LoadMagicInteger - 3");
        await Task.Delay(100);
        Console.WriteLine("LoadMagicInteger - 4");
        return result;
    }
}

Bây giờ, một vấn đề phát sinh, nếu chúng ta đang sử dụng mã "không chính xác" như được minh họa bởi lớp này. Footruy cập không chính xác Task.Resultthay vì awaiting kết quả như Bar:

public sealed class ClassToTest
{
    private readonly IDataService _dataService;

    public ClassToTest(IDataService dataService)
    {
        this._dataService = dataService;
    }

    public async Task<int> Foo()
    {
        var result = this._dataService.LoadMagicInteger().Result;
        return result;
    }
    public async Task<int> Bar()
    {
        var result = await this._dataService.LoadMagicInteger();
        return result;
    }
}

Những gì chúng tôi (bạn) bây giờ cần là một cách để viết một bài kiểm tra thành công khi gọi Barnhưng không thành công khi gọi Foo(ít nhất là nếu tôi hiểu chính xác câu hỏi ;-)).

Tôi sẽ để mã nói; Đây là những gì tôi đã đưa ra (sử dụng các bài kiểm tra Visual Studio, nhưng nó cũng hoạt động bằng NUnit):

DataServiceMocktận dụng TaskCompletionSource<T>. Điều này cho phép chúng tôi đặt kết quả tại một điểm được xác định trong quá trình chạy thử dẫn đến thử nghiệm sau. Lưu ý rằng chúng tôi đang sử dụng một đại biểu để trả lại TaskCompletionSource trở lại thử nghiệm. Bạn cũng có thể đặt nó vào phương thức Khởi tạo của các thuộc tính kiểm tra và sử dụng.

TaskCompletionSource<int> tcs = null;
this._dataService.LoadMagicIntegerMock = t => tcs = t;

Task<int> task = null;
TaskTestHelper.AssertDoesNotBlock(() => task = this._instance.Foo());

tcs.TrySetResult(42);

var result = task.Result;
Assert.AreEqual(42, result);

this._end = true;

Điều xảy ra ở đây là trước tiên chúng tôi xác minh rằng chúng tôi có thể rời khỏi phương thức mà không chặn (điều này sẽ không hoạt động nếu có ai đó truy cập Task.Result- trong trường hợp này chúng tôi sẽ hết thời gian chờ vì kết quả của nhiệm vụ không khả dụng cho đến khi phương thức được trả về ).
Sau đó, chúng tôi đặt kết quả (bây giờ phương thức có thể thực thi) và chúng tôi xác minh kết quả (bên trong một bài kiểm tra đơn vị, chúng tôi có thể truy cập vào Task.Result vì chúng tôi thực sự muốn chặn xảy ra).

Hoàn thành lớp kiểm tra - BarTestthành công và FooTestthất bại như mong muốn.

[TestClass]
public class UnitTest1
{
    private DataServiceMock _dataService;
    private ClassToTest _instance;
    private bool _end;

    [TestInitialize]
    public void Initialize()
    {
        this._dataService = new DataServiceMock();
        this._instance = new ClassToTest(this._dataService);

        this._end = false;
    }
    [TestCleanup]
    public void Cleanup()
    {
        Assert.IsTrue(this._end);
    }

    [TestMethod]
    public void FooTest()
    {
        TaskCompletionSource<int> tcs = null;
        this._dataService.LoadMagicIntegerMock = t => tcs = t;

        Task<int> task = null;
        TaskTestHelper.AssertDoesNotBlock(() => task = this._instance.Foo());

        tcs.TrySetResult(42);

        var result = task.Result;
        Assert.AreEqual(42, result);

        this._end = true;
    }
    [TestMethod]
    public void BarTest()
    {
        TaskCompletionSource<int> tcs = null;
        this._dataService.LoadMagicIntegerMock = t => tcs = t;

        Task<int> task = null;
        TaskTestHelper.AssertDoesNotBlock(() => task = this._instance.Bar());

        tcs.TrySetResult(42);

        var result = task.Result;
        Assert.AreEqual(42, result);

        this._end = true;
    }
}

Và một lớp người trợ giúp nhỏ để kiểm tra sự bế tắc / thời gian chờ:

public static class TaskTestHelper
{
    public static void AssertDoesNotBlock(Action action, int timeout = 1000)
    {
        var timeoutTask = Task.Delay(timeout);
        var task = Task.Factory.StartNew(action);

        Task.WaitAny(timeoutTask, task);

        Assert.IsTrue(task.IsCompleted);
    }
}

Câu trả lời tốt đẹp. Tôi dự định tự mình thử mã của mình khi có thời gian (tôi thực sự không biết chắc là nó có hoạt động hay không), nhưng kudos và một người ủng hộ cho nỗ lực này.
Robert Harvey

-2

Đây là một chiến lược mà tôi đã sử dụng trong một ứng dụng rất lớn và rất, rất đa luồng:

Trước tiên, bạn cần một số cấu trúc dữ liệu xung quanh một mutex (không may) và không thực hiện đồng bộ thư mục cuộc gọi. Trong cấu trúc dữ liệu đó, có một liên kết đến bất kỳ mutex nào bị khóa trước đó. Mỗi mutex có "cấp độ" bắt đầu từ 0, mà bạn chỉ định khi mutex được tạo và không bao giờ có thể thay đổi.

Và quy tắc là: Nếu một mutex bị khóa, bạn chỉ phải khóa các mutex khác ở mức thấp hơn. Nếu bạn tuân theo quy tắc đó, thì bạn không thể có bế tắc. Khi bạn tìm thấy một vi phạm, ứng dụng của bạn vẫn hoạt động tốt.

Khi bạn tìm thấy một vi phạm, có hai khả năng: Bạn có thể đã gán các cấp sai. Bạn đã khóa A theo sau là khóa B, vì vậy B nên có mức thấp hơn. Vì vậy, bạn sửa cấp độ và thử lại.

Khả năng khác: Bạn không thể sửa nó. Một số mã khóa của bạn A theo sau là khóa B, trong khi một số mã khác khóa B theo sau là khóa A. Không có cách nào để gán các mức cho phép điều này. Và tất nhiên đây là một bế tắc tiềm năng: Nếu cả hai mã chạy đồng thời trên các luồng khác nhau, sẽ có cơ hội bế tắc.

Sau khi giới thiệu điều này, có một giai đoạn khá ngắn trong đó các mức độ phải được điều chỉnh, tiếp theo là một giai đoạn dài hơn nơi các bế tắc tiềm năng được tìm thấy.


4
Tôi xin lỗi, làm thế nào để áp dụng cho hành vi async / await? Tôi thực sự không thể đưa cấu trúc quản lý mutex tùy chỉnh vào Thư viện song song nhiệm vụ.
Telastyn

-3

Bạn có đang sử dụng Async / Await để bạn có thể song song các cuộc gọi đắt tiền như cơ sở dữ liệu không? Tùy thuộc vào đường dẫn thực thi trong DB, điều này có thể không thực hiện được.

Kiểm tra phạm vi bảo hiểm với async / await có thể là một thách thức và không có gì giống như việc sử dụng sản xuất thực sự để tìm lỗi. Một mẫu mà bạn có thể xem xét là chuyển ID tương quan và ghi lại nó xuống ngăn xếp, sau đó có thời gian chờ xếp tầng ghi lại lỗi. Đây là nhiều hơn một mô hình SOA nhưng ít nhất nó sẽ cho bạn cảm giác về việc nó đến từ đâu. Chúng tôi đã sử dụng điều này với Splunk để tìm bế tắ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.