Подтвердить что ты не робот

Async-ждут продолжения всплесков - ведут себя по-другому?

У меня есть код winform, который запускается после нажатия кнопки:

void button1_Click(object sender, EventArgs e)
{
    AAA();
}


async Task BBB(  int delay)
{
    await Task.Delay(TimeSpan.FromSeconds(delay));
    MessageBox.Show("hello");  
}

async Task AAA()
{
    var task1 = BBB(1);  // <--- notice delay=1;  
    var task2 = BBB(1);  // <--- notice delay=1;  
    var task3 = BBB(1);  // <--- notice delay=1;  
    await Task.WhenAll(task1, task2, task3);
}

Вопрос:

Почему я вижу один MessageBox в то время, когда delay=1:

введите описание изображения здесь

Но если я изменю задержку на: 1,2,3 -

    var task1 = BBB(1);  
    var task2 = BBB(2);  
    var task3 = BBB(3);  

Я вижу - 3 ящика сообщений, даже не щелкая на любом ящике сообщений?

введите описание изображения здесь

4b9b3361

Ответ 1

Обратите внимание, что вложенные петли сообщений являются злыми, потому что неожиданное повторное включение - это просто слишком тяжело (tm).

Я думаю, что для объяснения этого поведения есть два ключевых понимания. Во-первых, асинхронные продолжения - как и все остальные "запустить этот произвольный код" - сообщения Win32 - имеют более высокий приоритет, чем другие сообщения. Во-вторых, существует давняя традиция Win32 отправки сообщений и синхронная блокировка ответа при запуске вложенного цикла сообщений. (На мой взгляд, это мое личное мнение, что этот ужасный дизайн, основанный на всех версиях Win32 API, отвечает за подавляющее большинство ошибок приложений в Windows).

Если вы запустите свой код таким образом, чтобы сохранить следы стека, вы можете более четко увидеть, что происходит:

void button1_Click(object sender, EventArgs e)
{
    AAA();
}

private List<string> stacks = new List<string>();

async Task BBB(int delay)
{
    await Task.Delay(TimeSpan.FromSeconds(delay));
    var stack = new StackTrace().ToString();
    stacks.Add(stack);
    MessageBox.Show(stack);
}

async Task AAA()
{
    var task1 = BBB(1);  // <--- notice delay=1;  
    var task2 = BBB(1);  // <--- notice delay=1;  
    var task3 = BBB(1);  // <--- notice delay=1;  
    await Task.WhenAll(task1, task2, task3);
    Clipboard.SetText(string.Join("\r\n\r\n", stacks));
}

Сравните тексты диалоговых окон (самый большой стек сначала, а затем средний, а затем самый маленький) с буфером обмена после того, как все диалоги закрыты (сначала самые маленькие, затем средний, затем самый большой). Ясно, что диалоги отображаются в обратном порядке.

Я считаю, что что-то подобное происходит, но не хватает уверенности, чтобы точно сказать:

  • Первая задержка срабатывает и вызывает MessageBox.Show.
  • Функция Win32 MessageBox запускает вложенный цикл сообщений и начинает настройку фактического диалога с сообщениями для себя (т.е. установка заголовка, текста и т.д.). Обратите внимание, что эти вызовы вызывают сообщения о насосе, но они еще не готовы показать диалог.
  • Вторая задержка срабатывает и перескакивает перед этими сообщениями настройки со своим собственным вызовом MessageBox.Show.
  • Аналогично для третьей задержки. Третье окно сообщения о задержке фактически завершает настройку и отображается. Два других окна сообщений по-прежнему (синхронно) ждут, когда их циклы сообщений возвращают значение, но поскольку эти циклы запускаются кодом, они не могут вернуться.

Когда вы изменяете тайминги на 1, 2, 3, вы все равно получите одинаковые стеки в буфере обмена, но вы увидите, что диалоговые тексты теперь в порядке (наименьший стек сначала, а затем средний, а затем самый большой). Это связано с тем, что у каждого MessageBox.Show есть достаточно времени, чтобы настроить окно сообщения и установить его цикл сообщения и показать диалог перед следующим одним слоем поверх него.

Теоретически этому нечетному поведению можно было бы избежать с помощью API MessageBox.ShowAsync, который полностью избегает вложенных циклов. Однако я бы не затаил дыхание.