12

I want to use lock or a similar synchronization to protect a critical section. At the same time I want to listen to a CancellationToken.

Right now I'm using a mutex like this, but mutex doesn't have as good performance. Can I use any of other synchronization classes (including the new .Net 4.0) instead of the mutex?

WaitHandle.WaitAny(new[] { CancelToken.WaitHandle, _mutex});
CancelToken.ThrowIfCancellationRequested();
i3arnon
  • 113,022
  • 33
  • 324
  • 344
Karsten
  • 8,015
  • 8
  • 48
  • 83

3 Answers3

19

Take a look at the new .NET 4.0 Framework feature SemaphoreSlim Class. It provides SemaphoreSlim.Wait(CancellationToken) method.

Blocks the current thread until it can enter the SemaphoreSlim, while observing a CancellationToken

From some point of view using Semaphore in such simple case could be an overhead because initially it was designed to provide an access for multiple threads, but perhaps you might find it useful.

EDIT: The code snippet

CancellationToken token = new CancellationToken();            
SemaphoreSlim semaphore = new SemaphoreSlim(1,1);
bool tokenCanceled = false;

try {
   try {
      // block section entrance for other threads
      semaphore.Wait(token);
   }
   catch (OperationCanceledException) {
      // The token was canceled and the semaphore was NOT entered...
      tokenCanceled = true;
   }
   // critical section code
   // ...
   if (token.IsCancellationRequested)
   {
       // ...
   }
}
finally { 
   if (!tokenCanceled)
      semaphore.Release();
}
Eric
  • 6,364
  • 1
  • 32
  • 49
