2

in a project im playing around with threads. Im trying to make a safe thread that dosn't 'corrupt' the data. My thread runs in the background and call functions on an other class, I can call Go and Go2, one function adds and one deletes from a list. I dont want them to run at the same time, what is the difference between the following situation:

static readonly object _locker1 = new object();
static readonly object _locker2 = new object();


public void Go(Object something)
{
  lock (_locker1)
  {
    myList.add(something);
  }
}

public void Go2(Object something)
{
  lock (_locker2)
  {
    myList.Remove(something);
  }
}

And if i would replace Go2 with:

public void Go2(Object something)
{
  lock (_locker1)
  {
    myList.Remove(something);
  }
}

Note the lock parameter.

A third situation that would help me understand, lets say i call Go from a different thread (thread2), can it run because _locker1 is locked by thread2 and Go2 (which has _locker 1 that is locked by thread2) is called from thread1?

static readonly object _locker1 = new object();
static readonly object _locker2 = new object();


public void Go(Object something)
{
  lock (_locker1)
  {
    //Can I call Go2 which is locked by the same object?
    Go2(something);
  }
}

public void Go2(Object something)
{
  lock (_locker1)
  {
    myList.Remove(something);
  }
}

Could someone explain what the value passed to lock does?

Willem Van Onsem
  • 443,496
  • 30
  • 428
  • 555
Sven van den Boogaart
  • 11,833
  • 21
  • 86
  • 169

4 Answers4

6

It's very simple: If two locks use the same object, they will not run at the same time. In your first snippet, as Go and Go2 are locking on different objects, they could run at the same time and do bad things.

Cecilio Pardo
  • 1,717
  • 10
  • 13
  • @cecilloPardo Lets say I call Go and later Go2 (while Go isn't finished) does it just wait on Go to finish or skip Go2 because its locked? – Sven van den Boogaart Jan 04 '16 at 22:34
  • In the first situation, Go a Go2 will run at the same time. In the second situation, execution will stop at the lock statement of Go2 until the lock statement of Go1 finishes. – Cecilio Pardo Jan 04 '16 at 22:36
  • @cecilo Thanks! would i create a deadlock if i call Go2 from go in situation2? – Sven van den Boogaart Jan 04 '16 at 22:42
  • No. Locks are owned by the thread, so locking again does nothing. – Cecilio Pardo Jan 04 '16 at 22:43
  • But can i enter? I've added the example. – Sven van den Boogaart Jan 04 '16 at 22:44
  • Yes, you can. The thread already owns the lock, so it will just run. – Cecilio Pardo Jan 04 '16 at 22:47
  • Bravo on the terseness of this explanation – MattTannahill Jan 04 '16 at 22:53
  • 1
    A deadlock would be caused by thread A locking on _locker1 and thread B locking on _locker2. Thread A then requests a lock on _locker2 and will not release its lock on _locker1 until it locks on _locker2. Thread B then does the opposite: it requests a lock on _locker1 and will not release it's lock on _locker2 until it locks on _locker1. Because neither thread can acquire their second requested lock (because the other thread will not release it), the threads are deadlocked. – MattTannahill Jan 04 '16 at 22:59
  • @MattTannahill what about my 3th example, calling Go from thread1 and in Go call Go2 which uses the same lock. – Sven van den Boogaart Jan 04 '16 at 23:00
  • 1
    Like @CecilioPardo said, locks are at the thread level. This makes sense, because concurrency is only possible across multiple threads and not within a single thread. So, calling Go2 from Go where both Go and Go2 require the same lock will not deadlock; the thread will call Go, aquire the lock on _locker1, then call Go2, and will pass the lock statement because it already has the required lock. – MattTannahill Jan 04 '16 at 23:12
2

As the documentation says:

The lock keyword marks a statement block as a critical section by obtaining the mutual-exclusion lock for a given object, executing a statement, and then releasing the lock.

lock is thus syntactical sugar for a monitor for the given object. When you use your first code example, if both threads execute Go (or Go2) they will have to wait for each other whereas if the two perform a different method, the can result in synchronization conflicts.

In the second code example, since you always lock an unlock the same object, it is guaranteed that no myList.add and myList.remove can be performed at the same time.

EDIT: in case of the third situation, recursive locks are counted. This means that in case the first thread calls Go and the second thread calls Go2, if the first thread enters the lock first, it will do a call to Go2 get access to the lock, remove the item, return from the recursive call, leave the lock, and only then the second thread can enter the lock of Go2. In case the second thread wins the race, the second thread will first enter the lock and blocking the first thread from the outer lock. Only when the second thread leaves the lock of Go2, the first thread can enter the lock of Go (and perform the recursive call).

Community
  • 1
  • 1
Willem Van Onsem
  • 443,496
  • 30
  • 428
  • 555
1

A lock statement actually translates to a monitor lock under the hood, such as:

bool lockWasTaken = false;
var temp = obj;
try
{
    Monitor.Enter(temp, ref lockWasTaken);
    // body
}
finally
{
    if (lockWasTaken)
    {
        Monitor.Exit(temp); 
    }
}

To find out more about how Monitor locks work, look here: MSDN Documentation

To Quote MSDN:

Use Enter to acquire the Monitoron the object passed as the parameter. If another thread has executed an Enter on the object but has not yet executed the corresponding Exit, the current thread will block until the other thread releases the object. It is legal for the same thread to invoke Enter more than once without it blocking; however, an equal number of Exit calls must be invoked before other threads waiting on the object will unblock. Use Monitor to lock objects (that is, reference types), not value types. When you pass a value type variable to Enter, it is boxed as an object. If you pass the same variable to Enter again, it is boxed as a separate object, and the thread does not block. In this case, the code that Monitor is supposedly protecting is not protected. Furthermore, when you pass the variable to Exit, still another separate object is created. Because the object passed to Exit is different from the object passed to Enter, Monitor throws SynchronizationLockException. For more information, see the conceptual topic Monitors.

This means that your code will be blocked on 2 different objects, where you would only want to block on one to make it thread safe.

TheDaveJay
  • 753
  • 6
  • 11
1

The value passed into lock is symbolic of the shared state that will be accessed in the block (curly braces) the the lock statement. Each request for a lock on that value will be queued and processed in order. Only one requestor for that value is allowed to process at a time, so the shared state is only ever accessed by one requestor at a time.

Abstraction

It's like if I had a meeting with a rubber chicken, and I said "only the person holding the rubber chicken can speak". In this scenario, the rubber chicken is the lock parameter, and the ability to speak is the shared resource. Everyone wishing to speak will form a line. Only one person can hold the chicken, so only one person can speak. When the person speaking is done, they hand the chicken to the next person in line.

In your first situation, you have two rubber chickens: locker1 (rubber chicken 1) and locker2 (rubber chicken 2). Thus, Go and Go2 don't wait on each other for a turn (they both have a chicken!). A thread calling Go will be able to add to myList while another thread calling Go2 can simultaneouly access the myList to remove the item from the list. However, two threads calling to Go will have wait their turn because they both require the same rubber chicken: locker1; the same goes for two threads calling Go2.

If you make both Go and Go2 use the same value (the same chicken), then they would have to wait their turn to acquire the lock on that value. This would prevent them one thread calling Go and a second thread calling Go2 from accessing myList at the same time.

MattTannahill
  • 606
  • 4
  • 11