{"id":109702,"date":"2024-04-25T07:00:00","date_gmt":"2024-04-25T14:00:00","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/oldnewthing\/?p=109702"},"modified":"2024-04-25T09:32:55","modified_gmt":"2024-04-25T16:32:55","slug":"20240425-00","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/oldnewthing\/20240425-00\/?p=109702","title":{"rendered":"Adding state to the update notification pattern, part 7"},"content":{"rendered":"<p>Last time, we refined our <a title=\"Adding state to the update notification pattern, part 6\" href=\"https:\/\/devblogs.microsoft.com\/oldnewthing\/20240424-00\/?p=109700\"> change counter-based stateful but coalescing update notification<\/a>. This version still relies on a UI thread to do two things: (1) make the final final change counter check and the subsequent callback atomic, and (2) to serialize the callbacks.<\/p>\n<p>If we don&#8217;t have a UI thread, then we open a race condition.<\/p>\n<pre>class EditControl\r\n{\r\n    \u27e6 ... existing class members ... \u27e7\r\n\r\n    std::atomic&lt;unsigned&gt; m_latestId;\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    auto id = m_latestId.fetch_add(1, std::memory_order_relaxed);\r\n\r\n    co_await winrt::resume_background();\r\n\r\n    if (!IsLatestId(id))) co_return;\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        if (!IsLatestId(id))) co_return;\r\n    }\r\n\r\n    <span style=\"border: dashed 1px currentcolor;\">\/\/ <span style=\"text-decoration: line-through;\">co_await winrt::resume_foreground(Dispatcher());<\/span><\/span>\r\n\r\n    if (!IsLatestId(id))) co_return;\r\n\r\n    SetAutocomplete(matches);\r\n}\r\n<\/pre>\n<p>Another call to <code>TextChanged<\/code> could happen just before the <code>Set\u00adAutocomplete<\/code>, and its work could race ahead of the first task.<\/p>\n<table border=\"0\" cellspacing=\"0\" cellpadding=\"0\">\n<tbody>\n<tr>\n<th style=\"border: solid 1px currentcolor; border-top: none; padding: 0 1ex;\">UI thread<\/th>\n<th style=\"border: solid 1px currentcolor; border-top: none; padding: 0 1ex;\">Background thread 1<\/th>\n<th style=\"border: solid 1px currentcolor; border-top: none; padding: 0 1ex;\">Background thread 2<\/th>\n<\/tr>\n<tr>\n<td style=\"border-right: solid 1px currentcolor; padding: 0 1ex;\"><tt>TextChanged(\"Bob\")<\/tt><br \/>\n<tt>resume_background()<\/tt><\/td>\n<td style=\"border-right: solid 1px currentcolor; padding: 0 1ex;\">\u00a0<\/td>\n<td style=\"padding: 0 1ex;\">\u00a0<\/td>\n<\/tr>\n<tr>\n<td style=\"border-right: solid 1px currentcolor; padding: 0 1ex;\">\u00a0<\/td>\n<td style=\"border-right: solid 1px currentcolor; padding: 0 1ex;\">(Bob&#8217;s task)<br \/>\n<tt>id = m_latestId;<\/tt> (id is 1)<br \/>\ncalculate matches for &#8220;Bob&#8221;<br \/>\n<tt>if (1 == m_latestId) (true)<\/tt><\/td>\n<td style=\"padding: 0 1ex;\">\u00a0<\/td>\n<\/tr>\n<tr>\n<td style=\"border-right: solid 1px currentcolor; padding: 0 1ex;\"><tt>TextChanged(\"Alice\");<\/tt><br \/>\n<tt>resume_background()<\/tt><\/td>\n<td style=\"border-right: solid 1px currentcolor; padding: 0 1ex;\">\u00a0<\/td>\n<td style=\"padding: 0 1ex;\">\u00a0<\/td>\n<\/tr>\n<tr>\n<td style=\"border-right: solid 1px currentcolor; padding: 0 1ex;\">\u00a0<\/td>\n<td style=\"border-right: solid 1px currentcolor; padding: 0 1ex;\">\u00a0<\/td>\n<td style=\"padding: 0 1ex;\">(Alice&#8217;s task)<br \/>\n<tt>id = m_latestId;<\/tt> (id is 2)<br \/>\ncalculate matches for &#8220;Alice&#8221;<br \/>\n<tt>if (2 == m_latestId) (true)<\/tt><br \/>\n<tt>SetAutocomplete(alice's matches)<\/tt><\/td>\n<\/tr>\n<tr>\n<td style=\"border-right: solid 1px currentcolor; padding: 0 1ex;\">\u00a0<\/td>\n<td style=\"border-right: solid 1px currentcolor; padding: 0 1ex;\"><tt>SetAutocomplete(bob's matches)<\/tt><\/td>\n<td style=\"padding: 0 1ex;\">\u00a0<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>One temptation is to fix this by restoring atomicity by adding a lock:<\/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    auto id = m_latestId.fetch_add(1, std::memory_order_relaxed);\r\n\r\n    co_await winrt::resume_background();\r\n\r\n    if (!IsLatestId(id))) co_return;\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        if (!IsLatestId(id))) co_return;\r\n    }\r\n\r\n    <span style=\"border: solid 1px currentcolor;\">auto lock = std::unique_lock(m_mutex);<\/span>\r\n\r\n    if (!IsLatestId(id))) co_return;\r\n\r\n    SetAutocomplete(matches);\r\n}\r\n<\/pre>\n<p>The good news is that this avoids the race condition. One thing to check is that nothing bad happens if <code>Set\u00adAutocomplete<\/code> itself triggers a recursive call to <code>Text\u00adChanged<\/code>: Since the <code>Set\u00adAutocomplete<\/code> is happening on a background thread, a call to <code>Text\u00adChanged<\/code> would hop to another background thread before eventually blocking on the mutex. Nobody is deadlocked, so we&#8217;re okay there.<\/p>\n<p>But there is a problem if the <code>Text\u00adChange<\/code> calls come in faster than <code>Set\u00adAutocomplete<\/code> can process them. In that case, each <code>Text\u00adChange<\/code> consumes a background thread and blocks on the mutex. There could be a lot of background threads all waiting for their turn to call <code>Set\u00adAutocomplete<\/code>, but not able to make progress because the current active call to <code>Set\u00adAutocomplete<\/code> is taking too long. In the worst case, <code>Set\u00adAutocomplete<\/code> takes so long that you end up consuming all the threads in the threadpool, which tends not to end well.<\/p>\n<p>Instead of using a mutex, we can use an &#8220;async mutex&#8221;, where each waiting coroutine just suspends until its turn to enter the protected region. Fairness is not important here, because all but one of the coroutines will bail out when they realize that their change counter doesn&#8217;t match, so we can use <!-- backref: A simpler version of the task sequencer that doesn't promise fairness --> simple task sequencer that uses a kernel event.<\/p>\n<pre>class EditControl\r\n{\r\n    \u27e6 ... existing class members ... \u27e7\r\n\r\n    std::atomic&lt;unsigned&gt; m_latestId;\r\n    <span style=\"border: solid 1px currentcolor;\">wil::unique_event m_serializerEvent{ wil::EventOptions::Signaled };<\/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    auto id = m_latestId.fetch_add(1, std::memory_order_relaxed);\r\n\r\n    co_await winrt::resume_background();\r\n\r\n    if (!IsLatestId(id))) co_return;\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        if (!IsLatestId(id))) co_return;\r\n    }\r\n\r\n    <span style=\"border: solid 1px currentcolor; border-bottom: none;\">co_await winrt::resume_on_signal(m_serializerEvent.get());    <\/span>\r\n    <span style=\"border: solid 1px currentcolor; border-top: none;\">auto next = wil::SetEvent_scope_exit(m_serializerEvent.get());<\/span>\r\n\r\n    if (!IsLatestId(id))) co_return;\r\n\r\n    SetAutocomplete(matches);\r\n}\r\n<\/pre>\n","protected":false},"excerpt":{"rendered":"<p>Going free-threaded.<\/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-109702","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-oldnewthing","tag-code"],"acf":[],"blog_post_summary":"<p>Going free-threaded.<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/posts\/109702","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=109702"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/posts\/109702\/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=109702"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/categories?post=109702"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/tags?post=109702"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}