Deadlock when locking manualResetEvent
Asked Answered
Q

2

11

I encounter a deadlock caused when locking instance of manualResetEvent. I can't figure out how to solve it. I will appreciate any help.

I have 2 methods in a class performed by different threads:

private ManualResetEvent _event = new ManualResetEvent (true);

private void process(){
  ...
  lock(_event){
    _event.WaitOne();
    ...
  }
}

internal void Stop(){
  _event.Reset();
  lock(_event){
    ...
  }
}

The first thread acuired the lock and is blocked in _event.WaitOne();

The socond thread performed the line _event.Reset(); and is blocked when trying to perform lock(_event).

I thought that when thread 1 is blocked on WaitOne, the lock should be released. I guess I'm wrong. I don't know how can I fix it. b.t.w - I added the lock since the code in the lock block should be synchronized in both threads.

Thanks again and sorry for the long post.

Quatre answered 16/3, 2011 at 8:35 Comment(0)
S
14

  1. Why you got a deadlock

The short answer first: You missed Reset for Set.

I've copied your code (changed the braces to my prefered style) and I'll explain the problem in the comments:

private ManualResetEvent _event = new ManualResetEvent (true);

private void process()
{
  //...
    lock(_event)
    {
        _event.WaitOne(); //Thread A is here waiting _event to be set
        //...
    }
}

internal void Stop()
{
    _event.Reset(); //But thread B just did reset _event
    lock(_event) //And know thread B is here waiting... nobody is going to set _event
    {
        //...
    }
}

With that part clear, let's move onward to the solution.


  1. Solving the deadlock

Since we are going to exchange .Reset() with .Set() we will also have to change the default state of the ManualResetEvent from true to false.

So, to solve the deadlock edit the code as follows [tested]:

private ManualResetEvent _event = new ManualResetEvent (false);

private void process()
{
  //...
    lock(_event)
    {
        _event.WaitOne(); //Thread A will be here waiting for _event to be set
        //...
    }
}

internal void Stop()
{
    _event.Set(); //And thread B will set it, so thread a can continue
    lock(_event) //And when thread a releases the lock on _event thread b can enter
    {
        //...
    }
}

The code above does not only enforce that only one thread can enter the lock at the same time, but also that the thread that enters process will wait until there is a thread that calls Stop.


  1. But you got a race condition... fixing it.

The job is not done, because the code above suffers from the illness of a race condition. To understand why to imagine what happens in the case where multiple threads call process. Only one thread will enter the lock and will wait until Stop is called and _event is set, after that, it can continue. Now, consider what happens if the thread that calls Stops gets preempted just after it calls _event.Set(), the waiting thread that was at _event.WaitOne() continues and leaves the lock... now you can't tell if another thread that was waiting to enter the lock-in process will enter or if the thread that was preempted in Stop will continue and enter the lock-in that method. That is a race condition, I don't think you want that particular one.

That said I offer you yet a better solution [tested]:

private ManualResetEvent _event = new ManualResetEvent (false);
private ReaderWriterLockSlim _readWrite = new ReaderWriterLockSlim();

private void process()
{
    //...
    _readWrite.EnterReadLock();
    _event.WaitOne();
    try
    {
        //...
    }
    finally
    {
        _readWrite.ExitReadLock();
    }
}

internal void Stop()
{
    //there are three relevant thread positions at the process method:
    //a) before _readWrite.EnterReadLock();
    //b) before _event.WaitOne();
    //c) after _readWrite.EnterReadLock();

    _event.Set(); //Threads at position b start to advance
    Thread.Sleep(1); //We want this thread to preempt now!
    _event.Reset(); //And here we stop them
    //Threads at positions a and b wait where they are
    //We wait for any threads at position c
    _readWrite.EnterWriteLock();
    try
    {
        //...
    }
    finally
    {
        _readWrite.ExitWriteLock();
        //Now the threads in position a continues...
        // but are halted at position b
        //Any thread in position b will wait until Stop is called again
    }
}

Read the comments in the code to understand how it works. In simple terms, it takes advantage of a Read-Write lock to allow multiple threads to enter the method process but only one to enter Stop. Although additional work was done to ensure that the threads that are calling the method process will wait until a thread calls the method Stop.


  1. And now you got a reentry problem... fixing it.

The solution above is better... and that doesn't mean perfect. What's wrong with it? Well, if you call Stop recursively or if you call it from two different threads at the same time it will not work correctly because a second call may make threads at process advance while the first call is executing... and I think you don't want that. It did have the appearance that the Read-Write lock was enough to prevent any problems from multiple threads calling the method Stop, but that wasn't the case.

To solve this we need to make sure that Stop only executes once at the time. You can do that with lock:

private ManualResetEvent _event = new ManualResetEvent (false);
private ReaderWriterLockSlim _readWrite = new ReaderWriterLockSlim();
//I'm going to use _syncroot, you can use any object...
// as long as you don't lock on it somewhere else
private object _syncroot = new object();

private void process()
{
    //...
    _readWrite.EnterReadLock();
    _event.WaitOne();
    try
    {
        //...
    }
    finally
    {
        _readWrite.ExitReadLock();
    }
}

