Правильний спосіб боротьби з винятками в AsyncDispose


20

Під час переходу на новий .NET Core 3 IAsynsDisposable, я натрапив на наступну проблему.

Суть проблеми: якщо DisposeAsyncкидає виняток, цей виняток приховує будь-які винятки, кинуті всередину await using-блок.

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");
    }
}

Що потрапляє - це AsyncDisposeвилучення, якщо воно кинене, і виняток зсередини, await usingлише якщо AsyncDisposeвін не кидає.

Однак я б вважав за краще навпаки: отримати виняток із await usingблоку, якщо це можливо, та DisposeAsync-виключити, лише якщо await usingблок успішно закінчився.

Обгрунтування: Уявіть, що мій клас Dпрацює з деякими мережевими ресурсами і підписується на деякі сповіщення віддалено. Код всередині await usingможе зробити щось не так і провалити канал зв'язку, після чого код у програмі Dispose, який намагається граціозно закрити зв’язок (наприклад, скасувати підписку на сповіщення), теж не вдасться. Але перший виняток дає мені реальну інформацію про проблему, а другий - лише вторинну проблему.

В іншому випадку, коли основна частина пробіглась, а знешкодження не вдалося, справжня проблема знаходиться всередині DisposeAsync, тож виняток із DisposeAsyncвідповідної. Це означає, що придушення всіх винятків всередині DisposeAsyncне повинно бути доброю ідеєю.


Я знаю, що існує однакова проблема із випадком, що не стосується асинхронізації: виняток finallyзаміняє виняток у try, тому не рекомендується запускати їх Dispose(). Але з класами доступу до мережі придушення винятків у методах закриття зовсім не виглядає добре.


Можна вирішити проблему за допомогою наступного помічника:

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 { }
        }
    }
}

і використовувати його як

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

який є некрасивим (і забороняє такі речі, як раннє повернення всередину використовуючого блоку).

Чи є добре, канонічне рішення, await usingякщо це можливо? Мій пошук в Інтернеті не знайшов навіть обговорення цієї проблеми.


1
" Але якщо доступ до мереж, які придушують винятки в методах закриття, зовсім не виглядає добре " - я думаю, що більшість мережевих класів BLC мають саме Closeцю методику. Напевно, розумно робити те саме: CloseAsyncспроби красиво закрити речі та кидає невдачу. DisposeAsyncпросто робить все можливе і мовчить.
canton7

@ canton7: ​​Ну, маючи окремий CloseAsyncзасіб, що мені потрібно вжити додаткових запобіжних заходів для його запуску. Якщо я просто покладу це в кінці using-блока, він буде пропущений на ранньому поверненні тощо (це те, що ми хотіли б, щоб це сталося) та винятки (це те, що ми хотіли б, щоб це сталося). Але ідея виглядає багатообіцяючо.
Влад

Існує причина, що багато стандартів кодування забороняють раннє повернення :) Там, де задіяні роботи в мережі, бути явним трохи не погане ІМО. Disposeзавжди було: "Можливо, все пішло не так: просто зробіть все можливе, щоб покращити ситуацію, але не погіршуйте її", і я не бачу, чому AsyncDisposeслід інакше.
canton7

@ canton7: ​​Що ж, за мовою з винятками, кожне твердження може бути достроковим поверненням: - \
Влад

Правильно, але вони будуть винятковими . У цьому випадку DisposeAsyncзробити все можливе, щоб привести в порядок, але не кидати - це правильно зробити. Ви говорили про навмисне раннє повернення, де навмисне раннє повернення може помилково обійти виклик CloseAsync: саме ті заборонені багатьма стандартами кодування.
canton7

Відповіді:


3

Є винятки, які ви хочете вивести на поверхню (перервати поточний запит або збити процес), і є винятки, що ваш дизайн очікується, що трапляються іноді, і ви можете впоратися з ними (наприклад, повторити і продовжити).

Але розрізняти ці два типи - до остаточного абонента коду - це вся суть винятків, щоб рішення залишати за викликом.

Іноді абонент надає більший пріоритет розкриттю винятку з вихідного кодового блоку, а іноді винятку з Dispose. Не існує загального правила вирішення питання, яке має мати пріоритет. CLR принаймні відповідає (як ви зазначали) між поведінкою синхронізації та неасинхронізацією.

Можливо, прикро, що зараз ми маємо AggregateExceptionпредставляти кілька винятків, і це не може бути дооснащено для вирішення цього питання. тобто, якщо виняток вже є в польоті, а інший кинуто, вони об'єднуються в AggregateException. catchМеханізм може бути змінений таким чином , що якщо ви пишете catch (MyException)то зловить , AggregateExceptionщо включає в себе виключення типу MyException. Однак, з цієї ідеї є різні інші ускладнення, і зараз, мабуть, занадто ризиковано модифікувати щось таке принципове.

