{"id":110129,"date":"2024-08-14T07:00:00","date_gmt":"2024-08-14T14:00:00","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/oldnewthing\/?p=110129"},"modified":"2024-08-14T09:20:10","modified_gmt":"2024-08-14T16:20:10","slug":"20240814-00","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/oldnewthing\/20240814-00\/?p=110129","title":{"rendered":"Temporarily dropping a lock: The anti-lock pattern"},"content":{"rendered":"<hr \/>\n<p>There is a common pattern in C++ of using an RAII type to manage a synchronization primitive. There are different versions of this, but they all have the same basic pattern:<\/p>\n<ul>\n<li>Creating the object from a synchronization object: Locks the synchronization object.<\/li>\n<li>Destructing the object: Unlocks the synchronization object.<\/li>\n<\/ul>\n<p>These types go by various names, like <code>std::<wbr \/>lock_<wbr \/>guard<\/code>, <code>std::<wbr \/>unique_<wbr \/>lock<\/code>, or <code>std::<wbr \/>coped_<wbr \/>lock<\/code>, and specific libraries may have versions for their own types, such as C++\/WinRT&#8217;s <code>winrt::<wbr \/>slim_<wbr \/>lock_<wbr \/>guard<\/code> and WIL&#8217;s <code>wil::<wbr \/>rwlock_<wbr \/>release_<wbr \/>exclusive_<wbr \/>scope_<wbr \/>exit<\/code> (which you thankfully never actually write out; just use <code>auto<\/code>).<\/p>\n<p>One thing that is missing from most standard libraries, however, is the <i>anti<\/i>-lock.<\/p>\n<p>The idea of the anti-lock is that it counteracts an active lock.<\/p>\n<pre>template&lt;typename Mutex&gt;\r\nstruct anti_lock\r\n{\r\n    anti_lock() = default;\r\n\r\n    explicit anti_lock(Mutex&amp; mutex)\r\n    : m_mutex(std::addressof(mutex)) {\r\n        if (m_mutex) m_mutex-&gt;unlock();\r\n    }\r\n\r\nprivate:\r\n    struct anti_lock_deleter {\r\n        void operator()(Mutex* mutex) { mutex-&gt;lock(); }\r\n    };\r\n\r\n    std::unique_ptr&lt;Mutex, anti_lock_deleter&gt; m_mutex;\r\n};\r\n<\/pre>\n<p>The anti-lock <i>unlocks<\/i> a mutex at construction and locks it at destruction. Here&#8217;s an example:<\/p>\n<pre>void Widget::DoSomething()\r\n{\r\n    auto guard = std::lock_guard(m_mutex);\r\n\r\n    \u27e6 do stuff under the lock \u27e7\r\n\r\n    int cost;\r\n    if (m_isStandard) {\r\n        cost = GetStandardCost();\r\n    } else {\r\n        \/\/ Drop the lock temporarily while we call out.\r\n        auto anti_guard = anti_lock(m_mutex);\r\n        cost = m_callback-&gt;GetCost();\r\n    }();\r\n\r\n    \/\/ We are back under the lock.\r\n    \u27e6 do more stuff under the lock \u27e7\r\n}\r\n<\/pre>\n<p>The idea here is that you know you are running some code that acquires a lock, but you need to drop the lock temporarily, and then reacquire it afterward. The reason for dropping the lock might be that you are calling out to another component and don&#8217;t want to create a deadlock.<\/p>\n<p>For example, <a title=\"I've been thinking about the IMemoryBufferReference.Closed() event problem\" href=\"https:\/\/devblogs.microsoft.com\/oldnewthing\/20240221-00\/?p=109431#comment-141182\"> commenter Joshua Hudson could have used this around all of the <code>co_await<\/code>s<\/a>.<\/p>\n<pre>winrt::fire_and_forget DoSomething()\r\n{\r\n    auto guard = std::lock_guard(m_mutex);\r\n\r\n    step1();\r\n\r\n    \/\/ All co_awaits must be under an anti-lock.\r\n    int cost = [&amp;] {\r\n        auto anti_guard = anti_lock(m_mutex);\r\n        return co_await GetCostAsync();\r\n    }();\r\n\r\n    step2(cost);\r\n}\r\n<\/pre>\n<p>For extra safety, you might require that the anti-lock be given the lock guard that it is counteracting.<\/p>\n<pre>template&lt;typename Guard&gt;\r\nstruct anti_lock\r\n{\r\n    using mutex_type = typename Guard::mutex_type;\r\n\r\n    anti_lock() = default;\r\n\r\n    explicit anti_lock(Guard&amp; guard)\r\n    : m_mutex(guard.mutex())\r\n        if (m_mutex) m_mutex-&gt;unlock();\r\n    }\r\n\r\nprivate:\r\n    struct anti_lock_deleter {\r\n        void operator()(mutex_type* mutex) { m_mutex-&gt;lock(); }\r\n    };\r\n\r\n    std::unique_ptr&lt;mutex_type, anti_lock_deleter&gt; m_mutex;\r\n};\r\n<\/pre>\n<p>Being given the lock guard means that we can also make it so that the anti-lock of a non-owning guard is a non-owning anti-lock. The negative of zero is zero.<\/p>\n<p>Being given the lock guard also makes it slightly more noticeable to the caller that the anti-lock might mess with the lock state.<\/p>\n<p>Now, an anti-lock sounds weird, but you could very well be using it without realizing it: <code>std::<wbr \/>condition_<wbr \/>variable<\/code>s are secretly anti-locks. They enter with the lock held, then drop the lock while blocked, then reacquire the lock when unblocked.<\/p>\n<p>Here&#8217;s another scenario where you may want to use an anti-lock:<\/p>\n<pre>void DoSomething()\r\n{\r\n    \/\/ Hold the lock while we check m_nextQuery\r\n    std::unique_lock lock(m_mutex);\r\n    while (auto query = std::exchange(m_nextQuery, nullptr)) {\r\n        \/\/ Drop the lock while we do work\r\n        anti_lock anti(lock);\r\n        refresh_from_query(query);\r\n        \/\/ Reacquire the lock before rechecking m_nextQuery\r\n    }\r\n}\r\n<\/pre>\n<p>You need to hold the mutex while checking if there is a new query (because that mutex protects the code that sets the new query), but you can drop the mutex while you process the query.<\/p>\n<p>One downside of the anti-lock is that if you have an early return, the mutex is re-locked (when the anti-lock destructs) and then unlocked (when the outer guard destructs). This is hard to fix because there&#8217;s no guarantee that the outer guard is going to destruct when the anti-lock destructs:<\/p>\n<pre>void DoSomething()\r\n{\r\n    \/\/ Hold the lock while we check m_nextQuery\r\n    std::unique_lock lock(m_mutex);\r\n    while (auto query = std::exchange(m_nextQuery, nullptr)) {\r\n        <span style=\"border: solid 1px currentcolor;\">try {<\/span>\r\n            \/\/ Drop the lock while we do work\r\n            anti_lock anti(lock);\r\n            refresh_from_query(query);\r\n            \/\/ Reacquire the lock before rechecking m_nextQuery\r\n        <span style=\"border: solid 1px currentcolor;\">} CATCH_LOG(); \/\/ log refresh failures but don't stop<\/span>\r\n    }\r\n}\r\n<\/pre>\n<p>If you can live with this suboptimal behavior (which presumably is infrequent), the anti-lock is pretty handy.<\/p>\n<p><b>Bonus chatter<\/b>: The anti-lock does require you to know for sure that the lock is held exactly once. If it&#8217;s not held at all, then your anti-lock is unlocking a mutex that isn&#8217;t even locked, which is not allowed. And if it&#8217;s held twice (allowed by mutex classes such as <code>std::shared_mutex<\/code> and <code>std::recursive_mutex<\/code>), then your anti-lock only counteracts one of the locks, leave the other lock still active.<\/p>\n<p>And of course anti-locks complicate lock analysis. If you use an anti-lock to counteract a lock held by the caller, then this invalidates the assumption that holding a lock across a function call protects the state guarded by the lock.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>It&#8217;s not to prevent locking, but rather to counteract a lock.<\/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-110129","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-oldnewthing","tag-code"],"acf":[],"blog_post_summary":"<p>It&#8217;s not to prevent locking, but rather to counteract a lock.<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/posts\/110129","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=110129"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/posts\/110129\/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=110129"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/categories?post=110129"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/tags?post=110129"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}