-1
public class Test 
{
    public Test instance = null;
    public static readonly object lockA = new object();
    public static readonly object lockB = new object();
    public Dictionary<string, string> data = new Dictionary<string, string>();

    public static Test Instance 
    {
        get 
        {
            if(instance == null)
            {
                lock(lockA)
                {
                    instance = new Test();
                }
            }
            
            return instance;
        }
    }
    
    
    public async Task Init()
    {
        // fetch and populate data to the "data" dictionary
    }
    
    
    public string Get(string key)
    {
        if(data.ContainsKey(key))
        {
            return data[key];
        }
        else
        {
            return key;
        }
    }
    
    
    public void Update(string key, string value)
    {
        lock(lockB)
        {
            // some other code lines...

            data.Add(key, value); 
        }
    }
}

Let's take a scenario that, multiple users gonna consume this at a same time. In this case, Thread A is updating the data Dictionary. It means Thread A is inside the body of lock in Update method. There for any other thread won't be able to execute the body of the lock of Update method while thread A completes.

Now thread B,C and D wants to read the data from Dictionary at the same time.

Test.Instance.Get("SomeKey");

So my confusion is, should Thread B,C, and D have to wait until Thread A release the lock in Update method since Thread A uses data dictionary inside the lock statement?

As my understand, lock statement only not allow other threads to access the body of the lock if its acquired by some other thread right?

Please explain this.