Ви можете покращити свою UsingAsyncпідтримку якнайшвидшого повернення вартості:

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;
}

Тож я розумію правильно: ваша ідея полягає в тому, що в деяких випадках await usingможна використовувати просто стандарт (саме тут DisposeAsync не буде кидатись у нефатальній справі), а помічник подібний UsingAsyncє більш доречним (якщо DisposeAsync, ймовірно, кине) ? (Зрозуміло, мені потрібно змінити UsingAsyncтак, щоб воно не сліпо спіймало все, а лише нефатальне (і не з головою в користуванні Еріка Ліпперта ).)
Влад

@ Добре так - правильний підхід повністю залежить від контексту. Також зауважте, що UsingAsync не може бути записаний один раз для використання глобально правдивої категоризації типів винятків відповідно до того, чи слід їх ловити чи ні. Знову ж таки, це рішення потрібно приймати по-різному, залежно від ситуації. Коли Ерік Ліпперт говорить про ці категорії, вони не є сутнісними фактами про типи винятків. Категорія виду виключення залежить від вашого дизайну. Іноді проект IOException очікується, іноді ні.
Даніель

4

Можливо, ви вже розумієте, чому це відбувається, але це варто детально розібратися. Така поведінка не є специфічною для await using. Це сталося б і з простим usingблоком. Тож, поки я Dispose()тут кажу , все це стосується і цього DisposeAsync().

usingБлок просто синтаксичний цукор для try/ finallyблоку, як розділ зауваження документації говорить. Те, що ви бачите, відбувається тому, що finallyблок завжди працює, навіть після винятку. Отже, якщо виняток трапляється, а catchблоку немає , виняток ставиться на утримування до finallyзапуску блоку, а потім викидання викидається. Але якщо виняток трапиться в finally, ви ніколи не побачите старого винятку.

Ви можете побачити це на цьому прикладі:

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

Це не має значення, Dispose()чи DisposeAsync()викликається всередині finally. Поведінка однакова.

Перша моя думка: не кидайтеся Dispose(). Але після перегляду деяких власних кодів Microsoft, я думаю, це залежить.

Погляньте, наприклад, на їх реалізацію FileStream. І синхронний Dispose()метод, і DisposeAsync()може насправді викидати винятки. Синхронний Dispose()робить ігнорувати деякі винятки навмисно, але не всі.

Але я думаю, що важливо враховувати характер вашого класу. FileStreamНаприклад, у , наприклад, Dispose()буде передано буфер до файлової системи. Це дуже важливе завдання, і ви повинні знати, якщо це не вдалося . Ви не можете просто проігнорувати це.

Однак в інших типах об'єктів, коли ви телефонуєте Dispose(), ви справді більше не використовуєте об’єкт. Дзвінок Dispose()справді просто означає «цей об’єкт для мене мертвий». Можливо, це очищає виділену пам'ять, але помилка не впливає на роботу вашої програми. У такому випадку ви можете вирішити ігнорувати виняток усередині себе Dispose().

Але в будь-якому випадку, якщо ви хочете розрізнити виняток усередині usingабо виняток, який виникла Dispose(), вам знадобиться try/ catchблокувати всередині і зовні usingблоку:

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
}

Або ви просто не могли використовувати using. Випишіть try/ catch/ finallyблокувати себе, де ви ловите будь виключення в 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) / referenceource.microsoft.com (.NET Framework) набагато простіше переглядати, ніж GitHub
canton7

Спасибі за вашу відповідь! Я знаю, в чому полягає справжня причина (у питанні я згадав спробу / нарешті та синхронний випадок). Тепер про вашу пропозицію. catch Усередині в usingблоці буде не допоможе , тому що , як правило , обробка виключень робиться де - то далеко від usingсамого блоку, так його обробки всередині using, як правило , не надто можливо. Про використання "ні" using- це дійсно краще, ніж запропонований спосіб вирішення?
Влад

2
@ canton7 Дивовижно! Мені було відомо про referenceource.microsoft.com , але не знав, що існує еквівалент для .NET Core. Дякую!
Габріель Лучі

@Vlad "Краще" - це те, на що ти можеш відповісти. Я знаю, якби я читав чужий код, я вважав за краще бачити try/ catch/ finallyблокувати, оскільки було б відразу зрозуміло, що він робить, не потрібно читати, що AsyncUsingробить. Ви також зберігаєте можливість зробити дострокове повернення. Також буде додаткова вартість процесора AwaitUsing. Це було б мало, але воно є.
Габріель Лучі

