Форум программистов, компьютерный форум, киберфорум
C# .NET
Войти
Регистрация
Восстановить пароль
 
 
Рейтинг 4.82/11: Рейтинг темы: голосов - 11, средняя оценка - 4.82
85 / 12 / 1
Регистрация: 24.05.2010
Сообщений: 590
1
.NET 4.x

Почему в async void методе не перехватывается исключение?

02.01.2019, 10:21. Показов 2240. Ответов 25

Привет!

В ряде статей читал о том, что в подобном случае исключение не перехватывается и программа в какой-то момент упадет:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
    class Program
    {
        private static async void ThrowExceptionAsync()
        {
            throw new InvalidOperationException();
        }
 
        static void Main(string[] args)
        {
            try
            {
                ThrowExceptionAsync();
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
            }
 
            Console.WriteLine("End");
            Console.ReadLine();
        }
    }
Так оно и есть на самом деле. Но я не понимаю почему. Код выполняется в одном и том же потоке. Т.к. в async методе нигде нету await, то ко всему прочему это ещё и синхронно выполняться должно. Нигде, где я об этом читал, не написано доходчиво из-за чего это происходит.

Кто-то может пояснить из-за чего так? Или нужно это просто запомнить?
__________________
Помощь в написании контрольных, курсовых и дипломных работ здесь
0
Programming
Эксперт
94731 / 64177 / 26122
Регистрация: 12.04.2006
Сообщений: 116,782
02.01.2019, 10:21
Ответы с готовыми решениями:

Почему не перехватывается исключение типа double?
#include <iostream> #include <conio.h> #include <cstring> using namespace std; void obrob(int...

не перехватывается исключение, хоть по книге c++ за 21 день и должно
#include <exception> #include <iostream> #include <string> using namespace std; class...

Async await ожидание void
await Task.WaitAll(tasks); Как можно реализовать? Если делать без асинхронности то получается...

Зачем async void требует await?
Привет! У меня есть метод который долго кропотливо кое-что рисует, я его обозначил как async void,...

25
Эксперт .NET
15566 / 11814 / 3099
Регистрация: 17.09.2011
Сообщений: 19,728
02.01.2019, 13:43 2
Цитата Сообщение от V0fka Посмотреть сообщение
из-за чего так?
Для одинакового поведения с исключениями в методах, возвращающих Task и в методах-итераторах.
Если не ожидать асинхронный метод, возвращающий Task, то исключение тоже не возникнет до тех пор, пока вы не попытаетесь извлечь результат из таски через await или через Result:
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
    class Program
    {
        private static async Task ThrowExceptionAsync()
        {
            throw new InvalidOperationException();
        }
 
        static void Main(string[] args)
        {
            try
            {
                // Исключения не будет
                ThrowExceptionAsync();
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
            }
 
            Console.WriteLine("End");
            Console.ReadLine();
        }
    }
Ну а поскольку асинхронные void-методы нельзя ожидать и нет таски, чтобы узнать об их исключениях, а с выпавшим исключением что-то делать надо, постольку было принято решение валить приложение.

Цитата Сообщение от V0fka Посмотреть сообщение
Или нужно это просто запомнить?
Запомните вот это: если в асинхронном void-методе ожидаются исключения, то ловить их надо там же.
2
85 / 12 / 1
Регистрация: 24.05.2010
Сообщений: 590
03.01.2019, 14:33  [ТС] 3
kolorotur, спасибо за ответ!
0
TheGreatCornholio
1244 / 722 / 285
Регистрация: 30.07.2015
Сообщений: 2,398
03.01.2019, 18:09 4
Цитата Сообщение от V0fka Посмотреть сообщение
Или нужно это просто запомнить?
Просто не используйте async void, кроме как в местах, где это приходится
использовать для обеспечения совместимости с legacy кодом:
В вашей консольной программе легче и намного лучше будет обойтись без этого, используя async Task
но, например, для WinForm's event handlers нужно следовать рекомендациям выше:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
        private async void button1_Click(object sender, EventArgs e)
        {
            try
            {
                LockUI(); //lock some controls
 
                await Task.Delay(1000); //your async code
 
                Log("Done..."); //show message
            }
            catch (Exception ex)
            {
                Log(ex.Message);
            }
            finally
            { 
                UnlockUI();
            }
        }
