An interesting deadlock situation came up in our application recently. We use NHibernate for our ORM and our database is SQL Server. Within the application we've constructed some utility classes for NHibernate so that there is a standard pattern for finding, accessing, and modifying persisten objects. A key part of this is our UnitOfWork class that wraps up an NHibernate session and transaction. All access to persistent objects occurs in the context of a UnitOfWork. When a UnitOfWork is completed it either commits its changes back to the DB or they are aborted.
Initially, the typical pattern for carrying out some changes to a persistent object (or set of objects) looked like:
using (new UnitOfWork())
{
Person p = PersonFactory.FindById(personId);
p.Salary += 10;
}
By default, the UnitOfWork commits at the end, so that the change to the Salary property would end up being saved to the DB by this code. Much to our chagrin, we found ourselves getting quite a few deadlocks when using this code.
The trouble with this code arises because, by default, NHibernate acquires a read lock when getting the data from the DB as part of the FindById() call. If multiple threads execute this code concurrently, they can acquire a read lock on the same object since read locks are allowed to be shared. When the transaction commits, the read locks need to be upgraded to write locks (because we are changing the object). The result is a deadlock because the shared read lock cannot be upgraded.
To try and simplify life for the programmer, we decided to add a parameter to the UnitOfWork which would indicated whether the code was read-only or not. That would eliminate the need to determine on a case b case basis what locks should be used for each object. Calls such as FindById() would observe the value of the read-only flag of the current session and modify their locking behaviour accordingly. So, for a UnitOfWork which was not read-only, it would acquire an update lock -- which is used in SQL server to indicate a lock which may need to be upgraded to a write lock later in the transaction.
Overall this was a vary effective strategy. It was a relatively simple programming model and it virtually eliminated all of the deadlocks that we were seeing with the application. In part 2 of this posting, I'll talk about how we eventually needed to go beyond this simple model due to more complex interactions between concurrent threads.