0

We have following class:

class ProcessStore
{
   ConcurrentDictionary<int, MyDisposableClass> _processes = new();
   bool _disposed;

   void Store(int Id)
   {
        if(_disposed) { throw; } 
        var mdc = new MyDisposableClass();
       _requests.Add(Id, mdc);
   }

   void Dispose()
   {
       if(_disposed)
       {
            return;
       }
       _disposed = true;
        
       foreach(var process in _processes)
       {
           process.Dispose();

       }
   }
}

Multiple threads may Store processes, and another thread will dispose it (but only single thread will dispose at time). This is effectively producer-consumer problem.

How can we make this class thread-safe? By thread-safe I mean that disposing instance disposes all Store requests and throws on new attempts. Simple answer is using locks, but I wonder if it's possible to do it correctly lock-free. I have following idea:

class ProcessStore
{
   ConcurrentDictionary<int, MyDisposableClass> _processes = new();
   volatile bool _disposed;

   void Store(int Id)
   {
        if(_disposed) { throw; } 
        var mdc= new MyDisposableClass();
       _requests.Add(Id, mdc);

        if(_disposed) { _requests.Remove(mdc); mdc.Dispose(); throw; } 
   }

   void Dispose()
   {
       if(_disposed)
       {
            return;
       }
       _disposed = true;
        
       foreach(var process in _processes)
       {
           process.Dispose();

       }
   }
}

So two new things are volatile on disposal, which ensures all threads see latest state, and additional check after adding to _requests dictionary, which eventually undoes it if object was disposed in meanwhile.

So if class was disposed after first dispose check, but before adding to dictionary, thread running Store thread will see that operation failed after adding, and will reverse it.

