2

Summary

I have a class that uses lock to provide thread-safe access to a private field. However, for reasons detailed below I'm considering switching to using SemaphoreSlim to implement thread-safety. I know that if I surround all access to this field with lock (_lock) (as I currently do), I'm guaranteed that writes to the field will be atomic and that all threads will see the most recently-written value (source). I'm wondering if I get both of these guarantees with SemaphoreSlim, or only the guarantee about atomic writes.

Details

I have an interface I wrote a while back to represent services that can be used for communicating with external devices:

public interface ICommunicationService : IDisposable
{
    event EventHandler<ReceivedMessageEventArgs> MessageReceived;
    void SendMessage(GenericMessage message);
    void Start();
}

I've recently come across a situation where I'd prefer to have an async implementation of these methods, and apparently when you start doing async you need to sort of go the whole hog with it to avoid deadlocks, etc., so I'm planning on updating the interface and all existing implementations that I've written to support asynchronous operation:

public interface ICommunicationService : IDisposable
{
    event EventHandler<ReceivedMessageEventArgs> MessageReceived;
    Task SendMessage(GenericMessage message);
    Task Start();
}

One of my classes that implemented this interface did a bunch of locking in its implementations of SendMessage() and Start():

public virtual void SendMessage(GenericMessage message)
{
    CheckDisposed();

    lock (_lock)
    {
        if (CommunicationState != CommunicationState.Up)
        {
            throw new InvalidOperationException("Message cannot be sent as communication is not established.");
        }

        try
        {
            _currentDelegate.SendMessage(message);
        }
        catch (Exception)
        {
            TerminateCommunication();
        }
    }
}

Trying to switch this code to an async implementation, I noticed I couldn't await a task within a lock statement. According to async guru Stephen Cleary's blog post on the subject, the async way of doing this is to use SemaphoreSlim.

Based on that post, I've changed my SendMessage implementation to this:

public virtual async Task SendMessage(GenericMessage message)
{
    CheckDisposed();

    if (CommunicationState != CommunicationState.Up)
    {
        throw new InvalidOperationException("Message cannot be sent as communication is not established.");
    }

    // _mutex = new SemaphoreSlim(0, 1)
    await _mutex.WaitAsync().ConfigureAwait(false);

    try
    {
        await _currentDelegate.SendMessage(message);
    }
    catch (Exception)
    {
        TerminateCommunication();
    }
    finally
    {
        _mutex.Release();
    }
}

What I'm wondering though is whether I'm guaranteed that any given thread will see the most recent value of _currentDelegate when it executes await _currentDelegate.SendMessage(message), or whether I need to use another construct to ensure writes are immediately visible to other threads.

In particular, this class has another method TryRestartCommunication:

private bool TryRestartCommunication(bool initialStart)
{
    // ...
    lock (_lock)
    {
        // ...
        try
        {
            _currentDelegate = _factory();
            _currentDelegate.Start();
            _currentDelegate.MessageReceived += MessageReceived;
            CommunicationState = CommunicationState.Up;
            return true;
        }
        // ...
    }
}

If I re-implement this method to lock on the semaphore and have some Thread A call TryRestartCommunication() and immediately after Thread B call SendMessage(), am I guaranteed that Thread B will see the new value of _currentDelegate set by Thread A?

If not, would a decent solution be to just make _currentDelegate volatile, or to use Interlocked to update its value?

Edit

Got a close vote because apparently this question isn't clear enough. Let me try to make it as clear as possible: if I switch from protecting my critical regions with lock to SemaphoreSlim, do I then need to mark the shared fields as volatile (or something similar) to ensure the same level of thread safety?

