Как BackgroundWorker.CancellationPending потокобезопасен?
Способ отмены операции BackgroundWorker заключается в вызове BackgroundWorker.CancelAsync()
:
// RUNNING IN UI THREAD
private void cancelButton_Click(object sender, EventArgs e)
{
backgroundWorker.CancelAsync();
}
В обработчике событий BackgroundWorker.DoWork мы проверяем BackgroundWorker.CancellationPending
:
// RUNNING IN WORKER THREAD
void backgroundWorker_DoWork(object sender, DoWorkEventArgs e)
{
while (!backgroundWorker.CancellationPending) {
DoSomething();
}
}
Вышеупомянутая идея существует во всем Интернете, в том числе на странице MSDN для BackgroundWorker.
Теперь, мой вопрос таков: как это безопасно?
Я просмотрел класс BackgroundWorker в ILSpy - CancelAsync()
просто устанавливает cancelationPending на true без использования барьера памяти, а CancellationPending
просто возвращает cancelationPending без использования барьера памяти.
Согласно этой странице Jon Skeet, вышесказанное не является потокобезопасным. Но документация для BackgroundWorker.CancellationPending говорит:" Это свойство предназначено для использования рабочим потоком, который должен периодически проверять отмену отмены и отменять фоновый режим если для него установлено значение true.
Что здесь происходит? Это поточно-безопасный или нет?
Ответы
Ответ 1
Это поточно-безопасный.
Код
while (!backgroundWorker.CancellationPending)
читает свойство, и компилятор знает, что он не может кэшировать результат.
И так как в обычной структуре каждая запись такая же, как и volatileWrite, метод CancelAsync() может просто установить поле.
Ответ 2
Это потому, что BackgroundWorker наследует от Component, который наследуется от MarshalByRefObject. Объект MBRO может находиться на другом компьютере или в другом процессе или приложении. Это работает при наличии объекта, олицетворяемого прокси-сервером, который имеет все те же самые свойства и методы, но чьи реализации маршалируют вызов через провод.
Один из побочных эффектов заключается в том, что джиттер не может встроить методы и свойства, которые нарушили бы прокси. Это также предотвращает любые оптимизации, которые сохраняют значение поля в регистре cpu. Так же, как volatile делает.
Ответ 3
Хороший момент и очень справедливое сомнение. Похоже, что это не потокобезопасно. Я думаю, что MSDN также имеет такое же сомнение и почему это происходит под BackgroundWorker.CancelAsync Method.
Внимание
Помните, что ваш код в обработчике событий DoWork может завершить работа в качестве запроса на отмену, и ваш цикл опроса может пропустить CancellationPending, установленное в true. В этом случае Отмененный флаг System.ComponentModel.RunWorkerCompletedEventArgs в ваш обработчик события RunWorkerCompleted не будет установлен в true, даже хотя был сделан запрос на отмену. Эта ситуация называется и является общей проблемой при многопоточном программировании. Дополнительные сведения о проблемах с многопоточным дизайном см. В разделе Управляемые Рекомендации по использованию Threading.
Я не уверен в реализации класса BackgroundWorker. В этом случае важно использовать свойство BackgroundWorker.WorkerSupportsCancellation.
Ответ 4
Вопрос может быть интерпретирован двумя способами:
1) Правильно ли используется BackgroundWorker.CancellationPending реализация?
Это неверно, потому что это может привести к тому, что запрос аннулирования останется незамеченным. Реализация использует обычное чтение из поля поддержки. Если это поле поддержки обновляется другими потоками, обновление может быть невидимым для кода чтения. Это выглядит так:
// non volatile variable:
private Boolean cancellationPending;
public Boolean CancellationPending {
get {
// ordinary read:
return cancellationPending;
}
}
Правильная реализация попытается прочитать самое последнее значение. Этого можно добиться, объявив поле "volatile", используя барьер памяти или блокировку. Вероятно, есть другие варианты, и некоторые из них лучше других, но это зависит от команды, которая владеет классом BackgroundWorker.
2) Является ли код использует BackgroundWorker.CancellationPending правильно?
while (!backgroundWorker.CancellationPending) {
DoSomething();
}
Этот код верен. Цикл будет вращаться, пока CancellationPending не вернет true. Помните, что свойства С# - это просто синтаксический сахар для методов CLR. На уровне IL это еще один метод, который будет выглядеть как "get_CancellationPending". Возвращаемые значения метода не кэшируются при вызове кода (возможно, потому, что слишком сложно определить, имеет ли метод побочные эффекты).