This is the tenth part of the Concurrency series. For your convenience you can find other parts in the table of contents in Part 1 – Mutex performance in .NET
We have already seen how to implement custom mutex using memory mapped files and CAS operation. It has one drawback — it is not reentrant. Actually, if we try to take if recursively we will end up with deadlock. Today we will fix it.
Since our mutex protocol requires a delegate to execute while holding mutex (kind of a RAII pattern or try-with-resources approach) we don’t need to worry about taking the mutex and not releasing it. Conceptually, this is not allowed:
1 2 3 4 5 |
mutex.Lock() mutex.Lock() // again ... mutex.Release() // Not releasing again |
It is guaranteed that mutex will be released once it’s taken. Well, this is not 100% true as there are AccessViolation
exceptions resulting in finally
block not being executed but we will ignore this fact. So, we don’t need to take lock for the second time if we can guarantee that we won’t try releasing it.
Let’s see this code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 |
using System; using System.Diagnostics; using System.IO.MemoryMappedFiles; using System.Linq; using System.Runtime.InteropServices; using System.Threading; namespace MutexUtils { public static class MutexUtils { [DllImport("kernel32.dll", SetLastError = true)] unsafe public static extern long InterlockedCompareExchange64(long* destination, long exchange, long comperand); public static unsafe bool DoWithMutex(string name, Action action, int timeoutInSeconds = int.MaxValue, int spinTimeout = 250) { var myPid = Process.GetCurrentProcess().Id; var myTid = AppDomain.GetCurrentThreadId(); // Store <PID><TID> as one field long myIdentifier = ((long)myPid << 32) + myTid; // Calculate timeout wall clock DateTime end = DateTime.UtcNow.AddSeconds(timeoutInSeconds); // Open memory mapped file initialized with zeros using (var memoryMappedFile = MemoryMappedFile.CreateOrOpen(name, 8)) using (var viewStream = memoryMappedFile.CreateViewStream(8, 8)) { var pointer = viewStream.SafeMemoryMappedViewHandle.DangerousGetHandle(); try { var currentLock = Interlocked.Read((long*)pointer); if(GetPid(currentLock) == myPid && GetTid(currentLock) == myTid){ action(); return true; } } catch(Exception e) { Console.WriteLine($"Something very bad happened: {e}"); return false; } try { int holderPid = -1; int holderTid = -1; while (DateTime.UtcNow < end) { // Take lock only if it is not taken var currentLock = InterlockedCompareExchange64((long*)pointer, myIdentifier, 0); if (currentLock == 0) { action(); return true; } // Lock is taken, let's see who holds it holderTid = GetTid(currentLock); holderPid = GetPid(currentLock); bool exists = false; try { exists = Process.GetProcessById(holderPid).Threads.OfType<ProcessThread>().Any(t => t.Id == holderTid); } catch { } // If holding thread doesn't exist then the lock is abandoned if (!exists) { // Clear lock only if it is still held by previous owner var currentLock2 = InterlockedCompareExchange64((long*)pointer, 0, currentLock); if (currentLock == currentLock2) { Console.WriteLine($"Mutex {name} was abandoned by pid={holderPid} tid={holderTid}"); } } Thread.Sleep(spinTimeout); } Console.WriteLine($"Timeout when waiting on mutex {name} held by pid={holderPid} tid={holderTid}"); } finally { // Clear lock only if I'm the one holding it var currentLock = InterlockedCompareExchange64((long*)pointer, 0, myIdentifier); if (currentLock != myIdentifier) { Console.WriteLine($"I tried to release mutex held by someone else, pid={GetPid(currentLock)} tid={GetTid(currentLock)}"); } } } return false; } private static int GetPid(long value) { return (int)(value >> 32); } private static int GetTid(long value) { return (int)(value & 0xFFFFFFFF); } } } |
We check in line 34 if the current owner of the lock is us — in that case we just execute the action and return early. If something wrong happened (we have exception) then we cannot assume anything about lock owner or executed action so we need to return immediately. The only thing we know for sure is that we didn’t modify the lock. You may be tempted to set a flag that lock is on us and return it in the exception handler — thinking that the exception was thrown by the executed action — but you cannot guarantee that you started executing the action (think about OOM thrown after setting the flag and before calling the handler). You could try enhancing this by using CER but then you constrain yourself a lot.