Tagc
  • 8,736
  • 7
  • 61
  • 114
  • You can't change delegate with `Interlocked` and it's not clear what is the problem with semaphore. See [Monitor.Enter](https://msdn.microsoft.com/en-us/library/dd289498(v=vs.110).aspx#Anchor_3) for lock acquisition pattern, this is what you have to do with semaphore. – Sinatr Oct 10 '17 at 11:34
  • @Sinatr "You can't change delegate with Interlocked" Not sure what you mean, I've added `Interlocked.Exchange(ref _currentDelegate, _factory());` to an async version of `TryRestartCommunication` and the compiler doesn't complain. "it's not clear what is the problem with semaphore." I'm not saying there necessarily is one, I just want to be sure about the guarantees it provides. The [Microsoft docs on SemaphoreSlim](https://msdn.microsoft.com/en-us/library/system.threading.semaphoreslim%28v=vs.110%29.aspx?f=255&MSPPError=-2147217396) show them using `Interlocked` too, FWIW. – Tagc Oct 10 '17 at 11:39
  • I mean you have to run multiple statements, which is not possible with `Interlocked`, but is possible with semaphore, once you obtain ownership (I assume you are using it as [lightweight `Mutex`](http://www.albahari.com/threading/part2.aspx#_Locking) with maximum 1 owners). – Sinatr Oct 10 '17 at 11:55
  • @Sinatr If you see my post I already am using semaphores to restrict access to my critical sections. What I'm asking is whether acquiring a semaphore involves the same memory barrier operations that `lock` does to ensure any shared state I change within the critical section is immediately visible to other threads by the time I leave it. If not, I believe I need to use `Interlocked` *in conjunction* with my semaphore, as [Microsoft seemed to have done](https://msdn.microsoft.com/en-us/library/system.threading.semaphoreslim(v=vs.110).aspx?f=255&mspperror=-2147217396#Anchor_6). – Tagc Oct 10 '17 at 12:02
  • It does, no framework or OS can implement a synchronization object without a memory barrier. This kind of accidental barrier is a lot more common than most programmers assume. But otherwise the reason for the common advice: if you think you need a barrier then you probably should just use lock. – Hans Passant Oct 10 '17 at 12:16
  • @HansPassant "It does, no framework or OS can implement a synchronization object without a memory barrier" Sure, but there's different kinds of memory barriers with different guarantees. `volatile` for instance uses half-barriers that guarantee read/writes are visible, but not necessarily atomic. `lock` uses full-barriers. If `SemaphoreSlim` provided all the same guarantees as the `lock` statement does, I'm confused why Microsoft's example shows them using `Interlocked` as well to update the field's value. – Tagc Oct 10 '17 at 12:19
  • *" believe I need to use Interlocked"* - why? That `interlocked` in example you are linking is used to sync access to `padding`, because more than one thread enter critical section (it's critical with up to 3 owners). Make maximum 1 (that's what using `lock` imply), then nothing inside need additional synchonizations. – Sinatr Oct 10 '17 at 12:22
  • volatile just doesn't mean what anybody thinks it does, it got hopelessly broken by Itanium. Semaphore is necessary when the lock should not have thread affinity, the opposite of what lock and Mutex do. I can't guess what example you looked at. – Hans Passant Oct 10 '17 at 12:24
  • @Sinatr Ah okay, makes sense. So in my case where I'm using a mutex (`SemaphoreSlim(1)`), I can essentially treat it as if I were using `lock`? That means I don't need to worry about using `Interlocked.Exchange` and can just update the value `_currentDelegate` normally within my critical regions? – Tagc Oct 10 '17 at 12:26
  • Yes for `new SemaphoreSlim(0,1)`. – Sinatr Oct 10 '17 at 12:28
  • I've [answered](https://stackoverflow.com/a/40991759/800524) a similar question. – acelent Oct 10 '17 at 17:21

1 Answers1

2

If you switch from protecting your critical regions/data by lock() to SemaphoreSlim(1), you will end up with the same level of thread safety.

We have done it several times and never encounter single data protection bug there.

However, be aware that after await xxx.ConfigureAwait(false); you might end in different synchronization context (thread) and the method await _currentDelegate.SendMessage(message); might not be ok with it. For example, if that method access Webform UI controls.

sANDwORm
  • 191
  • 5