{"id":109685,"date":"2024-04-18T07:00:00","date_gmt":"2024-04-18T14:00:00","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/oldnewthing\/?p=109685"},"modified":"2024-04-18T07:32:36","modified_gmt":"2024-04-18T14:32:36","slug":"20240418-00","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/oldnewthing\/20240418-00\/?p=109685","title":{"rendered":"Adding state to the update notification pattern, part 2"},"content":{"rendered":"<p>Last time, we started looking at solving the problem of <a title=\"Adding state to the update notification pattern, part 1\" href=\"https:\/\/devblogs.microsoft.com\/oldnewthing\/20240417-00\/?p=109679\"> a stateful but coalescing update notification<\/a>, where multiple requests for work can arrive, and your only requirement is that you send a notification for the last one. Any time a new request for work arrives, it replaces the existing one.<\/p>\n<p>One attempt to fix this is to check if the work is already in progress, and if so, then hand off the new query to the existing worker. We are using <code>winrt::<wbr \/>fire_<wbr \/>and_<wbr \/>forget<\/code>, which fails fast on any unhandled exception. This saves us from having to worry about recovering from exceptions. (At least for now.)<\/p>\n<pre>class EditControl\r\n{\r\n    \u27e6 ... existing class members ... \u27e7\r\n\r\n    <span style=\"border: solid 1px currentcolor; border-bottom: none;\">std::mutex m_workMutex;             <\/span>\r\n    <span style=\"border: 1px currentcolor; border-style: none solid;\">std::mutex m_textMutex;             <\/span>\r\n    <span style=\"border: solid 1px currentcolor; border-top: none;\">std::optional&lt;string&gt; m_pendingText;<\/span>\r\n};\r\n\r\nwinrt::fire_and_forget\r\nEditControl::TextChanged(std::string text)\r\n{\r\n    auto lifetime = get_strong();\r\n\r\n    <span style=\"border: solid 1px currentcolor; border-bottom: none;\">auto workLock = std::unique_lock(m_workMutex, std::try_to_lock);<\/span>\r\n    <span style=\"border: 1px currentcolor; border-style: none solid;\">if (!workLock) {                                                <\/span>\r\n    <span style=\"border: 1px currentcolor; border-style: none solid;\">    auto textLock = std::unique_lock(m_textMutex);              <\/span>\r\n    <span style=\"border: 1px currentcolor; border-style: none solid;\">    m_pendingText = std::move(text);                            <\/span>\r\n    <span style=\"border: 1px currentcolor; border-style: none solid;\">    co_return;                                                  <\/span>\r\n    <span style=\"border: 1px currentcolor; border-style: none solid;\">}                                                               <\/span>\r\n    <span style=\"border: 1px currentcolor; border-style: none solid;\">\u00a0                                                               <\/span>\r\n    <span style=\"border: solid 1px currentcolor; border-top: none;\">while (true) {                                                  <\/span>\r\n        co_await winrt::resume_background();\r\n\r\n        std::vector&lt;std::string&gt; matches;\r\n        for (auto&amp;&amp; candidate : FindCandidates(text)) {\r\n            if (candidate.Verify()) {\r\n                matches.push_back(candidate.Text());\r\n            }\r\n        }\r\n\r\n        co_await winrt::resume_foreground(Dispatcher());\r\n\r\n        SetAutocomplete(matches);\r\n\r\n    <span style=\"border: solid 1px currentcolor; border-bottom: none;\">    auto text = std::unique_lock(m_textMutex);<\/span>\r\n    <span style=\"border: 1px currentcolor; border-style: none solid;\">    if (!m_pendingText) {                     <\/span>\r\n    <span style=\"border: 1px currentcolor; border-style: none solid;\">        co_return;                            <\/span>\r\n    <span style=\"border: 1px currentcolor; border-style: none solid;\">    }                                         <\/span>\r\n    <span style=\"border: 1px currentcolor; border-style: none solid;\">    text = std::move(*m_pendingText);         <\/span>\r\n    <span style=\"border: 1px currentcolor; border-style: none solid;\">    m_pendingText.reset();                    <\/span>\r\n    <span style=\"border: solid 1px currentcolor; border-top: none;\">}                                             <\/span>\r\n}\r\n<\/pre>\n<p>But before even thinking about whether this addresses the race condition, we have to call out that this code isn&#8217;t even legal.<\/p>\n<p>This code carries a lock across a suspension point, which we saw <a title=\"On the perils of holding a lock across a coroutine suspension point, part 2: Nonrecursive mutexes\" href=\"https:\/\/devblogs.microsoft.com\/oldnewthing\/20210708-00\/?p=105420\"> is not a good idea<\/a>. In this case, we use <code>try_<\/code> mode to acquire the mutex, and the rules for <code>try_lock<\/code> say two things, one bad, and the other worse.<\/p>\n<p>The bad thing is that <code>try_lock<\/code> is <a title=\"[thread.mutex.requirements.mutex]: An implementation may fail to obtain the lock even if it is not held by any other thread.\" href=\"https:\/\/timsong-cpp.github.io\/cppwp\/thread.mutex.requirements.mutex#general-15\"> allowed to fail spuriously<\/a> and return <code>false<\/code> even if the mutex is not locked. This means that it&#8217;s possible that the <code>workLock<\/code> will report that it does not own the lock, even though the lock is available, and you will have a <code>m_pendingText<\/code> sitting around waiting futilely for some work to process it.<\/p>\n<p>The worse thing is that calling <code>try_lock<\/code> from a thread that already holds the mutex <a title=\"[thread.mutex.requirements.mutex]: Preconditions: The calling thread does not own the mutex.\" href=\"https:\/\/timsong-cpp.github.io\/cppwp\/thread.mutex.requirements.mutex#general-14\"> results in undefined behavior<\/a>. If two text changes occur in rapid succession on the UI thread, the second one will try to lock the <code>m_workMutex<\/code> from the same thread that already locked it, and you have now broken the rules and anything could happen.<\/p>\n<p>Even worse than the worse case is the possibility that the mutex is released from the wrong thread. This code switches back to the UI thread before allowing the <code>unique_<wbr \/>lock<\/code> to destruct, so you think you&#8217;re safe, but you&#8217;re not because an exception while building the matches will result in the lock being destructed from a background thread. This happens before the promise&#8217;s <code>unhandled_<wbr \/>exception<\/code> is called, so you&#8217;ve corrupted the system before your <code>fire_<wbr \/>and_<wbr \/>forget<\/code> can fail fast.<\/p>\n<p>The <code>m_workMutex<\/code> is really a red herring. It doesn&#8217;t need to be a mutex. The code uses it merely as a flag. So let&#8217;s switch to a flag and avoid all the undefined behavior.<\/p>\n<p>Also, the <code>m_textMutex<\/code> is unnecessary since the <code>m_pendingText<\/code> is always accessed from the UI thread, so there is no concurrency. We can get rid of that too.<\/p>\n<p>We&#8217;re now left with this:<\/p>\n<pre>class EditControl\r\n{\r\n    \u27e6 ... existing class members ... \u27e7\r\n\r\n    <span style=\"border: solid 1px currentcolor;\">bool m_busy = false;<\/span>\r\n    <span style=\"border: dashed 1px currentcolor;\">\/\/ <span style=\"text-decoration: line-through;\">std::mutex m_textMutex;<\/span> \/\/ no longer needed<\/span>\r\n    std::optional&lt;string&gt; m_pendingText;\r\n};\r\n\r\nwinrt::fire_and_forget\r\nEditControl::TextChanged(std::string text)\r\n{\r\n    auto lifetime = get_strong();\r\n\r\n    <span style=\"border: solid 1px currentcolor; border-bottom: none;\">if (std::exchange(m_busy, true)) {<\/span>\r\n    <span style=\"border: 1px currentcolor; border-style: none solid;\">    m_pendingText = text;         <\/span>\r\n    <span style=\"border: 1px currentcolor; border-style: none solid;\">    co_return;                    <\/span>\r\n    <span style=\"border: 1px currentcolor; border-style: none solid;\">}                                 <\/span>\r\n    <span style=\"border: 1px currentcolor; border-style: none solid;\">\u00a0                                 <\/span>\r\n    <span style=\"border: solid 1px currentcolor; border-top: none;\">while (true) {                    <\/span>\r\n        co_await winrt::resume_background();\r\n\r\n        std::vector&lt;std::string&gt; matches;\r\n        for (auto&amp;&amp; candidate : FindCandidates(text)) {\r\n            if (candidate.Verify()) {\r\n                matches.push_back(candidate.Text());\r\n            }\r\n        }\r\n\r\n        co_await winrt::resume_foreground(Dispatcher());\r\n\r\n        SetAutocomplete(matches);\r\n\r\n    <span style=\"border: solid 1px currentcolor; border-bottom: none;\">    if (!m_pendingText) {              <\/span>\r\n    <span style=\"border: 1px currentcolor; border-style: none solid;\">        m_busy = false;                <\/span>\r\n    <span style=\"border: 1px currentcolor; border-style: none solid;\">        co_return;                     <\/span>\r\n    <span style=\"border: 1px currentcolor; border-style: none solid;\">    }                                  <\/span>\r\n    <span style=\"border: 1px currentcolor; border-style: none solid;\">    text = std::move(*m_pendingText);  <\/span>\r\n    <span style=\"border: 1px currentcolor; border-style: none solid;\">    m_pendingText.reset();             <\/span>\r\n    <span style=\"border: solid 1px currentcolor; border-top: none;\">}                                      <\/span>\r\n}\r\n<\/pre>\n<p>We can simplify the code by simply treating every case as the pending case.<\/p>\n<pre>winrt::fire_and_forget\r\nEditControl::TextChanged(std::string text)\r\n{\r\n    auto lifetime = get_strong();\r\n\r\n    <span style=\"border: solid 1px currentcolor; border-bottom: none;\">m_pendingText = std::move(text);     <\/span>\r\n    <span style=\"border: 1px currentcolor; border-style: none solid;\">if (std::exchange(m_busy, true)) {   <\/span>\r\n    <span style=\"border: 1px currentcolor; border-style: none solid;\">    co_return;                       <\/span>\r\n    <span style=\"border: 1px currentcolor; border-style: none solid;\">}                                    <\/span>\r\n    <span style=\"border: 1px currentcolor; border-style: none solid;\">\u00a0                                    <\/span>\r\n    <span style=\"border: 1px currentcolor; border-style: none solid;\">while (m_pendingText) {              <\/span>\r\n    <span style=\"border: 1px currentcolor; border-style: none solid;\">    text = std::move(*m_pendingText);<\/span>\r\n    <span style=\"border: solid 1px currentcolor; border-top: none;\">    m_pendingText.reset();           <\/span>\r\n\r\n        co_await winrt::resume_background();\r\n\r\n        std::vector&lt;std::string&gt; matches;\r\n        for (auto&amp;&amp; candidate : FindCandidates(text)) {\r\n            if (candidate.Verify()) {\r\n                matches.push_back(candidate.Text());\r\n            }\r\n        }\r\n\r\n        co_await winrt::resume_foreground(Dispatcher());\r\n\r\n        SetAutocomplete(matches);\r\n\r\n    }\r\n    <span style=\"border: solid 1px currentcolor;\">m_busy = false;<\/span>\r\n}\r\n<\/pre>\n<p>The UI thread is doing a lot of heavy lifting here because it implicitly locks the combined accesses to <code>m_busy<\/code> and <code>m_pendingText<\/code>.<\/p>\n<p>Next time, we&#8217;ll try to reduce the amount of unnecessary work.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>First attempt to try to fix the race condition.<\/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-109685","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-oldnewthing","tag-code"],"acf":[],"blog_post_summary":"<p>First attempt to try to fix the race condition.<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/posts\/109685","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=109685"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/posts\/109685\/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=109685"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/categories?post=109685"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/tags?post=109685"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}