Cách thích hợp để xử lý các trường hợp ngoại lệ trong AsyncDispose


20

Trong quá trình chuyển sang .NET Core 3 mới IAsynsDisposable, tôi đã gặp phải vấn đề sau.

Cốt lõi của vấn đề: nếu DisposeAsyncném ngoại lệ, ngoại lệ này sẽ ẩn bất kỳ ngoại lệ nào được ném vào bên trong await using-block.

class Program 
{
    static async Task Main()
    {
        try
        {
            await using (var d = new D())
            {
                throw new ArgumentException("I'm inside using");
            }
        }
        catch (Exception e)
        {
            Console.WriteLine(e.Message); // prints I'm inside dispose
        }
    }
}

class D : IAsyncDisposable
{
    public async ValueTask DisposeAsync()
    {
        await Task.Delay(1);
        throw new Exception("I'm inside dispose");
    }
}

Điều bị bắt là AsyncDisposengoại lệ nếu nó bị ném và ngoại lệ chỉ từ bên trong await usingnếu AsyncDisposekhông ném.

Tuy nhiên, tôi thích nó theo cách khác: lấy ngoại lệ từ await usingkhối nếu có thể và DisposeAsyncchỉ ngoại lệ khi await usingkhối kết thúc thành công.

Đặt vấn đề: Tưởng tượng rằng lớp của tôi Dhoạt động với một số tài nguyên mạng và đăng ký một số thông báo từ xa. Mã bên trong await usingcó thể làm điều gì đó sai và làm hỏng kênh liên lạc, sau đó mã trong Dispose cố gắng đóng giao tiếp một cách duyên dáng (ví dụ: hủy đăng ký thông báo) cũng sẽ thất bại. Nhưng ngoại lệ đầu tiên cho tôi thông tin thực sự về vấn đề này, và ngoại lệ thứ hai chỉ là vấn đề thứ yếu.

Trong trường hợp khác khi phần chính chạy qua và xử lý không thành công, vấn đề thực sự nằm ở bên trong DisposeAsync, do đó, ngoại lệ DisposeAsynclà phần có liên quan. Điều này có nghĩa là chỉ cần loại bỏ tất cả các ngoại lệ bên trong DisposeAsynckhông nên là một ý tưởng tốt.


Tôi biết rằng có một vấn đề tương tự với trường hợp không đồng bộ: ngoại lệ finallyghi đè lên ngoại lệ try, đó là lý do tại sao không nên đưa vào Dispose(). Nhưng với các lớp truy cập mạng loại bỏ các ngoại lệ trong các phương thức đóng không hoàn toàn tốt.


Có thể giải quyết vấn đề với người trợ giúp sau:

static class AsyncTools
{
    public static async Task UsingAsync<T>(this T disposable, Func<T, Task> task)
            where T : IAsyncDisposable
    {
        bool trySucceeded = false;
        try
        {
            await task(disposable);
            trySucceeded = true;
        }
        finally
        {
            if (trySucceeded)
                await disposable.DisposeAsync();
            else // must suppress exceptions
                try { await disposable.DisposeAsync(); } catch { }
        }
    }
}

và sử dụng nó như

await new D().UsingAsync(d =>
{
    throw new ArgumentException("I'm inside using");
});

đó là loại xấu xí (và không cho phép những thứ như trả lại sớm bên trong khối sử dụng).

Có một giải pháp tốt, hợp quy, await usingnếu có thể? Tìm kiếm của tôi trên internet thậm chí không tìm thấy thảo luận về vấn đề này.


1
" Nhưng với các lớp truy cập mạng loại bỏ các ngoại lệ trong các phương thức đóng không hoàn toàn tốt " - Tôi nghĩ rằng hầu hết các lớp BLC nối mạng đều có một Closephương thức riêng vì lý do này. Có lẽ nên làm điều tương tự: CloseAsynccố gắng đóng mọi thứ lại một cách độc đáo và ném vào thất bại. DisposeAsyncchỉ làm tốt nhất của nó, và thất bại âm thầm.
cant7

@ canton7: ​​Vâng, có một CloseAsyncphương tiện riêng mà tôi cần phải thực hiện các biện pháp phòng ngừa bổ sung để chạy nó. Nếu tôi chỉ đặt nó ở cuối using-block, nó sẽ bị bỏ qua khi trả về sớm, v.v. (đây là điều chúng tôi muốn xảy ra) và ngoại lệ (đây là điều chúng tôi muốn xảy ra). Nhưng ý tưởng có vẻ đầy hứa hẹn.
Vlad

