{"id":112288,"date":"2026-04-30T07:00:00","date_gmt":"2026-04-30T14:00:00","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/oldnewthing\/?p=112288"},"modified":"2026-04-30T07:54:52","modified_gmt":"2026-04-30T14:54:52","slug":"20260430-00","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/oldnewthing\/20260430-00\/?p=112288","title":{"rendered":"Developing a cross-process reader\/writer lock with limited readers, part 3: Fairness"},"content":{"rendered":"<p>We&#8217;ve been building a cross-process reader\/writer lock with a cap on the number of readers, we concluded our investigation last time by noting <a title=\"Developing a cross-process reader\/writer lock with limited readers, part 2: Taking turns when being grabby\" href=\"https:\/\/devblogs.microsoft.com\/oldnewthing\/20260429-00\/?p=112286\"> that throughput of exclusive accesses was poor<\/a>. What&#8217;s going on?<\/p>\n<p>The problem is that exclusive acquisitions are working to claim semaphore tokens one at a time, so it can lose out to shared acquisitions that are requested even after the exclusive acquisition had started, effectively letting shared acquisitions &#8220;jump ahead of the exclusive acquisition&#8221;, and thereby starving out exclusive acquisitions.<\/p>\n<table class=\"cp3\" style=\"border-collapse: collapse;\" border=\"0\" cellspacing=\"0\" cellpadding=\"3\">\n<tbody>\n<tr>\n<th style=\"border: solid 1px currentcolor;\" rowspan=\"2\">Token count<\/th>\n<th style=\"border: solid 1px currentcolor;\" rowspan=\"2\">Exclusive<br \/>\nacquirer<\/th>\n<th style=\"border: solid 1px currentcolor;\" colspan=\"4\">Shared acquirers<\/th>\n<\/tr>\n<tr>\n<th style=\"border: solid 1px currentcolor;\">A<\/th>\n<th style=\"border: solid 1px currentcolor;\">B<\/th>\n<th style=\"border: solid 1px currentcolor;\">C<\/th>\n<th style=\"border: solid 1px currentcolor;\">D<\/th>\n<\/tr>\n<tr>\n<td style=\"border: solid 1px currentcolor;\">5<\/td>\n<td style=\"border: solid 1px currentcolor;\">\u00a0<\/td>\n<td style=\"border: solid 1px currentcolor;\">\u00a0<\/td>\n<td style=\"border: solid 1px currentcolor;\">\u00a0<\/td>\n<td style=\"border: solid 1px currentcolor;\">\u00a0<\/td>\n<td style=\"border: solid 1px currentcolor;\">\u00a0<\/td>\n<\/tr>\n<tr>\n<td style=\"border: solid 1px currentcolor;\">4<\/td>\n<td style=\"border: solid 1px currentcolor;\">\u00a0<\/td>\n<td style=\"border: solid 1px currentcolor;\">Acq<\/td>\n<td style=\"border: solid 1px currentcolor;\">\u00a0<\/td>\n<td style=\"border: solid 1px currentcolor;\">\u00a0<\/td>\n<td style=\"border: solid 1px currentcolor;\">\u00a0<\/td>\n<\/tr>\n<tr>\n<td style=\"border: solid 1px currentcolor;\">3<\/td>\n<td style=\"border: solid 1px currentcolor;\">\u00a0<\/td>\n<td style=\"border: solid 1px currentcolor;\">\u00a0<\/td>\n<td style=\"border: solid 1px currentcolor;\">Acq<\/td>\n<td style=\"border: solid 1px currentcolor;\">\u00a0<\/td>\n<td style=\"border: solid 1px currentcolor;\">\u00a0<\/td>\n<\/tr>\n<tr>\n<td style=\"border: solid 1px currentcolor;\">2<\/td>\n<td style=\"border: solid 1px currentcolor;\">1st Acq<\/td>\n<td style=\"border: solid 1px currentcolor;\">\u00a0<\/td>\n<td style=\"border: solid 1px currentcolor;\">\u00a0<\/td>\n<td style=\"border: solid 1px currentcolor;\">\u00a0<\/td>\n<td style=\"border: solid 1px currentcolor;\">\u00a0<\/td>\n<\/tr>\n<tr>\n<td style=\"border: solid 1px currentcolor;\">1<\/td>\n<td style=\"border: solid 1px currentcolor;\">2nd Acq<\/td>\n<td style=\"border: solid 1px currentcolor;\">\u00a0<\/td>\n<td style=\"border: solid 1px currentcolor;\">\u00a0<\/td>\n<td style=\"border: solid 1px currentcolor;\">\u00a0<\/td>\n<td style=\"border: solid 1px currentcolor;\">\u00a0<\/td>\n<\/tr>\n<tr>\n<td style=\"border: solid 1px currentcolor;\">0<\/td>\n<td style=\"border: solid 1px currentcolor;\">3rd Acq<\/td>\n<td style=\"border: solid 1px currentcolor;\">\u00a0<\/td>\n<td style=\"border: solid 1px currentcolor;\">\u00a0<\/td>\n<td style=\"border: solid 1px currentcolor;\">\u00a0<\/td>\n<td style=\"border: solid 1px currentcolor;\">\u00a0<\/td>\n<\/tr>\n<tr>\n<td style=\"border: solid 1px currentcolor;\">0<\/td>\n<td style=\"border: solid 1px currentcolor;\">4th Acq (blocks)<\/td>\n<td style=\"border: solid 1px currentcolor;\">\u00a0<\/td>\n<td style=\"border: solid 1px currentcolor;\">\u00a0<\/td>\n<td style=\"border: solid 1px currentcolor;\">\u00a0<\/td>\n<td style=\"border: solid 1px currentcolor;\">\u00a0<\/td>\n<\/tr>\n<tr>\n<td style=\"border: solid 1px currentcolor;\">0<\/td>\n<td style=\"border: solid 1px currentcolor;\">\u00a0<\/td>\n<td style=\"border: solid 1px currentcolor;\">\u00a0<\/td>\n<td style=\"border: solid 1px currentcolor;\">\u00a0<\/td>\n<td style=\"border: solid 1px currentcolor;\">Acq (blocks)<\/td>\n<td style=\"border: solid 1px currentcolor;\">\u00a0<\/td>\n<\/tr>\n<tr>\n<td style=\"border: solid 1px currentcolor;\">0<\/td>\n<td style=\"border: solid 1px currentcolor;\">\u00a0<\/td>\n<td style=\"border: solid 1px currentcolor;\">\u00a0<\/td>\n<td style=\"border: solid 1px currentcolor;\">\u00a0<\/td>\n<td style=\"border: solid 1px currentcolor;\">\u00a0<\/td>\n<td style=\"border: solid 1px currentcolor;\">Acq (blocks)<\/td>\n<\/tr>\n<tr>\n<td style=\"border: solid 1px currentcolor;\">1<\/td>\n<td style=\"border: solid 1px currentcolor;\">\u00a0<\/td>\n<td style=\"border: solid 1px currentcolor;\">Rel<\/td>\n<td style=\"border: solid 1px currentcolor;\">\u00a0<\/td>\n<td style=\"border: solid 1px currentcolor;\">\u00a0<\/td>\n<td style=\"border: solid 1px currentcolor;\">\u00a0<\/td>\n<\/tr>\n<tr>\n<td style=\"border: solid 1px currentcolor;\">0<\/td>\n<td style=\"border: solid 1px currentcolor;\">4th Acq<\/td>\n<td style=\"border: solid 1px currentcolor;\">\u00a0<\/td>\n<td style=\"border: solid 1px currentcolor;\">\u00a0<\/td>\n<td style=\"border: solid 1px currentcolor;\">\u00a0<\/td>\n<td style=\"border: solid 1px currentcolor;\">\u00a0<\/td>\n<\/tr>\n<tr>\n<td style=\"border: solid 1px currentcolor;\">0<\/td>\n<td style=\"border: solid 1px currentcolor;\">5th Acq (blocks)<\/td>\n<td style=\"border: solid 1px currentcolor;\">\u00a0<\/td>\n<td style=\"border: solid 1px currentcolor;\">\u00a0<\/td>\n<td style=\"border: solid 1px currentcolor;\">\u00a0<\/td>\n<td style=\"border: solid 1px currentcolor;\">\u00a0<\/td>\n<\/tr>\n<tr>\n<td style=\"border: solid 1px currentcolor;\">1<\/td>\n<td style=\"border: solid 1px currentcolor;\">\u00a0<\/td>\n<td style=\"border: solid 1px currentcolor;\">\u00a0<\/td>\n<td style=\"border: solid 1px currentcolor;\">Rel<\/td>\n<td style=\"border: solid 1px currentcolor;\">\u00a0<\/td>\n<td style=\"border: solid 1px currentcolor;\">\u00a0<\/td>\n<\/tr>\n<tr>\n<td style=\"border: solid 1px currentcolor;\">0<\/td>\n<td style=\"border: solid 1px currentcolor;\">\u00a0<\/td>\n<td style=\"border: solid 1px currentcolor;\">\u00a0<\/td>\n<td style=\"border: solid 1px currentcolor;\">\u00a0<\/td>\n<td style=\"border: solid 1px currentcolor;\">Acq<\/td>\n<td style=\"border: solid 1px currentcolor;\">\u00a0<\/td>\n<\/tr>\n<tr>\n<td style=\"border: solid 1px currentcolor;\">1<\/td>\n<td style=\"border: solid 1px currentcolor;\">\u00a0<\/td>\n<td style=\"border: solid 1px currentcolor;\">\u00a0<\/td>\n<td style=\"border: solid 1px currentcolor;\">\u00a0<\/td>\n<td style=\"border: solid 1px currentcolor;\">Rel<\/td>\n<td style=\"border: solid 1px currentcolor;\">\u00a0<\/td>\n<\/tr>\n<tr>\n<td style=\"border: solid 1px currentcolor;\">0<\/td>\n<td style=\"border: solid 1px currentcolor;\">\u00a0<\/td>\n<td style=\"border: solid 1px currentcolor;\">\u00a0<\/td>\n<td style=\"border: solid 1px currentcolor;\">\u00a0<\/td>\n<td style=\"border: solid 1px currentcolor;\">\u00a0<\/td>\n<td style=\"border: solid 1px currentcolor;\">Acq<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Let&#8217;s say that we have capped the number of shared acquisitions to five. In the above scenario, we have an exclusive acquiring thread and four shared acquiring threads. The first two shared acquiring threads (call them A and B) succeed at their shared acquisitions, and then the exclusive acquiring thread tries to enter exclusively. The exclusive acquiring thread needs five tokens, and it quickly gets three of them before blocking when it tries to get the fourth.<\/p>\n<p>While the exclusive acquiring thread is waiting to get its fourth token, two other shared acquiring threads (call them C and D) try to enter in shared mode. They too block.<\/p>\n<p>One of the original shared acquiring threads releases its shared lock, which release a token, and that token is quickly snapped up by the exclusive acquiring thread, thanks to the &#8220;mostly FIFO&#8221; policy for synchronization objects. (Let&#8217;s assume for the purpose of this discussion that none of the things that violate FIFO-ness has occurred.) The exclusive acquiring thread now waits to claim its fifth token.<\/p>\n<p>When the second of the original shared acquiring threads releases its token, it is given to thread C, even though thread C started its shared acquisition <i>after<\/i> the exclusive acquiring thread tried to acquire exclusively.<\/p>\n<p>And then when thread C releases its token, that token is given to thread D, since its request for the token precedes the exclusive thread&#8217;s request for the fifth token. The exclusive acquiring thread has once again gotten boxed out.<\/p>\n<p>To fix this, we can make <i>all<\/i> acquisitions claim the shared mutex. The shared mutex then does the work of enforcing &#8220;mostly FIFO&#8221; acquisition behavior across all acquisitions.<\/p>\n<p>Since we&#8217;re going to be doing combined timeouts, I&#8217;ll refactor the timeout management into a helper class.<\/p>\n<pre>struct TimeoutTracker\r\n{\r\n    explicit TimeoutTracker(DWORD timeout)\r\n        : m_timeout(timeout) {}\r\n\r\n    DWORD m_start = GetTickCount();\r\n\r\n    bool Wait(HANDLE h)\r\n    {\r\n        DWORD elapsed = GetTickCount() - m_start;\r\n        if (elapsed &gt; m_timeout) return false;\r\n        return WaitForSingleObject(h, m_timeout - elapsed)\r\n                    == WAIT_OBJECT_0;\r\n    }\r\n};\r\n<\/pre>\n<p>We can use this helper class to manage our timeouts.<\/p>\n<pre>HANDLE sharedSemaphore;\r\nHANDLE sharedMutex;\r\n\r\nvoid AcquireShared()\r\n{\r\n    <span style=\"border: solid 1px currentcolor;\">WaitForSingleObject(sharedMutex, INFINITE);<\/span>\r\n\r\n    WaitForSingleObject(sharedSemaphore, INFINITE);\r\n\r\n    <span style=\"border: solid 1px currentcolor;\">ReleaseMutex(sharedMutex);<\/span>\r\n}\r\n\r\nbool AcquireSharedWithTimeout(DWORD timeout)\r\n{\r\n    <span style=\"border: solid 1px currentcolor; border-bottom: none;\">TimeoutTracker tracker(timeout);         <\/span>\r\n    <span style=\"border: 1px currentcolor; border-style: none solid;\">bool result = tracker.Wait(hSharedMutex);<\/span>\r\n    <span style=\"border: 1px currentcolor; border-style: none solid;\">if (!result) return false;               <\/span>\r\n    <span style=\"border: 1px currentcolor; border-style: none solid;\">result = tracker.Wait(sharedSemaphore);  <\/span>\r\n    <span style=\"border: 1px currentcolor; border-style: none solid;\">ReleaseMutex(sharedMutex);               <\/span>\r\n    <span style=\"border: solid 1px currentcolor; border-top: none;\">return result;                           <\/span>\r\n}\r\n\r\n\/\/ no change to AcquireExclusive\r\nvoid AcquireExclusive()\r\n{\r\n    WaitForSingleObject(sharedMutex, INFINITE);\r\n\r\n    for (unsigned i = 0; i &lt; MAX_SHARED; i++) {\r\n        WaitForSingleObject(sharedSemaphore, INFINITE);\r\n    }\r\n\r\n    ReleaseMutex(sharedMutex);\r\n}\r\n\r\n\/\/ no functional change, but using the new helper class\r\nbool AcquireExclusiveWithTimeout(DWORD timeout)\r\n{\r\n    <span style=\"border: solid 1px currentcolor; border-bottom: none;\">TimeoutTracker tracker(timeout);        <\/span>\r\n    <span style=\"border: 1px currentcolor; border-style: none solid;\">bool result = tracker.Wait(sharedMutex);<\/span>\r\n    <span style=\"border: solid 1px currentcolor; border-top: none;\">if (!result) return false;              <\/span>\r\n\r\n    for (unsigned i = 0; i &lt; MAX_SHARED; i++) {\r\n        <span style=\"border: solid 1px currentcolor;\">if (!tracker.Wait(sharedSemaphore)) {<\/span>\r\n            \/\/ Restore the tokens we already claimed.\r\n            if (i &gt; 0) {\r\n                ReleaseSemaphore(sharedSemaphore, i, nullptr);\r\n            }\r\n            ReleaseMutex(sharedMutex);\r\n            return false;\r\n        }\r\n    }\r\n    ReleaseMutex(sharedMutex);\r\n    return true;\r\n}\r\n<\/pre>\n<p>(Yes, I&#8217;m not using RAII. I&#8217;ve made that choice for expository purposes, since it lets you see exactly when each synchronization object is acquired and released.)<\/p>\n<p>Are we done?<\/p>\n<p>No, we&#8217;re not done.<\/p>\n<p>There is still a serious problem that needs to be fixed. We&#8217;ll look at it next time.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Let the exclusive acquisition have a fair chance against shared acquisitions.<\/p>\n","protected":false},"author":1069,"featured_media":111744,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[1],"tags":[25],"class_list":["post-112288","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-oldnewthing","tag-code"],"acf":[],"blog_post_summary":"<p>Let the exclusive acquisition have a fair chance against shared acquisitions.<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/posts\/112288","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/users\/1069"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/comments?post=112288"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/posts\/112288\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/media\/111744"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/media?parent=112288"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/categories?post=112288"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/tags?post=112288"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}