CancellationTokenSource.Cancel генерирует исключение ObjectDisposedException
У меня есть класс, который владеет CancellationTokenSource
,
public class GrabboxCell : UICollectionViewCell
{
CancellationTokenSource _tokenSource = new CancellationTokenSource ();
// ...
}
Я использую текущий токен для запуска некоторых длительных операций.
Мой объект также должен поддерживать "переработку". Подумайте о реинкарнации. Все длительные операции, начатые в течение предыдущей жизни, должны быть отменены.
В этом случае я звоню Cancel
а также Dispose
на источнике и выдайте новый источник токена:
void CancelToken (bool createNew)
{
_tokenSource.Cancel ();
_tokenSource.Dispose ();
_tokenSource = null;
if (createNew) {
_tokenSource = new CancellationTokenSource ();
}
}
Я вызываю этот метод в двух местах: когда я хочу, чтобы токен истек, и когда этот класс удаляется.
public override void PrepareForReuse ()
{
CancelToken (true);
base.PrepareForReuse ();
}
protected override void Dispose (bool disposing)
{
CancelToken (false);
base.Dispose (disposing);
}
Иногда я получаю ObjectDisposedException
при звонке _tokenSource.Cancel ()
от моего Dispose
метод. Документация гласит:
Все публичные и защищенные члены
CancellationTokenRegistration
являются потокобезопасными и могут использоваться одновременно из нескольких потоков, за исключениемDispose
, который должен использоваться только тогда, когда все другие операции наCancellationTokenRegistration
завершено.
Я не уверен, что делать в этот момент. Заворачивать CancelToken
в lock
?
Где именно происходит состояние гонки и как его смягчить?
Я точно знаю что PrepareForReuse
всегда вызывается в одном потоке, но Dispose
может быть вызван на другой.
Если это пригодится, я использую Mono, а не.NET Framework, но я уверен, что они должны иметь одинаковую семантику в отношении токенов отмены.
1 ответ
Это не очень интересно, но я завернул Cancel
а также Dispose
в пробную ловушку, которая глотает ObjectDisposedException
и с тех пор не было проблем.
Потокобезопасность операций (по отдельности) не означает, что ваша последовательность операций выполняется сразу. В частности, посколькуPrepareForReuse
может работать в другом потоке как Dispose
, то может произойти следующее:
_tokenSource.Cancel ();
_tokenSource.Dispose ();
выполняется в одном потоке, то перед выполнением происходит переключение контекста между потоками _tokenSource = null;
а затем другой поток пытается снова запустить _tokenSource.Cancel ()
. Но tokenSource уже был удален и не регенерировался, так как первый поток не достиг последнего блока кода отмены:
_tokenSource = new CancellationTokenSource ();
Я тоже не удивлюсь, если вы будете время от времени NullPointerException
, если переключение контекста произошло сразу после _tokenSource = null;
вместо того, как я объяснил ранее (это тоже возможно).
Чтобы решить эту проблему, я бы заблокировал ваш Cancel
так что потоки не могут запустить какую-либо часть метода до того, как другая будет завершена.
Кроме того, чтобы защитить NullPointerException
, что может произойти, только если ваш метод Dispose
называется раньше PrepareForReuse
, вы можете использовать оператор с нулевым условием.