1

Here is the code:

private int _count = 0;
public bool Ongoing
{
    get
    {
        return _count > 0;
    }
}

public void Method1(object param)
{
    new Thread(new ParameterizedThreadStart(Method2)).Start(param);
}
private void Method2(object param)
{
    _count++;
    lock (_lock)
        {
            // Something
        }
    _count--;
}

The variable _count, as you can guess, is used to count up how many threads are locked. It is initialized to 0 and only modified inside this method and I use it to know if the class is doing something.

Now the problem: sometimes _count goes below 0. It's like _count++ get sometimes ignored.

This happens very seldom, like about once every 500 times I start this method, maybe even less.

Should I declare _count as volatile maybe?

  • 5
    [Interlocked.Increment Method](https://msdn.microsoft.com/en-us/library/system.threading.interlocked.increment(v=vs.110).aspx). – 001 Jun 13 '17 at 20:14
  • @JohnnyMopp Why in the world would you use two separate synchronization methods at the same exact time. The code *is already taking out a lock*. – Servy Jun 13 '17 at 20:21
  • @Servy Because the increment happens outside the lock. – 001 Jun 13 '17 at 20:31
  • @JohnnyMopp Yes, and it shouldn't. *That is the problem*. – Servy Jun 13 '17 at 20:32
  • Here's a dup: [Is there any way to determine the number of threads waiting to lock in C#?](https://stackoverflow.com/q/4455024/669576) – 001 Jun 13 '17 at 20:35
  • 1
    And another: [How to get the amount of threads waiting to enter a lock?](https://stackoverflow.com/q/28784953/669576) – 001 Jun 13 '17 at 20:36
  • Be very careful on how you're going to use `Ongoing`. Making `_count` accurate does not make `Ongoing` inherently useful unless you know exactly when `Method1`/`Method2` can be called. If you want to know if any threads are holding the lock, `Monitor.TryEnter` with a zero timeout is reliable. Specifically, any code of the form `if (!Ongoing) { ... *something involving stuff protected by the lock* }` will not work correctly if other code could be firing up a thread in the background. – Jeroen Mostert Jun 13 '17 at 20:39
  • Sorry, I actually searched it up but I used the wrong keywords apparently. I focused on this problem alone, not as a general one. – FloydCrimson Jun 13 '17 at 20:40
  • @JeroenMostert Thanks for the advice. I only check `Ongoing` when the user wants to close the form to notify it that it's wtill working. – FloydCrimson Jun 13 '17 at 20:52

2 Answers2

1

You need to ensure the operations are applied atomically and the value isn't cached. To achieve the first you must use Interlocked.Increment and Interlocked.Decrement, for the second mark the field as volatile:

private volatile int _count = 0;
public bool Ongoing
{
    get
    {
        return _count > 0;
    }
}

public void Method1(object param)
{
    new Thread(new ParameterizedThreadStart(Method2)).Start(param);
}
private void Method2(object param)
{
    Interlocked.Increment(ref _count);
    lock (_lock)
        {
            // Something
        }
    Interlocked.Decrement(ref _count);
}
Gusman
  • 14,905
  • 2
  • 34
  • 50
  • Marking the field as `volatile` is not the best approach for getting a fresh read. You would do better with an explicit memory barrier in the getter, just before the read. – Douglas Jun 13 '17 at 20:34
  • @Douglas Is there an added value in this case in doing an explicit memory barrier rather than a `Volatile.Read(ref _count)`? – Kevin Gosse Jun 13 '17 at 20:37
  • 1
    @KevinGosse: Yes. A volatile read introduces a half-fence *after* the read, meaning you'll still get the stale value on the first read. – Douglas Jun 14 '17 at 07:52
  • @Douglas Though if that's really the case, then the MSDN documentation is wrong: https://msdn.microsoft.com/en-us/library/gg712971(v=vs.110).aspx `This value is the latest written by any processor in the computer, regardless of the number of processors or the state of processor cache.` – Kevin Gosse Jun 14 '17 at 11:41
  • @KevinGosse: The MSDN documentation is abysmally wrong. "The MSDN documentation states that use of the volatile keyword ensures that the most up-to-date value is present in the field at all times. This is incorrect, since as we’ve seen [in the given example], a write followed by a read can be reordered." (http://www.albahari.com/threading/part4.aspx) – Douglas Jun 14 '17 at 17:19
1

As has been correctly pointed out in the accepted answer, you need to perform thread synchronization around the increment and decrement operations. This can be achieved with the Interlocked class.

However, you also need to use some memory synchronization mechanism to ensure that the up-to-date value is available to other threads. The MSDN documentation on volatile is abysmally wrong:

The MSDN documentation states that use of the volatile keyword ensures that the most up-to-date value is present in the field at all times. This is incorrect, since as we’ve seen, a write followed by a read can be reordered. (Joseph Albahari)

Specifically, the volatile keyword instructs the compiler to generate an acquire-fence every read from that field. However, the acquire-fence takes effect after the read, in order to prevent other reads/writes from being moved before it. This means that the read itself may be moved up, allowing a stale value to be retrieved the first time the variable is read.

Unfortunately, there is no clean method for reading an integer with the required memory barriers, but the nearest candidate is a redundant Interlocked.CompareExchange (see this answer):

private int _count = 0;

public bool Ongoing => Interlocked.CompareExchange(ref _count, 0, 0) > 0;

public void Method1(object param)
{
    new Thread(new ParameterizedThreadStart(Method2)).Start(param);
}

private void Method2(object param)
{
    Interlocked.Increment(ref _count);
    lock (_lock)
    {
        // Something
    }
    Interlocked.Decrement(ref _count);
}
Douglas
  • 53,759
  • 13
  • 140
  • 188
  • This prevents completely the purpose of the user, he wants to know how many threads are waiting the lock, so the increment and decrement must be outside the lock. – Gusman Jun 14 '17 at 17:41
  • @Gusman: You're right; I completely missed that. I'll fix the code. – Douglas Jun 14 '17 at 18:02
  • `Interlocked.Read` is shorthand for `Interlocked.CompareExchange(..., 0, 0)`. – Jeroen Mostert Jun 14 '17 at 18:13
  • @JeroenMostert: On which version of .NET does `Interlocked.Read` take `int`? – Douglas Jun 14 '17 at 18:37
  • Well, shame that overload's missing. – Jeroen Mostert Jun 14 '17 at 18:41
  • True. `Interlocked.Read` was intended primarily to provide for atomic reads; like with `CompareExchange`, the memory barriers are a favorable side-effect. I guess a more explicit way of writing the getter would be with a pair of `Interlocked.MemoryBarrier` calls. – Douglas Jun 14 '17 at 18:52
  • Between `volatile`, `Thread.VolatileRead`, `Volatile.Read`, `Interlocked.CompareExchange` and `Thread.MemoryBarrier` (with the final, definitive advice on what's really most correct in version X of the framework on platform Y shifting every so often as more experts weigh in) I'm honestly content to leave the memory barrier business to the rest and just stick to `lock`s where possible. I have not yet had this policy fail me in production code. :-) – Jeroen Mostert Jun 14 '17 at 18:59
  • That's a very sensible approach. `Interlocked` is generally fine for simple cases, but `volatile` is widely misused. (Incidentally, I have a [related question](https://stackoverflow.com/q/44121866/1149773) on a popular pattern involving `volatile` that even experts seem to have gotten wrong.) – Douglas Jun 14 '17 at 19:04