14

The documentation for LazyThreadSafetyMode states that using the value ExecutionAndPublication could cause deadlocks if the initialization method (or the default constructor, if there is no initialization method) uses locks internally. I am trying to get a better understanding of examples that could cause a deadlock when using this value. In my use of this value, I am initializing a ChannelFactory. I cannot see the ChannelFactory's constructor using any internal locks (reviewing the class with Reflector), so I believe this scenario does not fit the possible deadlock situation, but I am curious what situations could cause a deadlock as well as if there could be a possible deadlock initializing the ChannelFactory.

So, to summarize, my questions are:

  1. Is it possible to cause a deadlock initializing the ChannelFactory using ExecutionAndPublication?

  2. What are some possible ways to cause a deadlock initializing other objects using ExecutionAndPublication?

Suppose you have the following code:

class x
{
   static Lazy<ChannelFactory<ISomeChannel>> lcf = 
        new Lazy<ChannelFactory<ISomeChannel>>(
        () => new ChannelFactory<ISomeChannel>("someEndPointConfig"), 
        LazyThreadSafetyMode.ExecutionAndPublication
        );

    public static ISomeChannel Create()
    {
        return lcf.Value.CreateChannel();
    }
}
dugas
  • 12,025
  • 3
  • 45
  • 51

1 Answers1

11
  1. It's as documented – if it doesn't use any locks, this usage cannot cause any deadlocks.
  2. Imagine that you have a lazy value that you initialize by reading from a database, but you want to make sure that only one thread is accessing the DB at any moment. If you have other code that accesses the DB, you could have a deadlock. Consider the following code:
void Main()
{
    Task otherThread = Task.Factory.StartNew(() => UpdateDb(43));
    Thread.Sleep(100);
    Console.WriteLine(lazyInt.Value);
}

static object l = new object();
Lazy<int> lazyInt = new Lazy<int>(Init, LazyThreadSafetyMode.ExecutionAndPublication);

static int Init()
{
    lock(l)
    {
        return ReadFromDb();
    }
}

void UpdateDb(int newValue)
{
    lock(l)
    {
        // to make sure deadlock occurs every time
        Thread.Sleep(1000);

        if (newValue != lazyInt.Value)
        {
            // some code that requires the lock
        }
    }
}

Init() reads from the DB, so it has to use the lock. UpdateDb() writes to the DB, so it needs the lock too, and since Lazy uses a lock internally too in this case, it causes deadlock.

In this case, it would be easy to fix the deadlock by moving the access to lazyInt.Value in UpdateDb() outside the lock statement, but it may not be so trivial (or obvious) in other cases.

svick
  • 236,525
  • 50
  • 385
  • 514
  • 1
    Great answer @svick - classic example of nested locks acquired in opposite order, this is along the lines of what I was thinking - great example to clarify the scenario, thanks! – dugas May 29 '11 at 20:56
  • Maybe I don't get it, but this code seems a bit theoretical in nature. It is difficult to know if third party or system library code which might be called in the constructors is using locks internally, and much caution needs to be taken when using the `ExecutionAndPublication` mode, which is the default one. However, the solution for this specific case and to real-world multi threaded deadlock situations while using Lazy would be to use `PublicationOnly` mode instead. Any known drawbacks? Why is it not the default? – octo Feb 13 '21 at 09:52
  • 1
    @horiac7 The problem isn't just any lock, it needs to be a lock that can get into a cycle with the `Lazy` lock. And in well-designed libraries, such locks tend to be rare, to avoid the potential for deadlocks. So `ExecutionAndPublication` is not that bad. And `PublicationOnly` means the initialization code can execute more than once, which might be unexpected. – svick Feb 14 '21 at 16:03