При работе в проектах связанными с 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