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, вы можете использовать оператор с нулевым условием.

Другие вопросы по тегам