internal void Stop()
{
    lock(_syncroot)
    {
        //there are three relevant thread positions at the process method:
        //a) before _readWrite.EnterReadLock();
        //b) before _event.WaitOne();
        //c) after _readWrite.EnterReadLock();
    
        _event.Set(); //Threads at position b start to advance
        Thread.Sleep(1); //We want this thread to preempt now!
        _event.Reset(); //And here we stop them
        //Threads at positions a and b wait where they are
        //We wait for any threads at position c
        _readWrite.EnterWriteLock();
        try
        {
            //...
        }
        finally
        {
            _readWrite.ExitWriteLock();
            //Now the threads in position a continues...
            // but are halted at position b
            //Any thread in position b will wait until Stop is called again
        }
    }
}

Why do we need a Read-Write lock? - you may ask - If we are using a lock to ensure that only one thread enters the method Stop...?

Because the Read-Write lock is also allowing the thread at the method Stop to stop newer threads that are calling the method process while allowing those that were there already to execute and wait until they finish.

Why do we need ManualResetEvent? - you may ask - If we already have the Read-Write lock to control the execution of the threads in the method process...?

Because Read-Write lock is not able to prevent the execution of the code in the method process before the method Stop has been called.

So, you wee we need all that... or do we?

Well, that depends on what behaviour do you have, so in case I did solve a problem that is not what you had, I offer some alternative solutions below.


  1. Alternative solution with an alternative behaviour

A lock is very easy to understand, but it is a bit too much for my taste... in particular if there is no need to make sure that each concurrent call to Stop has a chance to allow the execution of thread at the method process.

IF that is the case then you can rewrite the code as follows:

private ManualResetEvent _event = new ManualResetEvent (false);
private ReaderWriterLockSlim _readWrite = new ReaderWriterLockSlim();
private int _stopGuard;

private void process()
{
    //...
    _readWrite.EnterReadLock();
    _event.WaitOne();
    try
    {
        //...
    }
    finally
    {
        _readWrite.ExitReadLock();
    }
}

internal void Stop()
{
    if(Interlocked.CompareExchange(ref _stopGuard, 1, 0) == 0)
    {
        //there are three relevant thread positions at the process method:
        //a) before _readWrite.EnterReadLock();
        //b) before _event.WaitOne();
        //c) after _readWrite.EnterReadLock();
    
        _event.Set(); //Threads at position b start to advance
        Thread.Sleep(1); //We want this thread to preempt now!
        _event.Reset(); //And here we stop them
        //Threads at positions a and b wait where they are
        //We wait for any threads at position c
        _readWrite.EnterWriteLock();
        try
        {
            //...
        }
        finally
        {
            _readWrite.ExitWriteLock();
            //Now the threads in position a continues...
            // but are halted at position b
            //Any thread in position b will wait until Stop is called again
        }
    }
}

Not yet the right behaviour? Ok, let's see another.


  1. Alternative solution with an alternative behaviour... again

This time we are going to see how to allow multiple thread to enter the method process even before the method Stop was called.

private ReaderWriterLockSlim _readWrite = new ReaderWriterLockSlim();
private int _stopGuard;

private void process()
{
    //...
    _readWrite.EnterReadLock();
    try
    {
        //...
    }
    finally
    {
        _readWrite.ExitReadLock();
    }
}

internal void Stop()
{
    if(Interlocked.CompareExchange(ref _stopGuard, 1, 0) == 0)
    {
        //there are two relevant thread positions at the process method:
        //a) before _readWrite.EnterReadLock();
        //b) after _readWrite.EnterReadLock();

        //We wait for any threads at position b
        _readWrite.EnterWriteLock();
        try
        {
            //...
        }
        finally
        {
            _readWrite.ExitWriteLock();
            //Now the threads in position a continues...
            // and they will continue until halted when Stop is called again
        }
    }
}

Not what you want?

Ok, I give up... let's get back to the basics.


  1. And what you already knew

...for the sake of completeness, if you only need to make sure that the access of both methods is synchronized and you can allow the methods at the process to run at any time, then you can do it with just locks... and you already knew that.

private object _syncroot = new object();

private void process()
{
    //...
    lock(_syncroot)
    {
        //...
    }
}

internal void Stop()
{
    lock(_syncroot)
    {
        //...
    }
}

  1. Conclusion

We have seen why the deadlock happened in the first place and how to fix it, but we also discovered that the absence of deadlock is not a warranty of thread safety. Finally, we have seen three solutions (points 4, 5, 6 and 7 above) with four different behaviours and complexities. All in all, we can conclude that developing with multithreading can be a very complex task where we need to keep our goals clear and be aware of what can go wrong at every turn. You can say it ok to be a bit paranoid, and that doesn't only apply to multithreading.

Scrouge answered 26/6, 2012 at 0:29 Comment(2)
Wow very nice example, thank you! Now I actually begin to understand what is thread safety!Pilfer
@Scrouge it gives Recursive read lock acquisitions not allowed in this mode. error :( It seems we should put EnterReadLock(); inside the try block.Grafting
I
3

I am guessing you got confused with Monitor.Wait(object) and ManualResetEvent.WaitOne().

Monitor.Wait(object) release the lock and wait until it acquires the lock. ManualResetEvent.WaitOne() blocks the current thread until the event handle gets signaled.

I also recommend not to use ManualResetEvent object as a lock at the same time. Even though the compiler will not generate the error, this is likely going to create confusion as you may have now.

Ilona answered 16/3, 2011 at 8:58 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.