1
85 / 12 / 1
Регистрация: 24.05.2010
Сообщений: 590
09.01.2019, 11:38  [ТС] 5
Woldemar89, хорошо, спасибо! Думал, может просто есть какое-то "доходчивое" объяснение почему такое поведение. Но если нету, придется просто запомнить .
0
Эксперт .NET
15566 / 11814 / 3099
Регистрация: 17.09.2011
Сообщений: 19,728
09.01.2019, 12:11 6
Цитата Сообщение от V0fka Посмотреть сообщение
Думал, может просто есть какое-то "доходчивое" объяснение почему такое поведение.
Так выше же оно дано
Цитата Сообщение от kolorotur Посмотреть сообщение
Для одинакового поведения с исключениями в методах, возвращающих Task и в методах-итераторах.
0
85 / 12 / 1
Регистрация: 24.05.2010
Сообщений: 590
09.01.2019, 18:21  [ТС] 7
Цитата Сообщение от kolorotur Посмотреть сообщение
Так выше же оно дано
Проблема в том, что я не знаю что происходит с исключениями в методах-итераторах. Я вообще эту фразу, если честно, не совсем понял.
0
Эксперт .NET
15566 / 11814 / 3099
Регистрация: 17.09.2011
Сообщений: 19,728
09.01.2019, 18:39 8
Цитата Сообщение от V0fka Посмотреть сообщение
я не знаю что происходит с исключениями в методах-итераторах.
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static void Main()
{
    IEnumerable<int> iterator = null;
    try { iterator = Enumerate(); }
    catch (Exception ex) { Console.WriteLine(ex); }
 
    if (iterator != null)
    {
        foreach (int item in iterator)
            Console.WriteLine(item);
    }
}
 
static IEnumerable<int> Enumerate()
{
    throw new Exception("Kaboom!");
    yield return 1;
}
Поведение аналогичное.
1
85 / 12 / 1
Регистрация: 24.05.2010
Сообщений: 590
10.01.2019, 14:13  [ТС] 9
kolorotur, я не понимаю, что общего между методом-итератором и async void методом. В итераторе исключение будет в момент обращения к перечислителю, а в async void в непредсказуемый момент. Хотя async void метод по сути это же обычный синхронный метод, в котором можно использовать ключевое слово await, который по сути можно переписать без async и вместо использования await писать var result = GetMyAsyncMethod().Result. Или я тут тоже что-то не понимаю?
0
Эксперт .NET
15566 / 11814 / 3099
Регистрация: 17.09.2011
Сообщений: 19,728
10.01.2019, 16:15 10
Цитата Сообщение от V0fka Посмотреть сообщение
я не понимаю, что общего между методом-итератором и async void методом.
Отложенное выполнение.

Цитата Сообщение от V0fka Посмотреть сообщение
async void метод по сути это же обычный синхронный метод
Ну видите же, что не обычный!
Метод, возаращающий IEnumerable тоже может быть "обычным".
Измените метод Enumerate в примере выше на такой и сравните разницу:
C#
1
2
3
4
5
static IEnumerable<int> Enumerate()
{
    throw new Exception("Kaboom!");
    return Enumerable.Range(1, 10);
}
Внешне, с точки зрения пользователя метода, ничего не изменилось.
А поведение совсем другое.

Добавлено через 1 минуту
Цитата Сообщение от V0fka Посмотреть сообщение
В итераторе исключение будет в момент обращения к перечислителю, а в async void в непредсказуемый момент.
Правильно, потому что в отличие от перечислителя у вас нет контроля над выполнением асинхронного метода, возвращающего void.
А с исключением что-то делать надо.
1
904 / 661 / 318
Регистрация: 23.10.2016
Сообщений: 1,538
10.01.2019, 17:58 11
Цитата Сообщение от V0fka Посмотреть сообщение
Так оно и есть на самом деле. Но я не понимаю почему. Код выполняется в одном и том же потоке. Т.к. в async методе нигде нету await, то ко всему прочему это ещё и синхронно выполняться должно. Нигде, где я об этом читал, не написано доходчиво из-за чего это происходит.
Компилятор C# переписывает код метода отмеченного как async. В результате чего изначальный код заворачивается в код инфраструктурный. В частности, добавляется захват текущего контекста синхронизации при вызове этого метода:
C#
1
var context = SynchronizationContext.Current;
Также производится обработка возникшего исключения.

asyn void отличается от async Task тем, что
1) Вызывается context.OperationStarted() при вызове метода
2) Вызывается context.OperationCompleted() при завершении метода (не зависимо от того, завершился ли метод с исключением или нет)
3) При возникновении необработанного исключения оно будет перехвачено инфраструктурным кодом и будет вызван метод context.Post(...), в который будет передан делегат выбрасывающий это исключение заново

Если context==null, то пункты 1-2 не выполняются, а возникшее исключение будет равносильно исключению выброшенному в пуле потоков, что приводит к срабатыванию AppDomain.UnhandledException с последующим крашем приложения.

