Что такое sync over async deadlock

Posted by PDSW on Saturday, March 17, 2018
Last Modified on Thursday, September 20, 2018

Как вы помните из поста о контексте синхронизации есть такие типы ресурсов, которые доступны исключительно в контексте одного потока. Для работы с такими ресурсами в асинхронном коде, нужно явным образом указывать, что продолжать выполнятся на нужно именно в том потоке который владеет этим ресурсом. Непонимая этого можно перейти на новый концепт программирования как ниже на картинке.

multithreading
новый концепт программирования

Вспомним пример из предудущего поста. По нажатию на кнопку нужно вызвать длительную задачу. Так как наш код асинхронный, код длительной задачи нужно разместить в синхронном коде обработчика нажатия кнопки, для этого воспользуемся техникой sync over async. Т.е. просто дождёмся завершения асинхронной задачи при помощи метода taskName.Wait().

private void ButtonBase_OnClick_Async(object sender, RoutedEventArgs e)
{
	Task task = Run();
	
	// sync over async technic

	task.Wait(); // task.Result or etc
}

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

private void ButtonBase_OnClick_Async(object sender, RoutedEventArgs e)
{
	Button.IsEnabled = false; // выключаем кнопку
	
	Task task = Run();
	task.Wait();
	Button.IsEnabled = true; // включаем кнопку
	
}

private Task Run()
{
	return Task.Run(() =>
	{
		Task.Delay(1000);
	}).ContinueWith(_ =>
	{
		UpdateUI("finished");    
	}, TaskScheduler.FromCurrentSynchronizationContext());
}

private void UpdateUI(object state)
{
	Label.Content = state;
}

Запустив приложение и нажав на кнопку мы получим дэдлок. Давайте разберёмся откуда он мог взяться.

Sync Over Async Deadlock

На первом этапе в теле обработчика ButtonBase_OnClick_Async мы находимся в контексте UI-потока приложения. Метод Run и таск будут выполнятся в нём же. Пока всё очевидно, но что должно произойти дальше? Таск главного потока в обработчике синхронного кода ждёт когда же он освободится. В то время как продолжение (указанное с помощью метода ContinueWith), не может получить этот UI поток. Такое поведение получило название Sync Over Async Deadlock.

Ещё раз в коде:

private void ButtonBase_OnClick_Async(object sender, RoutedEventArgs e)
{
	Button.IsEnabled = false;
	// 1. здесь ui поток отработает
	
	Task task = Run();
	// 3. здесь ui поток завершения всей задачи
	
	task.Wait();
	Button.IsEnabled = true;
}

private Task Run()
{   
    // 2. здесь мы моментально отработаем и вернём запущеный таск

    return Task.Run(() =>
	{
		// 4. подождём и отпустим ui поток
		 
		Task.Delay(1000);
	}).ContinueWith(_ =>
	{
		// 5. сюда уже никогда не попадём,
		
		// ведь на шаге 3 мы ждём ui поток
		 
		UpdateUI("finished");
	}, TaskScheduler.FromCurrentSynchronizationContext());
}

private void UpdateUI(object state)
{
	Label.Content = state;
}

Разобравшись в сути проблемы нужно продумать как же устранить эту ошибку.

Решаем проблему deadlock-а

Одним из самых простых решений если нам не нужен контекст ui-потока, то нужно просто указать это. Если это действительно так то можно решить вот так:

private Task Run()
{
	return Task.Run(() =>
	{
		Task.Delay(1000);
	}).ContinueWith(_ =>
	{
		NotWorkWithUiResource("finished");
	});
	
	// Это будет равнозначно следующему коду
	
	// }, TaskScheduler.Default); 

}

// Этот метод не работает с ресурсами ui

private void NotWorkWithUiResource(object state) { }

Здесь используется TaskScheduler.Default планировщик пула потоков, т.е. будет взят не UI-поток, а любой свободный поток из пула потоков.

Если же нам нужен контекст ui-потока, то придётся убрать Sync Over Async, и продолжать работу в асинхронном коде дальше:

private void ButtonBase_OnClick_Async(object sender, RoutedEventArgs e)
{
	Button.IsEnabled = false;
	Run().ContinueWith(_ => 
	{
		Button.IsEnabled = true;
	}, TaskScheduler.FromCurrentSynchronizationContext());
}

Всё что было написано в отношении асинхронного кода на чистом TPL так же относится и даже в большей степени к коду написанному на async\await.

Рекомендую к просмотру видео Сергея Теплякова где описывается решение подобной проблемы.

Ссылки:

  1. Don’t Block on Async Code - Stephen Cleary

comments powered by Disqus