Kasun
  • 672
  • 8
  • 18
  • 5
    You should try reading the documentation for the `lock` statement again (or for the first time if you haven't done so yet). Your understanding is obviously incomplete. Two `lock` statements are linked if and only if they lock on the same object. If they lock on different objects, as you do there, then there's no association between two lock statements whatsoever. If there was then you'd only be able to have such linked set of `lock` statements for the entire application, which would hardly make sense. – jmcilhinney Jul 12 '23 at 03:13
  • 1
    above code contains a race condition. think about what happens in the Get() if another thread deletes an object just after testing if it's in the collection ..... All interactions with the collection must go through a lock to avoid race conditions – Mitch Wheat Jul 12 '23 at 03:27
  • 1
    One way to think of a `lock` statement is that it makes the (reference type) object that the statement locks upon like the key to the washroom at a 1960s-1970s gas station. If you have the key (on a really big key fob), you can get in to use the toilet. If someone else shows up, they wait till you return the key. Strong suggestion, use a field declared like `private object myLock = new object();`. You may want it static, depending on your usage needs – Flydog57 Jul 12 '23 at 03:55
  • 1
    Given your description, you probably want a [ReaderWriterLockSlim](https://learn.microsoft.com/en-us/dotnet/api/system.threading.readerwriterlockslim?view=net-7.0) – Flydog57 Jul 12 '23 at 03:58

2 Answers2

2

Let me address one of your comments first up:

In my case I need multiple threads concurrently read the data dictionary. Its okay in my case. No need to synchronized. But when it comes to update, it should be synchronized.

It can be done, but it's not trivial, and potentially leads to issues you won't expect. Here's one option for this:

class Test
{
    private readonly object dataLock = new();
    private volatile Dictionary<string, string> data = new();

    // Ignoring all the other parts for now.

    public string ShortGet(string key)
    {
        // Since we only reference 'data' once we don't need special handling
        return data.TryGetValue(key, out var value) ? value : key;
    }
    
    public string LongGet(string key)
    {
        // Since we reference 'data' twice, make a local copy to ensure it
        // doesn't change between references
        var localData = data;
        if (localData.ContainsKey(key))
            return localData[key];
        return key;
    }

    public void Update(string key, string value)
    {
        lock (dataLock)
        {
            // Don't duplicate if the value is the same
            if (data.TryGetValue(key, out var current) && current == value)
                return;
            // Don't insert if key==value
            if (key == value)
                return;
            
            // Dupluicate the existing dictionary, update it, then switch
            // for the original
            var newData = new Dictionary<string, string>(data);
            newData[key] = value;
            Interlocked.Exchange(ref data, newData);
        }
    }
}

Neither ShortGet() nor LongGet() use locks, so they won't block at all. The Update() method locks for the duration of the operation, so only one thread can perform an update at a time.

The downside here is that you have to create a full copy of data each time you want to update a value. I've added some simple (and badly done) checks to maybe reduce that occasionally.


Now, let's explain locking.

The lock (...) { } statement attempts to acquire a thread-specific lock on the object before executing the code in the block. If the current thread already has a lock it'll pass through to the code block, otherwise if the object is locked by another thread it will block until the other thread releases the lock. And of course if no thread has it locked, the thread acquires a lock on the object. At the end of the code block if the lock was acquired in this specific lock statement then it is released from the thread.

And that is, in very broad details, everything that a lock statement does. It doesn't restrict access to the object to other things, it doesn't stop other threads from doing nasty things to the object without your consent. It just acquires and releases locks.

Now let's look at your Instance property and supporting objects:

class Test
{
    public static Test instance = null;
    public static readonly object lockA = new object();
    public static Test Instance
    {
        get
        {
            if (instance == null)
            {
                lock (lockA)
                {
                    instance = new Test();
                }
            }
            return instance;
        }
    }
}

First, both of the static fields (instance and lockA) should be private. If something can write to the instance field at any time without having to honor the lock, or something can acquire a lock from outside of your code, then all bets are off.

Now, in the Instance getter you're accessing the instance field three time: two reads and a write. And only the write is protected. This leaves it open for race conditions where two or more threads are trying to read and write at the same time.

Let's trace what happens when two threads try to get the value of Instance at the exact same time. In an ugly table.

Thread A Thread B instance Notes
if (instance == null) null
if (instance == null) null
lock (lockA) null
lock (lockA) null B blocked until A releases
instance = new Test(); {Test#1} Create first instance
instance = new Test(); {Test#2} Create second instance
return instance; {Test#2} returns second instance
return instance; {Test#2} returns second instance

(That's assuming that the threads are acting interleaved at the line level... unlikely, but possible.)

So you've created two different instances, one of which got lost. Could we make it worse? Sure. Let's delay the execution of the second thread by a line:

Thread A Thread B instance Notes
if (instance == null) null
lock (lockA) null
if (instance == null) null
instance = new Test(); {Test#1} Create first instance
lock (lockA) {Test#1} B not blocked
return instance; {Test#1} returns first instance
instance = new Test(); {Test#2} Create second instance
return instance; {Test#2} returns second instance

And now you have two completely separate instances of the Test class floating around, one of which is orphaned.

What you should be doing is locking the whole code of the getter, and doing the operation as quickly as possible. Like this:

    private static readonly object lockA;
    private static Test instance;

    public static Test Instance
    {
        get
        {
            lock (lockA)
            {
                return instance ??= new();
            }
        }
    }

Now all of the interactions with the instance field are behind a lock, nothing can interfere with instance or acquire a bad lock on lockA, so all of the issues with race conditions between threads should be resolved.

(And since I'm in nitpick mode: lockA is stunningly opaque. Why not call it instanceLock instead?)

Corey
  • 15,524
  • 2
  • 35
  • 68
  • 2
    Your first example with the `ShortGet`/`LongGet`/`Update` methods is not entirely correct, because the `data` is not declared `volatile` as it should, allowing instructions to be reordered by the Jitter producing unexpected phenomena. It also seems like an attempt to reinvent the [`ImmutableDictionary`](https://learn.microsoft.com/en-us/dotnet/api/system.collections.immutable.immutabledictionary-2) class, and the [`ImmutableInterlocked.Update`](https://learn.microsoft.com/en-us/dotnet/api/system.collections.immutable.immutableinterlocked.update) method. – Theodor Zoulias Jul 12 '23 at 06:24
  • 1
    @TheodorZoulias Thanks, I always miss that. In this case capturing the value once in the method - both the short and long forms - *should* make it reasonably safe on read. I've added `volatile` to the `data` declaration just in case. – Corey Jul 13 '23 at 03:47
  • @TheodorZoulias Oh and yes, this is quite similar to the way the various thread-safe and immutable collections work. It's a direct answer to the question though, and hopefully shows some of the difficulties of working with locks. – Corey Jul 13 '23 at 03:51
0

lock statement only not allow other threads to access the body of the lock if its acquired by some other thread right?

Yes. The lock is a block of code where only one thread can enter at a time. While a thread is inside this block, other threads have to wait until the owning thread exits the block. The code inside a lock block is sometimes called critical section.

The lock protects code, not variables or objects. It's your responsibility to ensure that non-thread-safe shared state (like the data dictionary in your example) is always accessed inside lock protected sections with the same locker object. If you forget to protect even a single interaction with the shared state, allowing more than one threads to interact with it concurrently, the behavior of your program becomes undefined. In your example the data is not protected inside the Get method, and also the instance is read without protection inside the Test method, so your program has undefined behavior.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
  • If I use a lock inside Get as you mentioned, then multiple threads won't be able to concurrently access(read) data dictionary. – Kasun Jul 12 '23 at 03:18
  • 2
    @Kasun yes, if you put all interactions with the `data` dictionary inside `lock (lockB)` regions, the interactions will be serialized. Only one thread at a time will interact with the dictionary. Try to keep the code inside the `lock` short and lightweight, otherwise your program might suffer from [high contention](https://learn.microsoft.com/en-us/dotnet/api/system.threading.monitor.lockcontentioncount) (too many threads blocked waiting for their turn to acquire the lock). – Theodor Zoulias Jul 12 '23 at 03:27
  • Yeah, I got you. But that's not my concern is about. In my case I need multiple threads concurrently read the `data` dictionary. Its okay in my case. No need to synchronized. But when it comes to update, it should be synchronized. That's why I've used `lock` only inside `Update`. My confusion is whether some other threads(B,C, D) can read it concurrently while some other thread(A) is acquired the `lock` of `Update` method. – Kasun Jul 12 '23 at 03:33
  • 4
    @Kasun it's never OK to read and write concurrently on a `Dictionary`, from multiple threads without synchronization. You can read the documentation of the collection, down at the bottom in the [Thread Safety](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.dictionary-2#thread-safety) section. – Theodor Zoulias Jul 12 '23 at 03:48
  • 2
    @Kasun *"My confusion is whether some other threads(B,C,D) can read it concurrently while some other thread(A) is acquired the `lock` of `Update` method."* -- Yes, they can. You haven't taken any action to prevent it, so it can happen, in which case all sorts of unexpected things can follow. – Theodor Zoulias Jul 12 '23 at 04:00