Для улучшения понимания предлагаю поэкспериментировать с этим кодом:
Кликните здесь для просмотра всего текста
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
static ManualResetEventSlim sync = new ManualResetEventSlim(false);
 
void Main()
{
    SynchronizationContext.SetSynchronizationContext(new CustomContext());
    Run();
    sync.Wait();
}
 
async void Run()
{
    Console.WriteLine("Part I");
    await Task.Delay(1000);
    
    Console.WriteLine("Part II");
    await Task.Delay(1000).ConfigureAwait(false);
    
    Console.WriteLine("Part III");
    await Task.Delay(1000).ConfigureAwait(true);
    
    Console.WriteLine("Part IV");
    throw new Exception("Exception in part IV");
    await Task.Delay(1000);
    
    Console.WriteLine("Part V");
}
 
class CustomContext : SynchronizationContext
{
    public override void OperationStarted()
    {
        Console.WriteLine();
        Console.WriteLine("OperationStarted");
        Console.WriteLine();
    }
 
    public override void OperationCompleted()
    {
        Console.WriteLine();
        Console.WriteLine("OperationCompleted");
        Console.WriteLine();
        
        sync.Set();
    }
 
    public override void Post(SendOrPostCallback d, object state)
    {
        Console.WriteLine();
        Console.WriteLine("<Posted>");
        Console.Write("    ");
        try
        {
            d.Invoke(state);
        }
        catch (Exception e)
        {
            Console.WriteLine($"Exception thrown with message '{e.Message}'");
        }
        Console.WriteLine("</Posted>");
        Console.WriteLine();
    }
}
1
Эксперт .NET
15566 / 11814 / 3099
Регистрация: 17.09.2011
Сообщений: 19,728
10.01.2019, 19:18 12
Цитата Сообщение от TopLayer Посмотреть сообщение
Компилятор C# переписывает код метода отмеченного как async.
Какой код генерирует компилятор — это уже следствие.
Причина — это почему он генерирует такой код, а не какой-то иной
0
904 / 661 / 318
Регистрация: 23.10.2016
Сообщений: 1,538
11.01.2019, 05:56 13
kolorotur, возможно я не так понял вопрос. Ну хорошо, попробую объяснить причины именной такой реализации.

Я думаю, что сделано всё так, а не иначе, для унификации обработки исключений в async методах. Ко всем исключениям в async методе отношение одинаковое и не зависит от синхронности/асинхронности его вбрасывания: для async Task исключение всегда связывается с возвращаемой задачей, а для async void всегда перевбрасывается в захваченным методом контексте синхронизации (ну или в пуле потоков, если этого контекста не было).

Если бы эскалация исключения зависела от синхронности/асинхронности вбрасывания исключения, как предложил ТС, то всё стало бы только сложнее. Да, следующий метод всегда выполняется синхронно и мог бы совершенно спокойно обрабатываться во внешнем try/catch блоке:
C#
1
2
3
4
async void Run()
{
    throw new Exception();
}
Но давайте теперь рассмотрим другой метод:
C#
1
2
3
4
5
async void Run()
{
    await someTask;
    throw new Exception();
}
Этот метод может выполнится как синхронно, так и асинхронно. В первым случае он перехватится try/catch блоком, а во втором - нет. Получаем два сценария эскалации исключения вместо одного. Например, сейчас мы можем перехватить все возможные исключения в async void установив свой SynchronizationContext, а если бы было по-другому, то в дополнение к этому пришлось бы ещё и каждый вызов async void заворачивать в try/catch.

Кстати, неплохо бы понимать, что следующий метод может выполнится полностью синхронно:
C#
1
2
3
4
5
async void Run()
{
    await Task.Delay(100500);
    throw new Exception(); 
}
1
TheGreatCornholio
1244 / 722 / 285
Регистрация: 30.07.2015
Сообщений: 2,398
13.01.2019, 14:44 14
Цитата Сообщение от TopLayer Посмотреть сообщение
Кстати, неплохо бы понимать, что следующий метод может выполнится полностью синхронно
Вы имели ввиду 'однопоточно' наверное?
Цитата Сообщение от TopLayer Посмотреть сообщение
В первым случае он перехватится try/catch блоком, а во втором - нет.
Вы это проверили?) Не должно так быть, в обоих примерах исключение не должно перехватится..

В случае с async void исключение будет выброшено в контекст синхронизации, который был активен на момент вызова метода,
и не важно идет ли исключение до или после await, и есть ли вообще await в методе.

SynchronizationContext
Контекст синхронизации представляет собой планировщик - класс унаследованный от
SynchronizationContext, реализующий методы

