Skip to content

Instantly share code, notes, and snippets.

@quetzalcoatl
Created October 6, 2020 22:26
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save quetzalcoatl/10ce9806d97551b8e4a89b4e2d7b9132 to your computer and use it in GitHub Desktop.
Save quetzalcoatl/10ce9806d97551b8e4a89b4e2d7b9132 to your computer and use it in GitHub Desktop.
There are several ways to do that, but first some facts:
- your code is executed by some thread, if you did not start more of them, then probably by the main thread of the application
- that isPrime function is probably executed by 1 specif thread that may get stuck in that method for a longer time
- it's hard to tell a thread to stop doing it's piece of work if that piece of work is not designed to be stoppable
Regarding last point: it seems easy - you can just kill the thread (unless it's THE main thread) or you can call thread.Abort(),
but both methods are dangerous, and in most cases that's a bad idea, and explaining that is another story.
So, let's first focus on "not designed to be stoppable".
Imagine .. something silly. Silly solutions are still some solutions.
Please tread this code as pseudocode, minor fixes may be needed.
public static bool isPrime(BigInteger num) {
DateTime startedAt = DateTime.Now;
if (num <= 1) return false;
if (num == 2) return true;
for (int i = 3; i <= num / 2; ++i)
{
if (num % i == 0)
return false;
if(DateTime.Now - startedAt > TimeSpan.FromHours(1))
throw new TimeoutException();
}
return true;
}
I suppose that doesn't need an explanation. And it seems silly. But no doubt it will work just fine.
The most important observation here are:
- the 'work' must take care of being cancellable on its own, somehow
- something must take care of observing how the time passes
- the 'work' must have some way of notifying the caller about the time-out
Let's assume you don't want to put the burden of observing time in your math algo.
Something else will observe the time, and tell the algo to stop at some moment.
.Net comes with a CancellationToken/CancellationTokenSource classes.
CancellationToken can be basically used as a boolean "stop now" flag:
public static bool? isPrime(BigInteger num, CancellationToken ct) {
if (num <= 1) return false;
if (num == 2) return true;
for (int i = 3; i <= num / 2 && !ct.IsCancellationRequested; ++i)
if (num % i == 0)
return false;
return ct.IsCancellationRequested ? null : true;
}
Naturally, when we break a loop, some result should still be produced,
ideally a result that can be distinguished from normal result, so I changed it to bool?/null.
CancellationToken also comes with a handy "throw something for me when cancelled" method:
public static bool isPrime(BigInteger num, CancellationToken ct) {
if (num <= 1) return false;
if (num == 2) return true;
for (int i = 3; i <= num / 2; ++i)
{
ct.ThrowIfCancellationRequested();
if (num % i == 0)
return false;
}
return true;
}
The exception thrown is OperationCancelledException and it has some handy support from .Net in certain places.
Like not killing your application if it bubbles up as unhandled-exception from a side thread, etc.
Now, let's use the isPrime with a CancellationToken!
void FooBar()
{
CancellationTokenSource ctSrc = new CancellationTokenSource();
BigInteger num = ....;
bool? result = isPrime(num, ctSrc.Token);
Console.WriteLine(...., result);
// ctSrc.Cancel(); ??
}
That's just that, the Source is a factory/manager object that gives us the tokens,
it also has a Cancel() method that marks the tokens as Cancelled, so easy-peasy..
..but who and when will call the 'cancel()' method?
We can't cancel before calling isPrime, it would not make any sense.
We can't cancel after calling isPrime, because our current thread is busy in that method for the next 3 days.
If something is meant to observe time, it cannot be THIS thread. We need ANOTHER one.
CancellationTokenSource ctSrc = new CancellationTokenSource();
void FooBar()
{
BigInteger num = ....;
Thread watchdog = new Thread(Watchdog);
watchdog.Start();
bool? result = isPrime(num, ctSrc.Token);
Console.WriteLine(...., result);
// probably kill the watchdog thread if isPrime managed to finish in time
}
void Watchdog()
{
Thread.Sleep( 1 hour );
ctSrc.Cancel();
}
I hope what happens here this is more-or-less self-descriptive.
Another thread is started and just waits for 1hour then cancels the token.
If isPrime didn't finish in time, it will notice the token is cancelled and will stop.
If isPrime did finish.. then noone is watching the token anymore.
Thread will still wake up after 1 hour and cancel the token though it will have no effect.
It might be a good idea to clean up the thread when isPrime finishes in time.
That's the absolutely basic general idea about cancellation of any work pieces.
If you add to it more tools/libraries/etc like anon-functions, Tasks, Observables, etc
it will look more pretty or will take care of the cleanup for you more or less better
but at its code, it will stay the same:
- something must watch the time
- workpiece must discover it's cancelled
- it's a good idea to distinguish a cancelled workpiece from finished workpiece
@quetzalcoatl
Copy link
Author

Oh right, making the isPrime return a Task instead of just bool is also a great idea, as great as CancellationTokens.
Task<> is a generic wrapper that holds information about 'status' of a piece of work

  • did it finish? what's the result then?
  • or maybe it was cancelled?
  • did it crash? what was the exception then?
  • or maybe it's still running?

It doesn't transparently handle cancellation, your workpiece still has to support it, and it's still your job to give a CancellationToken to your workpiece, but Task<> covers whole "let the caller know about state of the job" part and also supports async/await/etc goodies.

@quetzalcoatl
Copy link
Author

@nicomp42
Copy link

nicomp42 commented Oct 7, 2020

This is a great explanation. Very informative.

@zacharba
Copy link

zacharba commented Oct 7, 2020

This was a really good answer to my question, in my research, I hadn't come across the cancellation tokens. So thank you for explaining that to me. One question I would like to add here is that you mentioned observing time is a silly solution, and I am just curious as to why that would be silly.

Otherwise, Thank you for the answer, like above, it was very informative.

@quetzalcoatl
Copy link
Author

@zacharba Maybe I chose a wrong word. "Silly" was the first thing that came to my mind and I just later repeated it as a pointer to indicate which code example I refer to..

Instead of "silly", probably "direct" or "trivial" or "straightforward" would be more appropriate. Or something that convey all those three meanings at the same time.

  • direct, because it's right here, obvious, all right before your eyes
  • straightforward, because it feels like the first thing that comes to mind if one's to think about how to solve "It has to stop itself" and "it has to stop after X minutes" and "nobody can stop it for me".. just add "check time & stop" to the code in question
  • trivial, because it doesn't use any "interesting" tools/features/mechanisms/etc

So.. out of a better word, I wrote "silly", but it's definitely not pejorative. It's fine, it works, etc. It's more like calling a kid "silly", because the kid did something but not like "the adults would do". Something along the lines... Because when I see code like that, I know that the most likely reason is that either someone was in hurry and wrote the simplest thing and had no time or motivation to polish it later, or didn't know CancellationTokens, or for whatever reasons ignored that:

  • .net base class library has CancellationTokens, and if something's going to be cancelled, it's the 'landmark' for that feature; like async/await for non-blocking, like Task/IPropertyChanged/etc; seeing a "landmark' class, most programmers immediately know what's going on and will expect these to be used for that precise function of theirs, so not using it feels weird or lacking
  • when you use a library and see DoTheTimeyThing(int a, string b, object c) inside, you don't see that it you may abort it at your discretion, when you see DoTheTimeyThing(int a, string b, object c, CancellationToken xx) you have quite a hint
  • for long term maintenance and evolution, it's good to separate concerns, it's good to keep the algorithm itself as clean as possible, and keep other concerns as away as possible. Dropping an easy to read one-liner "token.MaybeThrow();" here and there is one thing, and throwing 2-3 lines of time-diffing and if-over-threshold-then-throw is another.
  • time-diffing is easy, but imagine that the cancellation-reasons may be multiple and, like time, totally not related to the algorithm: time, user cancelled, network connection lost, database conflict detected elsewhere rendering current work obsolete, etc. Scattering such elaborate checks over the algo would be huge no-no
  • etc..

..but, there are cases when such "silly/obvious/direct/straighforward/trivial/easy/(...)" solution is fine, it's all tradeoffs. If the code is really non-typical, like, performance-critical, low-on-resources, like can't have that one more watchdog thread, or can't expect thread contexts to be switched regularily, or if there's no chance that the cancellation-reasons ever get more complex, or (..) then it may totally be fine to not use CancellationTokens and use something more basic. Oh. Maybe that would be the best word?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment