Pergunta

I have a class that owns a CancellationTokenSource.

public class GrabboxCell : UICollectionViewCell
{
    CancellationTokenSource _tokenSource = new CancellationTokenSource ();

    // ...
}

I'm using current token to start some long-running operations.

My object also needs to support “recycling”. Think reincarnation. All long-running operations started during previous life must be cancelled.

In this case I call Cancel and Dispose on the source, and issue a new token source:

void CancelToken (bool createNew)
{
    _tokenSource.Cancel ();
    _tokenSource.Dispose ();
    _tokenSource = null;

    if (createNew) {
        _tokenSource = new CancellationTokenSource ();
    }
}

I call this method in two places: when I want the token to expire and when this class is disposed.

public override void PrepareForReuse ()
{
    CancelToken (true);
    base.PrepareForReuse ();
}

protected override void Dispose (bool disposing)
{
    CancelToken (false);
    base.Dispose (disposing);
}

Sometimes I'm getting an ObjectDisposedException when calling _tokenSource.Cancel () from my Dispose method. Documentation says:

All public and protected members of CancellationTokenRegistration are thread-safe and may be used concurrently from multiple threads, with the exception of Dispose, which must only be used when all other operations on the CancellationTokenRegistration have completed.

I'm not sure what to do at this moment. Wrap CancelToken in a lock?
Where exactly does the race condition happen and how to mitigate it?

I know for sure that PrepareForReuse is always called on the same thread, but Dispose may be called on a different one.

If this is of any use, I'm running Mono and not .NET Framework but I'm pretty sure they should have the same semantics regarding cancellation tokens.

Foi útil?

Solução

This isn't really interesting but I wrapped Cancel and Dispose into a try-catch that swallows ObjectDisposedException and haven't had problems since.

Outras dicas

The operations being thread safe (individually) does not imply that your sequence of operations are executed at once. More specifically, since PrepareForReuse could run in a different thread as Dispose, then what could happen is that this:

_tokenSource.Cancel ();
_tokenSource.Dispose ();

is executed in one thread, then there is a Context Switch between threads before executing _tokenSource = null; and then another thread tries to run again the _tokenSource.Cancel (). But the tokenSource was disposed of already and not regenerated, since the first thread did not reach the last block of code of the cancel:

_tokenSource = new CancellationTokenSource ();

I would not be surprised either if you get from time to time a NullPointerException, if the context switch happened just after _tokenSource = null; instead of before as I explained (it is also possible).

To solve this problem I would lock your Cancel method so that the threads cannot run any part of the method before the other was finished.

Also, to protect for the NullPointerException, which can only happen if your method Dispose is called before PrepareForReuse, you can use the Null-Conditional Operator.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top