C#
1
2
public virtual void Post(SendOrPostCallback d, object state); //async
public virtual void Send(SendOrPostCallback d, object state); //sync
принимающие делегаты, помещающие их в очередь на выполнение.

Различные фреймворки имеют различные реализации, гарантирующие (или нет) порядок выполнения делегатов итд

WindowsFormsSynchronizationContext (System.Windows.Forms.dll: System.Windows.Forms)
DispatcherSynchronizationContext (WindowsBase.dll: System.Windows.Threading) WPF
AspNetSynchronizationContext (System.Web.dll: System.Web)
итд

SynchronizationContext.Current==null в консольных приложениях и службах Windows, что, в свою очередь,
означает, что используется дефолтная реализация SynchronizationContext, работающая на ThreadPool.QueueUserWorkItem:
Default (ThreadPool) SynchronizationContext (mscorlib.dll: System.Threading)

Task Parallel Library (TPL) TaskScheduler.FromCurrentSynchronizationContext (System.Threading.Tasks)
TaskScheduler работает примерно также как и дефолтный SynchronizationContext помещая Task'и на выполнение в ThreadPool


Обычные методы выбрасывают исключение в вызывающий код, если метод async - метод будет скомпилирован иначе.

await же, позволяет извлечь результат или исключение из метода async Task в вызывающий код,
и, соотвественно может работать только с Task, так как await всего лишь синтаксический сахар,
и "под капотом" используется метод Task.GetAwaiter (начиная с C# 7.0 асинхронные методы могут возвращать любой тип, имеющий доступный метод GetAwaiter).

Раз уж нельзя await'ить async void и, как следствие, пробросить исключение в вызывающий код,
как я упоминал в начале, исключение будет выброшено в контекст синхронизации и приложение грохнется...

kolorotur, поправьте, пожалуйста, если что не так..
0
904 / 661 / 318
Регистрация: 23.10.2016
Сообщений: 1,538
13.01.2019, 15:17 15
Цитата Сообщение от Woldemar89 Посмотреть сообщение
Вы имели ввиду 'однопоточно' наверное?
Не суть
Цитата Сообщение от Woldemar89 Посмотреть сообщение
Вы это проверили?) Не должно так быть, в обоих примерах исключение не должно перехватится..
Вы упустили вводную:
Цитата Сообщение от TopLayer Посмотреть сообщение
Если бы эскалация исключения зависела от синхронности/асинхронности вбрасывания исключения, как предложил ТС, то
Цитата Сообщение от Woldemar89 Посмотреть сообщение
await же, позволяет извлечь результат или исключение из метода async Task в вызывающий код
Не обязательно async Task, просто Task тоже подойдёт.
0
TheGreatCornholio
1244 / 722 / 285
Регистрация: 30.07.2015
Сообщений: 2,398
13.01.2019, 15:28 16
Цитата Сообщение от TopLayer Посмотреть сообщение
Не суть
Суть, - синхронно означает блокировку потока, а await - неблокирующий вызов.

Насколько я помню работу Task.Delay, этот код будет выполнен асинхронно одним потоком, не понимаю, что значит
Цитата Сообщение от TopLayer Посмотреть сообщение
следующий метод может выполнится полностью синхронно:
C#
1
2
3
4
5
async void Run()
{
* * await Task.Delay(100500);
* * throw new Exception(); 
}
?
0
904 / 661 / 318
Регистрация: 23.10.2016
Сообщений: 1,538
13.01.2019, 16:33 17
Цитата Сообщение от Woldemar89 Посмотреть сообщение
Суть, - синхронно означает блокировку потока, а await - неблокирующий вызов.
Причём тут await? Не суть синхронно или однопоточно. Важно то, что метод вернёт управление только после выполнения синхронной его части, т.е. вызывающий код дождётся выполнения синхронного куска и, соответственно, мог бы (если бы да кабы) обработать там же возникшее исключение.
Цитата Сообщение от Woldemar89 Посмотреть сообщение
Насколько я помню работу Task.Delay, этот код будет выполнен асинхронно одним потоком, не понимаю, что значит
Суть в том, что код исполняется не в реалтаймовой среде, а значит Task.Delay может вернуть уже выполненную задачу и исполнение оставшейся части метода начнётся немедленно в том же потоке.
0
TheGreatCornholio
1244 / 722 / 285
Регистрация: 30.07.2015
Сообщений: 2,398
13.01.2019, 16:53 18
Цитата Сообщение от TopLayer Посмотреть сообщение
Важно то, что метод вернёт управление только после выполнения синхронной его части
await как раз таки подразумевает возврат управления:

