Part 3 – Advanced Safety
This is the third part of a series addressing how to make an existing or new project completely deadlock free. This project primarily deals with C# code, but these techniques can also be implemented in any language. If you haven't read the earlier parts, please read them first:
This module begins where the previous module ended. You can download a full copy of the completed project up to this point from my shared skydrive here: Sky Drive, for the starting point of this module, download Phase2.zip.
Ensuring single use IDs
Every time an OrderedLock object is created, it’s important that the constructor is passed a unique value from the ID enumeration. Failure to do this will result in the framework thinking these two different OrderedLock object are the same. This will result in the framework incorrectly mapping the relationships, which could lead to false positives. To prevent this, we add a list of where each ID was used in the source code. If any line of source code uses the same ID that another line of source code uses, we report an error. This should only be done in a DEBUG executable, since this is extremely easy to detect with 100% accuracy. So long as you run a DEBUG executable before you release a non-debug executable, there is no need to check again. The code changes start with adding a static Dictionary that maps ID objects to StackFrame objects. This will hold the source code location where each object was created. We put in the static members region:
file Ostrowski\Utilities\OrderedLock.cs
- #region Static Members
- // If this is ever set to true, don't write out the new dependency chart on exit
- private static bool circularDependencyFound = false;
- // This is used to report single instances of errors detected to a log file:
- private static UniqueEntryReporter uniqueEntryReporter = new UniqueEntryReporter(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "Deadlocks.log");
- #if DEBUG
- // Used to ensure we only use each ID once across the entrire project:
- private static Dictionary<ID, StackFrame> idToCreationStackFrame = new Dictionary<ID, StackFrame>();
- #endif // DEBUG
- #endregion
Then in the common constructor of OrderedLock, in #if DEBUG blocks, we make the check, and raise a Debug.Assert if the ID has already been used:
file Ostrowski\Utilities\OrderedLock.cs
- public OrderedLock(ID id, object objectToLock)
- {
- this.Id = id;
- this.ObjectToLock = objectToLock;
- #if DEBUG
- // This section (delimited by the #if DEBUG section) is used to verify
- // that two different OrderedLock objects don't use the same ID.
- StackTrace stackTrace = new StackTrace(true/*fNeedFileInfo*/);
- StackFrame[] stackFrames = stackTrace.GetFrames();
- String thisFileName = null;
- StackFrame creationStackFrame = null;
- // Find the closest StackFrame that is not within this class.
- // This is the place where this object was created.
- // We record it so we can print it in ToString(), and we also use
- // it to make sure no two DIFFERENT objects
- // use the same ID (see next comment section below).
- foreach (StackFrame frame in stackFrames)
- {
- if (thisFileName == null)
- thisFileName = frame.GetFileName();
- else if (thisFileName.Equals(frame.GetFileName()))
- continue;
- else
- {
- creationStackFrame = frame;
- break;
- }
- }
- // This ensures that no two instances of an OrderedLock object use the same ID.
- // It is OK for two separate instances of a single class to create their own
- // instance of an OrderedLock, however, it is not permitted for two separate but
- // different instances of an OrderedLock to use the same ID.
- // We test this by looking at the StackFrame that created the object.
- // Each place in the source code can only use any given ID once.
- if (idToCreationStackFrame.ContainsKey(this.Id) &&
- !CallStack.SameStackFrame(idToCreationStackFrame[this.Id], creationStackFrame))
- {
- String error = "OrderedLock with name of '" + this.Id.ToString() + "' used twice! " +
- "\r\n first loc:" + idToCreationStackFrame[this.Id] +
- "\r\nsecond loc:" + creationStackFrame;
- Log.LogError(error);
- Debug.Assert(false, error);
- // fall through to re-write the new location. This can happen during
- // run-time modification of the code, and we want it auto-correct itself.
- }
- idToCreationStackFrame[this.Id] = creationStackFrame;
- #endif // DEBUG
- }
Now if we cut and paste an OrderedLock object, but forget to create a new ID for use in the constructor, the first time we execute the code in a DEBUG build, we will hit a Debug.Assert.
Peer Locking Authority
One problem common in complex system is that there are times when you want to lock the protection lock of obj A and obj B at the same time, even when they are of the same type. Consider the case of a Contact class that contains a Name and a PhoneNumber field:
file Ostrowski\ContactList\Contact.cs
And we add the new element to the OrderedLock.ID enumeration:- using System;
- using Ostrowski.Utilities;
- namespace Ostrowski.ContactList
- {
- class Contact
- {
- public String Name { get; private set; }
- public String PhoneNumber { get; private set; }
- readonly OrderedLock lockThis = new OrderedLock(OrderedLock.ID.Ostrowski_Contact_lockThis);
- public Contact(String name, String phoneNumber)
- {
- this.Name = name;
- this.PhoneNumber = phoneNumber;
- }
- public void SetContact(String newName, String newPhoneNumber)
- {
- using (this.lockThis.Lock())
- {
- this.Name = newName;
- this.PhoneNumber = newPhoneNumber;
- }
- }
- public bool Equals(Contact other)
- {
- using (this.lockThis.Lock())
- {
- if (this.Name != other.Name) return false;
- if (this.PhoneNumber != other.PhoneNumber) return false;
- }
- return true;
- }
- }
- }
file Ostrowski\Utilities\OrderedLock.cs
At first glance, this seems like decent code. We have locks in place to ensure that anytime we set the Name and PhoneNumber fields, we change them both at the same time. However, closer inspection of the Equals method reveals that we are not as atomic as it should be: In this method, we are accessing the Name and PhoneNumber properties of the other Contact without locking that object's OrderedLock. Granted, this is a contrived example, in the real world it probably wouldn't matter if someone could change the name of contact while we are comparing another contact to it, but more complicated cases do exist where it's important to lock both objects within a comparison function To be safest, we should rewrite the Equals method as such:- public enum ID
- {
- Ostrowski_Program_resource1,
- Ostrowski_Program_resource2,
- Ostrowski_Program_signalLock,
- Ostrowski_Contact_lockThis,
- }
file Ostrowski\ContactList\Contact.cs
Now anytime we do the comparison, we can be sure that the other object won’t be changing as we are comparing it. However, there is a problem: Since each of the two Lock() calls are actually locking different instances of OrderedLock object that have exactly identical orders, we are violating our rule of only locking object of lower order. The chances of this type of deadlock are very rare, but it’s a solvable situation, so we address it. The deadlock only occurs if we have two objects A and B, and while one thread is calling A.Equals(B), another thread is calling B.Equals(A). In this deadlock, both threads could become stuck waiting on their inner Lock() call. To show that our framework can catch this, we add this to the end of the Main method in Program.cs:- public bool Equals(Contact other)
- {
- using (this.lockThis.Lock())
- {
- using (other.lockThis.Lock())
- {
- if (this.Name != other.Name) return false;
- if (this.PhoneNumber != other.PhoneNumber) return false;
- }
- }
- return true;
- }
file Program.cs
Now when we execute the program, we see the following error, as expected:- ContactList.Contact contactA = new ContactList.Contact("A", "123-4567");
- ContactList.Contact contactB = new ContactList.Contact("B", "234-5678");
- contactA.Equals(contactB);
- // If any lock orderings changed during execution,
- // re-write the new order to the designated file:
- OrderedLock.DumpDependencyCode();
- }
acquired resource1 from main thread
acquired resource2 from worker thread
acquired resource1 from worker thread
acquired resource2 from main thread
Error: Possible Deadlock condition found:
Circular dependency found:
Lock held:OrderedLock: Ostrowski_Contact_lockThis, Info:
Lock attempted:OrderedLock: Ostrowski_Contact_lockThis, Info:
Ostrowski.ContactList.Contact::Boolean Equals(Ostrowski.ContactL
ist.Contact) C:\Users\Paul\My Document\Locking App\Phase3\Ostrowski\Con
tactList\Contact.cs(32)
Ostrowski.Program::Void Main(System.String[]) C:\Users\Paul\My
Document\Locking App\Phase3\Program.cs(37)
...System level method(s)...
The only way to solve this is to require a higher-order lock be locked first. This means that only thread at a time can acquire the first or second lock, thus preventing the deadlock condition. I call this lock a peer locking authority. It’s vital to realize that the peer locking authority lock must be the same instance called from each thread, and not an instance member of the object. Usually this is achieved by a static member of the class, but could be done other ways as well. To implement the peer locking authority, we declare the peer locking authority of a class when we construct the OrderedLock object, and then when we encounter a lock order violation, where both locks involved in the order violation have the same instance of a peer locking authority, and that single lock is held by the calling thread, we can safely ignore the error condition:acquired resource2 from worker thread
acquired resource1 from worker thread
acquired resource2 from main thread
Error: Possible Deadlock condition found:
Circular dependency found:
Lock held:OrderedLock: Ostrowski_Contact_lockThis, Info:
Lock attempted:OrderedLock: Ostrowski_Contact_lockThis, Info:
Ostrowski.ContactList.Contact::Boolean Equals(Ostrowski.ContactL
ist.Contact) C:\Users\Paul\My Document\Locking App\Phase3\Ostrowski\Con
tactList\Contact.cs(32)
Ostrowski.Program::Void Main(System.String[]) C:\Users\Paul\My
Document\Locking App\Phase3\Program.cs(37)
...System level method(s)...
File Ostrowski\Utilities\OrderedLock.cs:
Now in our TestAcquisition method, we check for the presence of this new lock:- public object ObjectToLock { get; private set; }
- // If this is set, it allows for order violations to occur, provided the peer locking authority lock is locked.
- public OrderedLock PeerLockingAuthority { get; private set; }
- public OrderedLock(ID id)
- : this(id, new object(), null/*peerLockingAuthority*/)
- {
- }
- public OrderedLock(ID id, OrderedLock peerLockingAuthority)
- : this(id, new object(), peerLockingAuthority)
- {
- }
- public OrderedLock(ID id, object objectToLock)
- : this(id, objectToLock, null/*peerLockingAuthority*/)
- {
- }
- public OrderedLock(ID id, object objectToLock, OrderedLock peerLockingAuthority)
- {
- this.Id = id;
- this.ObjectToLock = objectToLock;
- this.PeerLockingAuthority = peerLockingAuthority;
- #if DEBUG
File Ostrowski\Utilities\OrderedLock.cs:
Now we add the new static OrderedLock to the Contact class, and lock that lock before we lock either member lock in the Equals method. As I mentioned before, this must be declare static, so that each instance of a Contact class will use the same instance of the peer locking authority when it constructs its own member lockThis OrderedLock:- private void TestAcquisition()
- {
- List<OrderedLock> listOfOwnedObjects = ThreadOwnedLocks.CopyList();
- // Check if this lock is already held.
- // If so, there is no need to test the order against other locks
- if (listOfOwnedObjects.Contains(this))
- return;
- // Iterate across each lock already held, and make sure we know
- // about this parent-child relationship:
- foreach (OrderedLock heldOrderedLock in listOfOwnedObjects)
- {
- // RegisterParentChildRelationship will return false if an order violation is identified
- if (!ParentChildOrderingOnID.RegisterParentChildRelationship(
- heldOrderedLock.Id, this.Id))
- {
- // An order violation has been found, but it may be OK if a common
- // peer locking authority is held by both locks:
- bool peerLockingAuthorityHeld = false;
- if ((heldOrderedLock.PeerLockingAuthority != null) &&
- (object.ReferenceEquals(heldOrderedLock.PeerLockingAuthority,
- this.PeerLockingAuthority)))
- {
- // A peer locking authority can permit objects of the same order to be
- // locked. It does not grant the permission to lock in reverse order!
- if (ParentChildOrderingOnID.GetMinimumOrder(heldOrderedLock.Id) ==
- ParentChildOrderingOnID.GetMinimumOrder(this.Id))
- {
- // If we are holding the lock on the PeerLockingAuthority for
- // both of these object, this is not an error.
- foreach (OrderedLock heldLock in listOfOwnedObjects)
- {
- if (object.ReferenceEquals(heldLock, this.PeerLockingAuthority))
- {
- peerLockingAuthorityHeld = true;
- break;
- }
- }
- }
- }
- if (!peerLockingAuthorityHeld)
- {
- // Problem found!
- OrderedLock.circularDependencyFound = true;
- OrderedLock.ReportPossibleDeadlockCondition(
- "Circular dependency found", heldOrderedLock,
- this, 2/*stackFramesToIgnoreInErrorReport*/);
- }
- }
- }
- }
File Ostrowski\ContactList\Contact.cs:
And of course we must add the new enum value in OrderedLock.ID:- static readonly OrderedLock lockThisPeerLockingAuth = new OrderedLock(OrderedLock.ID.Ostrowski_Contact_lockThisPeerLockingAuth);
- readonly OrderedLock lockThis = new OrderedLock(OrderedLock.ID.Ostrowski_Contact_lockThis, Contact.lockThisPeerLockingAuth);
- public bool Equals(Contact other)
- {
- using (Contact.lockThisPeerLockingAuth.Lock())
- {
- using (this.lockThis.Lock())
- {
- using (other.lockThis.Lock())
- {
- if (this.Name != other.Name) return false;
- if (this.PhoneNumber != other.PhoneNumber) return false;
- }
- }
- }
- return true;
- }
File Ostrowski\Utilities\OrderedLock.cs:
Now we can run the application, and see that it no longer reports an error. Furthermore, we should see after the execution completed that the source code to OrderedLock.cs has been modified to include the newly defined relationships between the OrderedLock objects in the Contact class.- public enum ID
- {
- Ostrowski_Program_resource1,
- Ostrowski_Program_resource2,
- Ostrowski_Program_signalLock,
- Ostrowski_Contact_lockThis,
- Ostrowski_Contact_lockThisPeerLockingAuth,
- }
At this point, the framework is pretty much complete. The next part of this series will focus on improved diagnostics and debug options. Stay tuned for the upcoming releases.
You can download a full copy of the completed project from my shared skydrive here: Sky Drive, for this module, download Phase3.zip.
Here are links to each part. I will update this list as modules become available:
No comments:
Post a Comment