Có một lý do khiến nhiều tiêu chuẩn mã hóa cấm trả lại sớm :) Trường hợp có liên quan đến mạng, một chút rõ ràng không phải là điều xấu IMO. Disposeluôn luôn là "Mọi thứ có thể đã sai: chỉ cần cố gắng hết sức để cải thiện tình hình, nhưng đừng làm nó tồi tệ hơn", và tôi không hiểu tại sao AsyncDisposenên khác đi.
canton7

@ canton7: ​​Vâng, trong một ngôn ngữ có ngoại lệ, mọi tuyên bố có thể là sự trở lại sớm: - \
Vlad

Phải, nhưng những người sẽ được đặc biệt . Trong trường hợp đó, cố DisposeAsyncgắng hết sức để dọn dẹp nhưng không ném là điều nên làm. Bạn đang nói về việc trả lại sớm có chủ ý , trong đó việc trả lại sớm có chủ ý có thể bỏ qua một cuộc gọi đến CloseAsync: đó là những điều bị cấm bởi nhiều tiêu chuẩn mã hóa.
cant7

Câu trả lời:


3

Có những trường hợp ngoại lệ mà bạn muốn hiển thị (làm gián đoạn yêu cầu hiện tại hoặc đưa ra quy trình) và có những trường hợp ngoại lệ mà thiết kế của bạn đôi khi sẽ xảy ra và bạn có thể xử lý chúng (ví dụ: thử lại và tiếp tục).

Nhưng phân biệt giữa hai loại này là tùy thuộc vào người gọi cuối cùng của mã - đây là toàn bộ điểm ngoại lệ, để lại quyết định cho người gọi.

Đôi khi người gọi sẽ ưu tiên nhiều hơn cho việc lướt ngoại lệ từ khối mã gốc và đôi khi là ngoại lệ từ Dispose. Không có quy tắc chung để quyết định nên ưu tiên. CLR ít nhất là nhất quán (như bạn đã lưu ý) giữa hành vi đồng bộ hóa và không đồng bộ.

Có lẽ thật đáng tiếc khi bây giờ chúng ta phải AggregateExceptionđại diện cho nhiều trường hợp ngoại lệ, nó không thể được trang bị thêm để giải quyết vấn đề này. tức là nếu một ngoại lệ đã có trong chuyến bay và một ngoại lệ khác bị ném, chúng được kết hợp thành một AggregateException. Cơ catchchế có thể được sửa đổi để nếu bạn viết catch (MyException)thì nó sẽ bắt bất kỳ loại nào AggregateExceptioncó ngoại lệ MyException. Có nhiều sự phức tạp khác xuất phát từ ý tưởng này, và có lẽ quá rủi ro để sửa đổi một cái gì đó rất cơ bản bây giờ.

Bạn có thể cải thiện UsingAsyncđể hỗ trợ trả lại sớm một giá trị:

public static async Task<R> UsingAsync<T, R>(this T disposable, Func<T, Task<R>> task)
        where T : IAsyncDisposable
{
    bool trySucceeded = false;
    R result;
    try
    {
        result = await task(disposable);
        trySucceeded = true;
    }
    finally
    {
        if (trySucceeded)
            await disposable.DisposeAsync();
        else // must suppress exceptions
            try { await disposable.DisposeAsync(); } catch { }
    }
    return result;
}

Vì vậy, tôi có hiểu đúng không: ý tưởng của bạn là trong một số trường hợp chỉ await usingcó thể sử dụng tiêu chuẩn (đây là trường hợp DisposeAsync sẽ không ném vào trường hợp không gây tử vong) và một người trợ giúp thích UsingAsynchợp hơn (nếu DisposeAsync có khả năng ném) ? (Tất nhiên, tôi cần phải sửa đổi UsingAsyncđể nó không bắt mọi thứ một cách mù quáng, nhưng chỉ không gây tử vong (và không phải là xương trong cách sử dụng của Eric Lippert ).)
Vlad