C#
1
2
3
4
5
async void Run()
{
     await Task.Delay(100500);
     throw new Exception();
}
Это происходит в 3-й строчке - поток покидает метод (считай что return), начинается ожидание,
когда ожидание закончится, поток снова войдет в метод и продолжит выполнение оставшегося кода после await.
Насколько я знаю, Task.Delay - не CPU-Bound, и доп. потоков создано не будет - это и есть асинхронное выполнение в один поток.
Вы сказали, что этот метод может быть выполнен синхронно - как?

Добавлено через 38 секунд
Цитата Сообщение от TopLayer Посмотреть сообщение
Суть в том, что код исполняется не в реалтаймовой среде, а значит Task.Delay может вернуть уже выполненную задачу и исполнение оставшейся части метода начнётся немедленно в том же потоке.
А тут вообще не понял, можете пояснить?
0
904 / 661 / 318
Регистрация: 23.10.2016
Сообщений: 1,538
13.01.2019, 17:14 19
Цитата Сообщение от Woldemar89 Посмотреть сообщение
А тут вообще не понял, можете пояснить?
Ну лагнёт комп после того, как будильник в Task.Delay был активирован, но до того как этот метод вернёт задачу.
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Task MyDelay(int milliseconds)
{
    var delay = Task.Delay(milliseconds);
    
    // имитируем лаг (например, текущий поток был вытеснен высокоприоритетным потоком)
    Thread.Sleep(milliseconds + 500);
    return delay;
}
 
async Task Do()
{
    Console.WriteLine("Do started");
    await MyDelay(1000).ConfigureAwait(false);
    Console.WriteLine("Do completed");
}
 
void Main()
{
    SynchronizationContext.SetSynchronizationContext(null);
    Do();
    Console.WriteLine("Main completed!");
}
Добавлено через 1 минуту
Несмотря на, то что я не ожидал завершения метода Do, он всё равно завершился перед выводом "Main completed!". То есть синхронно выполнился.
0
TheGreatCornholio
1244 / 722 / 285
Регистрация: 30.07.2015
Сообщений: 2,398
13.01.2019, 18:11 20
Цитата Сообщение от TopLayer Посмотреть сообщение
Несмотря на, то что я не ожидал завершения метода Do, он всё равно завершился перед выводом "Main completed!". То есть синхронно выполнился.
Да, он выполнился синхронно, но потому что у Вас весь код полностью синхронный, ожидание или неожидание Do тут не причем, позволил себе немного переписать.


C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
using System;
using System.Threading;
using System.Threading.Tasks;
 
namespace ConsoleApp1
{
    class Program
    {
        static void Main()
        {
            //SynchronizationContext.Current уже null в Console и Windows Service
            SynchronizationContext.SetSynchronizationContext(null);
 
            Do();
 
            Console.WriteLine("Main completed!");
            Console.ReadKey(true);
        }
 
        /*
        static async Task Main()
        {
            await Do();
 
            Console.WriteLine("Main completed!");
            Console.ReadKey(true);
        }
        */
 
        static Task MyDelay(int milliseconds)
        {
            Thread.Sleep(milliseconds + 500);
            
            return Task.CompletedTask;
        }
 
        static async Task Do()
        {
            Console.WriteLine("Do started");
 
            var md = MyDelay(1000); //синхронный блокирующий вызов - MyDelay возвращает завершенный Task после блокирующего ожидания 
 
            var mdcaf = md.ConfigureAwait(false); //зачем?
 
            await mdcaf; //и когда вы ожидаете завершение завершенного Task'a это также происходит синхронно
 
            Console.WriteLine("Do completed");
        }//следовательно весь метод выполняется синхронно
    }
}
await метода возвращающего завершенный Task, как и метода async Task не cодержащего внутри await выполняется синхронно.
0
IT_Exp
Эксперт
87844 / 49110 / 22898
Регистрация: 17.06.2006
Сообщений: 92,604
13.01.2019, 18:11

Что такое void в методе?
что в данном случае делает метод void и как он работает? http://priscree.ru/img/397d9fa96dcc5b.jpg

Void menu(void); что это ? почему не void menu();
void menu(void); что это ? почему не void menu(); void naprimer(void); и это идет в классе это...

Исключение в методе
Есть два варианта кода, первый работает, а второй нет, почему? различие в том, что в первом...

WPF async void fun() для событий контролла. (MapCOntrol.WPF)
Задача: Отмечаем чекбокс элементы и рисуем их на карте. При нажатии на кнопку выполняется...


Искать еще темы с ответами

Или воспользуйтесь поиском по форуму:
20
Ответ Создать тему
Опции темы

КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2022, CyberForum.ru