15
  • Is this what you are looking for? Commented Dec 3, 2021 at 16:54
  • See also Commented Dec 3, 2021 at 16:55
  • 2
    UsingConcurrentDictionary doesn't mean you're "lock-free". It just means you haven't written any locks. (The locks are inside the dictionary implementation.)
    – davidbak
    Commented Dec 6, 2021 at 15:24
  • 1
    Locks are fine as long as you do not invoke (delegate to another thread) from within a locked clause. It can be done without locks (although I don't see the point) by invoking from both Store and Dispose, making sure Store and Dispose are always executed by the same specific thread. In any other way you will have race conditions. You can also queue the work (Store or Dispose) and have a worker thread check the queue and perform the work. But then you would also lock when accessing the queue. Commented Jan 1, 2023 at 9:57
  • 1
    Checking “_disposed” without any protection is pointless since it can be set to true a nanosecond after the check.
    – gnasher729
    Commented Jan 1, 2023 at 19:51

3 Answers 3

1

The blockingCollection essentially provides this functionality with the CompleteAdding and GetConsumingEnumerable methods. But as far as I know it does not provide provide a dictionary API.

Looking at the source it seems like this keeps a count of the number of Add methods that are currently running, so that CompleteAdding can wait for them. However, the source code for this is rather complex, and just because something is 'lock free' does not necessarily mean its better. So I would personally stick with a lock since I would be confident that it is safe.

In your example code I would be concerned about the lack of memory barriers, that could potentially allow for instructions to be reordered. As far as I understand, volatile does only guarantees that a load will be issued, not that it cannot be reordered in regards to other instructions. See Eric Lippert on reordering optimizations for details.

Another option might be a ReaderWriterLock, so that any number of threads could take a read lock, while dispose would take the writer lock.

Note that taking an uncontested lock is fairly cheap to take, in the same order of magnitude as using an interlocked instruction. So unless the store method is called very frequently I would not worry about it.

4
  • But since these instructions are called on separate threads, there's no danger of reordering affecting code - or I do not understand something?
    – Shadow
    Commented Dec 6, 2021 at 14:16
  • Also I am playing on fact that I can revert Add operation, something that blockingCollection does not know - so it needs to be more complex.
    – Shadow
    Commented Dec 6, 2021 at 14:18
  • @Shadow without synchronization, read/writes from other threads may appear out of order. See Eric Lippert on reordering optimizations for details. Lock free programming is difficult, and as with any multi threaded programming, there is a real risk of writing bugs that are really difficult to find, and may only appear in very specific circumstances. So you really need to know all the details of the .net Memory model.
    – JonasH
    Commented Dec 6, 2021 at 15:05
  • What JonasH says, and the real problem is that you can have bugs that affect the operation very, very rarely - so rare that you never catch it in development, not even if you receive a bug report and actively look for the problem. Yet some customers will experience it.
    – gnasher729
    Commented Jan 1, 2023 at 8:54
1

Your proposed solution depends on the assumption that thread executing Store() will see a modification to _disposed if it were modified after the check and add have been completed. This isn't guaranteed to be the case work on multiprocessor systems as stated here (under the first 'Note'):

On a multiprocessor system, a volatile read operation is not guaranteed to obtain the latest value written to that memory location by any processor. Similarly, a volatile write operation does not guarantee that the value written would be immediately visible to other processors.

[emphasis mine]

In other word, either check of _disposed in Store() could fail to see that another thread (on another processor) has set _disposed prior.

I think it might be viable on a single processor system but another problem:

These operations might involve memory barriers on some processors, which can affect performance.

So even in that context, it may not be 'lock-free'.

I think if you replace your volatile boolean with Interlock and the appropriate usage pattern, your overall approach is valid with one potential issue:

You could have both threads attempting to call dispose() on the same instance of MyDisposable class if Store() has added it to _processes prior to Dispose() executing but then checks _disposed after Dispose() has set _disposed to true. If calling dispose on the same instance twice from different threads is a problem, you need locks or something else to prevent that.

One thing to keep in mind as well. As JonasH alludes to, locks should be pretty fast. The performance issues around them come from contention i.e. lots of threads trying to acquire them. If you don't have high-contention, a lock-free approach may actually be slower overall.

3
  • I think there is problem brought up by @JonasH, that in: _requests.Add(Id, mdc); if(_disposed) { _requests.Remove(mdc); mdc.Dispose(); throw; } Add operation may be ordered after _disposed check. I think it need memory barrier there, I'm reading about .net memory model now
    – Shadow
    Commented Dec 6, 2021 at 16:55
  • @Shadow Also from that page "A volatile write operation prevents earlier memory operations from being reordered to occur after the volatile write. A volatile read operation prevents later memory operations from being reordered to occur before the volatile read." So I think it will prevent the reordering but I'm not 100%. While I share JonasH's trepidation with volatile in general, the note says this won't always work on a multi-processor system. Case closed unless you know this will only run on a single processor. If you want lock-free, use lock-free primitives. Otherwise use locks.
    – JimmyJames
    Commented Dec 6, 2021 at 17:00
  • @Shadow Not sure if you saw my update but I noticed another potential flaw in this approach.
    – JimmyJames
    Commented Dec 6, 2021 at 18:48
-2

Your assumption is wrong. In a multi-processor system, the state of a volatile variable after it is changed is not immediately visible to other threads. It is only visible after the OS takes some action, and for example a lock operation will take that action.

Could the compiler be changed so that changes to volatile variables are immediately visible elsewhere? Sure, no problem. Just change the compiler to protect every access to a volatile variable by a lock. Now you don’t need lock calls. But you actually have many more locks than before.

Use locks. They work. And uncontested locks are usually very fast. And contested locks - you have to wait anyway. If you are still tempted: Measure it. But keep in mind that your benchmark might have many (slow) contested locks, while in real life they are rare.

Using locks: We assume that Store and Dispose can be called at any time, when another call is running. It’s always good to hold locks for the shortest possible time.

We assume “disposed” means: Dispose has been called once, further Dispose calls have no effect, further Store calls throw exceptions. You need one lock.

Store: Create a new MyDisposeableClass. Under protection of the lock: If disposable is true then free MyDisposableClass and throw an exception, otherwise add the MyDisposableClass to requests.

Dispose: Under protection of the lock determine whether to dispose the processes (if disposed is false) and set disposed to true. Outside lock protection dispose the processes.

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.