@Vlad có - cách tiếp cận đúng hoàn toàn phụ thuộc vào ngữ cảnh. Cũng lưu ý rằng Sử dụng không thể được viết một lần để sử dụng một số phân loại thực sự trên toàn cầu về các loại ngoại lệ tùy theo việc chúng có nên bị bắt hay không. Một lần nữa đây là một quyết định được đưa ra khác nhau tùy thuộc vào tình huống. Khi Eric Lippert nói về những phạm trù đó, chúng không phải là sự thật nội tại về các loại ngoại lệ. Danh mục cho mỗi loại ngoại lệ phụ thuộc vào thiết kế của bạn. Đôi khi một IOException được mong đợi bởi thiết kế, đôi khi thì không.
Daniel Earwicker

4

Có thể bạn đã hiểu tại sao điều này xảy ra, nhưng nó đáng để đánh vần. Hành vi này không cụ thể await using. Nó sẽ xảy ra với một usingkhối đơn giản quá. Vì vậy, trong khi tôi nói Dispose()ở đây, tất cả áp dụng cho DisposeAsync()quá.

Một usingkhối chỉ là đường cú pháp cho một khối try/ finally, như phần nhận xét của tài liệu nói. Những gì bạn thấy xảy ra bởi vì finallykhối luôn chạy, ngay cả sau một ngoại lệ. Vì vậy, nếu một ngoại lệ xảy ra và không có catchkhối, ngoại lệ được giữ cho đến khi finallykhối chạy, và sau đó ngoại lệ được ném. Nhưng nếu một ngoại lệ xảy ra finally, bạn sẽ không bao giờ thấy ngoại lệ cũ.

Bạn có thể thấy điều này với ví dụ này:

try {
    throw new Exception("Inside try");
} finally {
    throw new Exception("Inside finally");
}

Nó không quan trọng cho dù Dispose()hay DisposeAsync()được gọi là bên trong finally. Các hành vi là như nhau.

Suy nghĩ đầu tiên của tôi là: đừng ném vào Dispose(). Nhưng sau khi xem xét một số mã riêng của Microsoft, tôi nghĩ nó phụ thuộc.

Hãy xem việc thực hiện của họ FileStream, ví dụ. Cả hai Dispose()phương thức đồng bộ , và DisposeAsync()thực sự có thể ném ngoại lệ. Đồng bộ Dispose()không bỏ qua một số ngoại lệ có chủ ý, nhưng không phải tất cả.

Nhưng tôi nghĩ điều quan trọng là phải tính đến bản chất của lớp học của bạn. Trong một FileStream, ví dụ, Dispose()sẽ tuôn ra bộ đệm cho hệ thống tập tin. Đó là một nhiệm vụ rất quan trọng và bạn cần biết nếu thất bại . Bạn không thể bỏ qua điều đó.

Tuy nhiên, trong các loại đối tượng khác, khi bạn gọi Dispose(), bạn thực sự không còn sử dụng đối tượng nữa. Gọi Dispose()thực sự chỉ có nghĩa là "đối tượng này đã chết đối với tôi". Có thể nó dọn sạch một số bộ nhớ được phân bổ, nhưng việc không làm ảnh hưởng đến hoạt động của ứng dụng của bạn theo bất kỳ cách nào. Trong trường hợp đó, bạn có thể quyết định bỏ qua ngoại lệ bên trong của bạn Dispose().

Nhưng trong mọi trường hợp, nếu bạn muốn phân biệt giữa một ngoại lệ bên trong usinghoặc một ngoại lệ xuất phát Dispose(), thì bạn cần một try/ catchkhối cả bên trong và bên ngoài usingkhối của bạn :

try {
    await using (var d = new D())
    {
        try
        {
            throw new ArgumentException("I'm inside using");
        }
        catch (Exception e)
        {
            Console.WriteLine(e.Message); // prints I'm inside using
        }
    }
} catch (Exception e) {
    Console.WriteLine(e.Message); // prints I'm inside dispose
}

Hoặc bạn không thể sử dụng using. Viết ra một try/ catch/ finallychặn chính bạn, nơi bạn bắt gặp bất kỳ ngoại lệ nào trong finally:

var d = new D();
try
{
    throw new ArgumentException("I'm inside try");
}
catch (Exception e)
{
    Console.WriteLine(e.Message); // prints I'm inside try
}
finally
{
    try
    {
        if (D != null) await D.DisposeAsync();
    }
    catch (Exception e)
    {
        Console.WriteLine(e.Message); // prints I'm inside dispose
    }
}