2
@PauloMorgado Це просто означає, що Dispose()не слід кидати, оскільки його називають не раз. Власні реалізації Microsoft можуть спричинити винятки, і це не є причиною, як я показав у цій відповіді. Однак я погоджуюсь, що вам слід уникати цього, якщо це можливо, оскільки ніхто зазвичай не очікує, що він кинеться.
Габріель Лучі

4

ефективне використання коду обробки винятків (синтаксичний цукор для спроби ... нарешті ... Dispose ()).

Якщо ваш код обробки винятків кидає Винятки, щось по-справжньому заповнено.

Що б ще не трапилося, щоб ви навіть туди потрапили, насправді вже не матер. Код обробки несправних винятків приховує всі можливі винятки, так чи інакше. Код обробки винятків повинен бути фіксованим, що має абсолютний пріоритет. Без цього ви ніколи не отримуєте достатньо налагоджувальних даних для реальної проблеми. Я бачу, що це робиться неправильно дуже часто. Це приблизно так просто, як помилитися, як обробка голих покажчиків. Тому часто є дві статті про тематичні я посилання, які можуть допомогти вам у будь-яких основних помилках дизайну:

Залежно від класифікації винятків, це те, що вам потрібно зробити, якщо ваш код оброблення винятків / розпорядження видає виняток:

Для фатальних, кісткових та розгульних рішень рішення те саме.

Екзогенних винятків слід уникати навіть за серйозних витрат. Існує причина, що ми все ще використовуємо файли журналів, а не бази даних журналів для журналу винятків - Операції з БД - це лише спосіб схильності до виникнення екзогенних проблем. Журнали - це єдиний випадок, коли я навіть не заперечую, якщо ви триматимете файл File Hand Open весь час виконання.

Якщо вам вдалося перервати зв’язок, не хвилюйтеся надто про інший кінець. Поводьтеся з нею, як робить UDP: "Я надішлю інформацію, але мені байдуже, чи отримає її інша сторона". Утилізація - це очищення ресурсів на стороні клієнта / стороні, над якою ви працюєте.

Я можу спробувати повідомити їх. Але чищення матеріалів на стороні Server / FS? Тобто те , що їх тайм - аути і їх обробка винятків несе відповідальність.


Тож ваша пропозиція ефективно зводиться до придушення винятків при закритті з'єднання, правда?
Влад

@Владні екзогенні? Звичайно. Утилізація / фіналізатор є для того, щоб прибирати з себе. Швидше за все, якщо закрити екземпляр Conneciton через виняток, ви зробите це тим, що ви більше не матимете робочого з'єднання з ними. І який би сенс було отримати виняток "Без зв'язку" під час обробки попереднього винятку "Без зв'язку"? Ви надсилаєте сингл "Йо, я закриваю це з'єднання", де ви ігноруєте всі екзогенні винятки або навіть якщо вони наближаються до цілі. Afaik типові реалізації Dispose вже роблять це.
Крістофер

@Vlad: Я зазначив, що є купа речей, з яких ніколи не слід викидати винятки (крім випадків, які мають фатальний зловживання). Введіть Initliaizers в списку. Розпорядження також є одним із таких: "Щоб забезпечити, щоб ресурси завжди були очищені належним чином, метод розпорядження повинен називатися кілька разів без викидів". docs.microsoft.com/en-us/dotnet/standard/garbage-collection/…
Крістофер

@Vlad Шанс смертельного винятку? Ми завжди повинні ризикувати з цими і ніколи не повинні поводитися з ними за межами "виклику розпорядження". І насправді нічого з цим не слід робити. Вони фактично йдуть без згадки в жодній документації. | Винятки з кістковими головами? Завжди їх фіксуйте. | Винятки щодо неприємних відчуттів є головними кандидатами на ковтання / поводження, як у TryParse () | Екзогенні? Також завжди слід обробляти. Часто ви також хочете повідомити користувачеві про них і ввійти до них. Але в іншому випадку не варто вбивати ваш процес.
Крістофер

@Vlad Я подивився на SqlConnection.Dispose (). Навіть байдуже надсилати щось на Сервер про те, що з'єднання закінчено. Щось все-таки може статися в результаті NativeMethods.UnmapViewOfFile();і NativeMethods.CloseHandle(). Але вони імпортуються із зовнішньої сторони. Немає перевірки будь-якого повернутого значення чи іншого, що могло б бути використане для отримання належного .NET-винятку навколо того, що ці двоє можуть зіткнутися. Тому я настійно припускаю, що SqlConnection.Dispose (bool) просто не хвилює. | Закрити це набагато приємніше, насправді це говорить сервер. Перед тим, як зателефонувати, розпоряджайтесь.
Крістофер

1

Ви можете спробувати використовувати AggregateException і змінити свій код приблизно так:

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/standard/parallel-programming/exception-handling-task-parallel-library

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.