sll
  • 61,540
  • 22
  • 104
  • 156
  • 1
    Please, put some try-finally around :-) – xanatos Sep 14 '11 at 14:48
  • @xanatos : you mean there is any exception? This is pseudo code to demonstrate how SemaphoreSlim tied with CancellationToken and no more :) – sll Sep 14 '11 at 14:50
  • @xanatos ... why not do so yourself? – Random Dev Sep 14 '11 at 14:51
  • 3
    @sll ... no you should do this to make sure that the semaphore is released no matter what – Random Dev Sep 14 '11 at 14:52
  • @Carsten König: Thanks for enhancing the snippet! – sll Sep 14 '11 at 14:52
  • Ah... and as a small note...I think it's funny that after adding the `ref lockTacken` to `Monitor.Enter`/`Monitor.Exit` they didn't do the same for `SemaphoreSlim`. You don't have any way to know if you have taken the Semaphore or not :-) – xanatos Sep 14 '11 at 14:52
  • @Carsten It's HIS reply. I edit replies only to adjust grammar/orthography. I live by this rule :-) Yes it was what I meant with try/finally – xanatos Sep 14 '11 at 14:54
  • @xanatos : You can check SemaphoreSlim.CurrentCount, but why you need to know whether Semaphore is taken? – sll Sep 14 '11 at 14:55
  • @sll There was a tricky problem with < 4.0 Monitor.Enter and Monitor.Exit. They changed their working to compensate for it. The newer version (used by the `lock` keyword) accepts a `ref bool lockTacken`. See this: http://oganix.wordpress.com/2010/09/09/lock-statement-in-c-4/ Here the problem is the same. If there is a tricky exception between the `Wait` and the `try` you'll have a locked semaphore. No good. – xanatos Sep 14 '11 at 15:00
  • @xanatos: no problem - but if I can make the best answer to a good question better than I'll try - of course make some comment/note on the edit – Random Dev Sep 14 '11 at 15:01
  • @sll No, you can't really. The CurrentCount could be because someone else has the lock. The CurrentCount is the TOTAL for the semaphore. There isn't a "per thread" count. – xanatos Sep 14 '11 at 15:01
  • @xanatos : CurrentCount shows how many threads can enter a semaphore, for maxCount=1: CurrentCount==0 indicates that single thread already entered – sll Sep 14 '11 at 15:04
  • @sll Yes, but you don't know if it's you or someone else. Let's say that you can have an exception in the middle of Wait, that this won't break the semaphore (it will still be usable). If you have the try AFTER the Wait then if the Wait takes the lock and before returning there is an exception you can't Release it. No good. So you move the try BEFORE the Wait. Now let's say that someone else has the Semaphore. CurrentCount = 0. You enter the try and the Wait. The Wait throws. Can you say if CurrentCount = 0 because YOU catched the semaphore or not? – xanatos Sep 14 '11 at 15:09
  • @sll The opposite situation. The try BEFORE the Wait. Now you catch the Semaphore. CurrentCount = 0. Before returning from the Wait, there is an Exception. You are in the finally. CurrentCount = 0. Should you Release the semaphore or not? And how should you distinguish this case from the last one? – xanatos Sep 14 '11 at 15:11
  • @sll What you wrote is currect. This problem was present in .NET <= 3.5 and it isn't a real problem. It can happen only in corner cases. It's very very difficult that a non critical (program terminating) Exception is throw between the `Wait` and the `try`. The most common case is `Thread.Abort`. This is the reason why you shouldn't EVER EVER use `Thread.Abort` – xanatos Sep 14 '11 at 15:14
  • @xanatos : 1) You should not enter Wait() if CurrentCount==0. 2) I believe this is not a real case when single thread can enter semaphore twice, an I right? – sll Sep 14 '11 at 15:15
  • @sll ??? If CurrentCount == 0 you should use the Wait() to wait your turn! You don't check before entering! It's the Wait() that checks for you! Are you checking CurrentCount == 0 in your example? And I don't comprehend your second question. – xanatos Sep 14 '11 at 15:20
  • @xanatos : second quuestion is regarding your note "Can you say if CurrentCount = 0 because YOU catched the semaphore or not?". I believe single thread can't enter semaphore (or any another block of code) twice because thread is synchronous in terms of code instruction executions, – sll Sep 14 '11 at 15:26
  • @sll You are entering up to one time... It's a little complex. I'll try to explain me better in an hour or so – xanatos Sep 14 '11 at 16:10
  • @sll The argument is surely interesting (I'm always very interested in threading :-) ), but it isn't something I can explan it here in 3 lines at a time. BUT if you are really interested, you can always put a question on SO like "Can you leak slots with SemaphoreSlim"? :-) :-) (I'm kidding but not so much. If you are really interested, SO is the right place to ask these questions) – xanatos Sep 14 '11 at 18:34
  • @sll 8.5 years late, but I fixed it for you. semaphore.Wait(token) will throw an exception and NOT enter the semaphore if the token is canceled. Releasing the semaphore if that happens causes other exceptions to be thrown. – Eric Jan 29 '20 at 01:57
1
private object _lockObject = new object();

lock (_lockObject)
{  
   // critical section  
   using (token.Register(() => token.ThrowIfCancellationRequested())
   {
       // Do something that might need cancelling. 
   }
}

Calling Cancel() on a token will result in the ThrowIfCancellationRequested() being invoked as that was what is hooked up to the Register callback. You can put whatever cancellation logic you want in here. This approach is great because you can cancel blocking calls by forcing the conditions that will cause the call to complete.

ThrowIfCancellationRequested throws a OperationCanceledException. You need to handle this on the calling thread or your whole process could be brought down. A simple way of doing this is by starting your task using the Task class which will aggregate all the exceptions up for you to handle on the calling thread.

try
{
   var t = new Task(() => LongRunningMethod());
   t.Start();
   t.Wait();
}
catch (AggregateException ex)
{
   ex.Handle(x => true); // this effectively swallows any exceptions
}

Some good stuff here covering co-operative cancellation

Peter Kelly
  • 14,253
  • 6
  • 54
  • 63
  • If you mean an OperationCanceledException then that is by design - that is what is thrown by the ThrowIfCancellationRequested method. You should handle it on the caller using the recommended pattern (updated answer). – Peter Kelly Sep 14 '11 at 15:01
  • Regarding your statement "Calling `Cancel()` on a t̶o̶k̶e̶n̶ C̲a̲n̲c̲e̲l̲l̲a̲t̲i̲o̲n̲T̲o̲k̲e̲n̲S̲o̲u̲r̲c̲e̲ will result in the `ThrowIfCancellationRequested()` being invoked" ... this behavior can be controlled by using the [Cancel(bool)](https://msdn.microsoft.com/en-us/library/dd321703(v=vs.110).aspx) overload. – Glenn Slayden Mar 22 '17 at 02:33
  • Registering `ThrowIfCancellationRequested` as a cancellation action doesn't make any sense at all. It just means that whoever calls `CancellationTokenSource.Cancel` will get an exception and it doesn't even do anything at all for the token's recipient. – relatively_random Mar 07 '22 at 12:46
0

You can use Monitor.TryEnter with timeout to wait for the lock and check periodically for cancellation.

private bool TryEnterSyncLock(object syncObject)
{
    while(!Monitor.TryEnter(syncObject, TimeSpan.FromMilliseconds(100)))
    {
        if (cts_.IsCancellationRequested)
            return false;
    }

    return true;
}

Note that I would not recommend this in high contention situations as it can impact performance. I would use it as a safety mechanism against deadlocks in case you cannot use SemaphoreSlim as it has different same thread re-entrancy semantics than Monitor.Enter.

After returning true, lock on syncObject has to be released using Monitor.Exit.

ghord
  • 13,260
  • 6
  • 44
  • 69