3
Btw, source.dot.net (NET Core) / referencesource.microsoft.com (.NET Framework) là dễ dàng hơn nhiều để duyệt hơn GitHub
canton7

Cảm ơn bạn vì câu trả lời! Tôi biết lý do thực sự là gì (tôi đã đề cập đến trường hợp thử / cuối cùng và đồng bộ trong câu hỏi). Bây giờ về đề xuất của bạn. Một catch trong các usingkhối sẽ không thể giúp ích vì thường xử lý ngoại lệ được thực hiện ở đâu đó xa usingkhối bản thân, vì vậy xử lý nó bên trong usingthường không phải là rất khả thi. Về việc sử dụng không using- nó có thực sự tốt hơn cách giải quyết được đề xuất không?
Vlad

2
@ canton7 Tuyệt vời! Tôi đã biết về Referenceource.microsoft.com , nhưng không biết có .NET Core tương đương. Cảm ơn!
Gabriel Luci

@Vlad "Tốt hơn" là điều mà chỉ bạn mới có thể trả lời. Tôi biết nếu tôi đang đọc mã của người khác, tôi muốn xem try/ catch/ finallychặn vì nó sẽ ngay lập tức rõ ràng những gì nó đang làm mà không cần phải đọc những gì AsyncUsingđang làm. Bạn cũng giữ tùy chọn thực hiện trở lại sớm. Cũng sẽ có thêm một chi phí CPU cho bạn AwaitUsing. Nó sẽ nhỏ, nhưng nó ở đó.
Gabriel Luci

2
@PauloMorgado Điều đó chỉ có nghĩa là Dispose()không nên ném nó được gọi nhiều hơn một lần. Việc triển khai của chính Microsoft có thể đưa ra các ngoại lệ và vì lý do chính đáng, như tôi đã trình bày trong câu trả lời này. Tuy nhiên, tôi đồng ý rằng bạn nên tránh nó nếu có thể vì không ai có thể mong đợi nó sẽ ném.
Gabriel Luci

4

sử dụng có hiệu quả Mã xử lý ngoại lệ (cú pháp đường để thử ... cuối cùng ... Vứt bỏ ()).

Nếu mã xử lý ngoại lệ của bạn đang ném Ngoại lệ, một cái gì đó thực sự bị phá vỡ.

Bất cứ điều gì khác xảy ra thậm chí đưa bạn vào đó, không thực sự là vấn đề nữa. Mã xử lý Faulty Exception sẽ ẩn tất cả các ngoại lệ có thể, bằng cách này hay cách khác. Mã xử lý ngoại lệ phải được sửa, có mức độ ưu tiên tuyệt đối. Không có điều đó, bạn không bao giờ có đủ dữ liệu gỡ lỗi cho vấn đề thực sự. Tôi thấy nó làm sai cực kỳ thường xuyên. Đó là về dễ dàng để có được sai, như xử lý con trỏ trần truồng. Vì vậy, thường xuyên, có hai bài viết về liên kết theo chủ đề tôi có thể giúp bạn với bất kỳ lỗi thiết kế cơ bản nào:

Tùy thuộc vào phân loại Ngoại lệ, đây là những gì bạn cần làm nếu mã Xử lý ngoại lệ / Mã nhúng của bạn ném Ngoại lệ:

Đối với Fatal, Boneheaded và Vexing, giải pháp là như nhau.

Ngoại lệ ngoại sinh, phải được tránh ngay cả với chi phí nghiêm trọng. Có một lý do chúng tôi vẫn sử dụng các tệp nhật ký thay vì đăng nhập cơ sở dữ liệu để ghi lại các ngoại lệ - DB Opeartions chỉ là cách để dễ gặp phải các vấn đề Ngoại sinh. Logfiles là một trường hợp, trong đó tôi thậm chí không bận tâm nếu bạn giữ Xử lý tệp Mở toàn bộ Thời gian chạy.

Nếu bạn phải đóng một kết nối, đừng lo lắng nhiều về đầu kia. Xử lý nó giống như UDP: "Tôi sẽ gửi thông tin, nhưng tôi không quan tâm nếu phía bên kia có nhận được không." Xử lý là về việc làm sạch tài nguyên ở phía khách hàng / phía bạn đang làm việc.

Tôi có thể cố gắng thông báo cho họ. Nhưng dọn dẹp các thứ ở phía Máy chủ / FS? Đó là những gì thời gian chờ của họ và xử lý ngoại lệ của họ chịu trách nhiệm.


Vì vậy, đề xuất của bạn có hiệu quả sôi nổi để ngăn chặn các ngoại lệ về kết nối gần, phải không?
Vlad

@Vlad Những người ngoại sinh? Chắc chắn rồi. Dipose / Finalizer có mặt để dọn dẹp về phía họ. Rất có thể khi đóng conneciton Instance do ngoại lệ, bạn làm như vậy becaue bạn không còn một kết nối làm việc với họ anyway. Và điểm nào sẽ xảy ra khi có Ngoại lệ "Không kết nối" trong khi xử lý Ngoại lệ "Không kết nối" trước đó? Bạn gửi một "Yo, tôi đóng kết nối này" trong đó bạn bỏ qua tất cả các Ngoại lệ ngoại sinh hoặc ngay cả khi nó đến gần mục tiêu. Afaik các cài đặt mặc định của Vứt bỏ đã làm điều đó.
Christopher

@Vlad: Tôi đã nhớ, rằng có rất nhiều thứ bạn không bao giờ phải ném ngoại lệ (Ngoại trừ những thứ gây tử vong). Loại Khởi tạo là cách lên trên danh sách. Vứt bỏ cũng là một trong những điều sau: "Để giúp đảm bảo rằng các tài nguyên luôn được dọn sạch một cách thích hợp, một phương thức Vứt bỏ phải được gọi nhiều lần mà không cần ném ngoại lệ." docs.microsoft.com/en-us/dotnet/stiteria/garbage-collection/ory
Christopher

@Vlad Cơ hội của ngoại lệ gây tử vong? Chúng tôi luôn phải mạo hiểm với những điều đó và không bao giờ nên xử lý chúng ngoài "cuộc gọi Vứt bỏ". Và không nên thực sự làm bất cứ điều gì với những người đó. Họ trong thực tế đi mà không đề cập đến trong bất kỳ tài liệu. | Ngoại lệ xương? Luôn luôn sửa chúng. | Các trường hợp ngoại lệ là các ứng cử viên chính cho việc nuốt / xử lý, như trong TryPude () | Ngoại sinh? Cũng nên luôn luôn được xử lý. Thường thì bạn cũng muốn nói với người dùng về chúng và đăng nhập chúng. Nhưng nếu không, chúng không đáng để giết quá trình của bạn.
Christopher

@Vlad Tôi đã tra cứu SqlConnection.Dispose (). Thậm chí không quan tâm gửi bất cứ điều gì đến Máy chủ về việc kết nối đã kết thúc. Một cái gì đó vẫn có thể xảy ra như là kết quả của NativeMethods.UnmapViewOfFile();NativeMethods.CloseHandle(). Nhưng những người được nhập khẩu từ bên ngoài. Không có kiểm tra bất kỳ giá trị trả về hoặc bất kỳ giá trị nào khác có thể được sử dụng để có được Ngoại lệ .NET thích hợp xung quanh bất cứ điều gì hai cái đó có thể gặp phải. Vì vậy, tôi rất quan tâm, SqlConnection.Dispose (bool) đơn giản là không quan tâm. | Đóng là đẹp hơn rất nhiều, thực sự nói với máy chủ. Trước khi nó gọi vứt bỏ.
Christopher

1

Bạn có thể thử sử dụng AggregateException và sửa đổi mã của mình như thế này:

class Program 
{
    static async Task Main()
    {
        try
        {
            await using (var d = new D())
            {
                throw new ArgumentException("I'm inside using");
            }
        }
        catch (AggregateException ex)
        {
            ex.Handle(inner =>
            {
                if (inner is Exception)
                {
                    Console.WriteLine(e.Message);
                }
            });
        }
        catch (Exception e)
        {
            Console.WriteLine(e.Message);
        }
    }
}

class D : IAsyncDisposable
{
    public async ValueTask DisposeAsync()
    {
        await Task.Delay(1);
        throw new Exception("I'm inside dispose");
    }
}

https://docs.microsoft.com/ru-ru/dotnet/api/system.aggregateexception?view=netframework-4.8

https://docs.microsoft.com/ru-ru/dotnet/st Chuẩn / vô song-programming/exception-handling-task-abul-l Library

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.