💙 Help Ukraine, click for information 💛

Контекст синхронизации при работе с Task и Thread

Apr. 12, 2018

При работе в проектах связанными с ui-control или asp.net часто приходится работать с ресурсами доступными в локальном хранилище потока (thread-local storage TLS). К слову: такое поведение можно задать при помощи аттрибута [ThreadStatic] или класса ThreadLocal<T>. При обращении к ним из другого потока платформа dotnet выбросит исключение System.InvalidOperationException с текстом ‘The calling thread cannot access this object because a different thread owns it.’.

Представим себе ситуацию когда в каком-то win-form приложении нам по нажатию кнопки нужно вызвать долгоиграющую задачу. Очевидно, что нужно было-бы создать поток и в этом потоке выполнить эту задачу. Проблематично будет отобразить результат выполнения из созданного потока на UI - ведь ресурсы UI принадлежат одному потоку, а обращаемся мы к нему в другом. Это и есть та вышеописанная ситуация, когда мы столкнёмся с System.InvalidOperationException. Для решения этой проблемы нам придётся подружиться с контекстом синхронизации.

типова проблама багатопоточності

Контекст синхронизации

Контекст синхронизации SynchronizationContext это абстракция позволяющая задать где, в каком потоке, должен выполнится код. В каждом потоке есть своя сущьность SynchronizationContext ассоциированная с этим потоком. Она находится в поле SynchronizationContext.Current, но это опціонально.

Делегаты переданные в методы контекста синхронизации send и post будут вызваны в этом потоке. post - это асинхронная неблокирующая версия send метода.

Повторив теорию мы готовы на практике реализовать решение задачи. Посмотрите на следующий код:

private void ButtonBase_OnClick_Async(object sender, RoutedEventArgs e)
{
// смотрим текущий идентификатор потока
int id = Thread.CurrentThread.ManagedThreadId;
Trace.WriteLine("ButtonBase_OnClick_Async thread: " + id);

	SynchronizationContext uiContext = SynchronizationContext.Current;

	// Создаём поток и ассоциируем его с методом
	Thread thread = new Thread(Run);
	thread.Start(uiContext);
}

private void Run(object state)
{
// Смотрим ещё раз поток
int id = Thread.CurrentThread.ManagedThreadId;
Trace.WriteLine("Run thread: " + id);

	// востанавливаем контекст синхронизации из стейта потока
	SynchronizationContext uiContext = state as SynchronizationContext;

	// эмулируем долгое выполнение задачи
	Thread.Sleep(1000);
	
	// вызываем продолжение в UI потоке 
	uiContext.Post(UpdateUI, "finished");

}

// Этот метод будет вызван в UI потоке
private void UpdateUI(object state)
{
int id = Thread.CurrentThread.ManagedThreadId;
Trace.WriteLine("UpdateUI thread:" + id);
Label.Content = state;
}

А давайте это перепишем на TPL?!

В вашем проекте скорее всего уже есть TPL, а значит глупо всё оставлять в том виде как это было ранее. После перевода потоков в таски наш код станет немного проще:

private void ButtonBase_OnClick_Async(object sender, RoutedEventArgs e)
{
// смотрим текущий идентификатор потока
int id = Thread.CurrentThread.ManagedThreadId;
Trace.WriteLine("ButtonBase_OnClick_Async thread: " + id);

	Task task = Run();
}

private Task Run()
{
return Task.Run(() =>
{
// смотрим текущий идентификатор потока
int id = Thread.CurrentThread.ManagedThreadId;
Trace.WriteLine("Run thread: " + id);

		// эмулируем долгое выполнение задачи
		Task.Delay(1000);
	}).ContinueWith(_ =>
	{
		UpdateUI("finished");
	}, TaskScheduler.FromCurrentSynchronizationContext()); // вызываем продолжение в UI потоке
}

private void UpdateUI(object state)
{
int id = Thread.CurrentThread.ManagedThreadId;
Trace.WriteLine("UpdateUI thread:" + id);
Label.Content = state;
}

Обратите внимание на следующий код:

.ContinueWith(_ =>
{
UpdateUI("finished");    
}, TaskScheduler.FromCurrentSynchronizationContext())

Метод ContinueWith содержит ряд полезных перегрузок и наверное заслуживает большего внимания, но сейчас при помощи него мы указываем в каком потоке нужно выполнить продолжение - вторым параметром мы указали использовать в продолжении поток из текущего контекста синхронизации TaskScheduler.FromCurrentSynchronizationContext()

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

Ccылка пока одна: Parallel Computing - It’s All About the SynchronizationContext

comments powered by Disqus