No, what you have is not safe. The check to see if _count >= MAXIMUM
could race with the call to Interlocked.Increment
from another thread. This is actually really hard to solve using low-lock techniques. To get this to work properly you need to make a series of several operations appear atomic without using a lock. That is the hard part. The series of operations in question here are:
- Read
_count
- Test
_count >= MAXIMUM
- Make a decision based on the above.
- Increment
_count
depending on the decision made.
If you do not make all 4 of these steps appear atomic then there will be a race condition. The standard pattern for performing a complex operation without taking a lock is as follows.
public static T InterlockedOperation<T>(ref T location)
{
T initial, computed;
do
{
initial = location;
computed = op(initial); // where op() represents the operation
}
while (Interlocked.CompareExchange(ref location, computed, initial) != initial);
return computed;
}
Notice what is happening. The operation is repeatedly performed until the ICX operation determines that the initial value has not changed between the time it was first read and the time the attempt was made to change it. This is the standard pattern and the magic all happens because of the CompareExchange
(ICX) call. Note, however, that this does not take into account the ABA problem.1
What you could do:
So taking the above pattern and incorporating it into your code would result in this.
public void CheckForWork()
{
int initial, computed;
do
{
initial = _count;
computed = initial < MAXIMUM ? initial + 1 : initial;
}
while (Interlocked.CompareExchange(ref _count, computed, initial) != initial);
if (replacement > initial)
{
Task.Run(() => Work());
}
}
Personally, I would punt on the low-lock strategy entirely. There are several problems with what I presented above.
- This might actually run slower than taking a hard lock. The reasons are difficult to explain and outside the scope of my answer.
- Any deviation from what is above will likely cause the code to fail. Yes, it really is that brittle.
- It is hard to understand. I mean look at it. It is ugly.
What you should do:
Going with the hard lock route your code might look like this.
private object _lock = new object();
private int _count;
public void CheckForWork()
{
lock (_lock)
{
if (_count >= MAXIMUM) return;
_count++;
}
Task.Run(() => Work());
}
public void CompletedWorkHandler()
{
lock (_lock)
{
_count--;
}
}
Notice that this is much simpler and considerably less error prone. You may actually find that this approach (hard lock) is actually faster than what I showed above (low lock). Again, the reason is tricky and there are techniques that can be used to speed things up, but it outside the scope of this answer.
1The ABA problem is not really an issue in this case because the logic does not depend on _count
remaining unchanged. It only matters that its value is the same at two points in time regardless of what happened in between. In other words the problem can be reduced to one in which it seemed like the value did not change even though in reality it may have.