問題描述
CancellationTokenSource.Cancel 引發 ObjectDisposedException (CancellationTokenSource.Cancel throws an ObjectDisposedException)
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 ofDispose
, which must only be used when all other operations on theCancellationTokenRegistration
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.
‑‑‑‑‑
參考解法
方法 1:
This isn't really interesting but I wrapped Cancel
and Dispose
into a try‑catch that swallows ObjectDisposedException
and haven't had problems since.
方法 2:
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.
(by Dan Abramov、Dan Abramov、Marc Cayuela)