{"id":111043,"date":"2025-04-03T07:00:00","date_gmt":"2025-04-03T14:00:00","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/oldnewthing\/?p=111043"},"modified":"2025-04-03T09:57:11","modified_gmt":"2025-04-03T16:57:11","slug":"20250403-00","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/oldnewthing\/20250403-00\/?p=111043","title":{"rendered":"Adding delays to our task sequencer, part 2"},"content":{"rendered":"<p>Last time, <a title=\"Adding delays to our task sequencer, part 1\" href=\"https:\/\/devblogs.microsoft.com\/oldnewthing\/20250402-00\/?p=111040\"> we added task throttling to our task sequencer<\/a> by adding a 1-second wait between tasks. But maybe you want the throttling to control how frequently tasks can be issued, rather than enforcing a cooling-off period between tasks. In that case, you can calculate how much you need to wait.<\/p>\n<p>For illustration purposes, say that we want to limit you to one task per second. The idea is to remember the earliest time we can start the next task, and if the previous task ends too soon, insert a delay before starting the next one.<\/p>\n<pre>struct task_sequencer\r\n{\r\n    \u27e6 ... \u27e7\r\n\r\n    struct completer\r\n    {\r\n        ~completer()\r\n        {\r\n            [](auto chain, <span style=\"border: solid 1px currentcolor;\">auto delay<\/span>) -&gt; winrt::fire_and_forget {\r\n                co_await winrt::resume_after(<span style=\"border: solid 1px currentcolor;\">delay<\/span>);\r\n                chain-&gt;complete();\r\n            }(std::move(chain),\r\n              <span style=\"border: solid 1px currentcolor; border-bottom: none;\">std::chrono::duration_cast&lt;                  <\/span>\r\n              <span style=\"border: 1px currentcolor; border-style: none solid;\">  winrt::Windows::Foundation::TimeSpan&gt;      <\/span>\r\n              <span style=\"border: solid 1px currentcolor; border-top: none;\">(earliest - std::chrono::steady_clock::now())<\/span>);\r\n        }\r\n        std::shared_ptr&lt;chained_task&gt; chain;\r\n        <span style=\"border: solid 1px currentcolor; border-bottom: none;\">std::chrono::steady_clock::time_point earliest =<\/span>\r\n        <span style=\"border: solid 1px currentcolor; border-top: none;\">    std::chrono::steady_clock::now();           <\/span>\r\n    };\r\n\r\n\r\npublic:\r\n    template&lt;typename Maker&gt;\r\n    auto QueueTaskAsync(Maker&amp;&amp; maker) -&gt;decltype(maker())\r\n    {\r\n        auto node = std::make_shared&lt;chained_task&gt;();\r\n\r\n        suspender suspend;\r\n\r\n        using Async = decltype(maker());\r\n        auto task = [&amp;]() -&gt; Async\r\n        {\r\n            completer completer{ current };\r\n            auto local_maker = std::forward&lt;Maker&gt;(maker);\r\n            auto context = winrt::apartment_context();\r\n\r\n            co_await suspend;\r\n            co_await context;\r\n            <span style=\"border: solid 1px currentcolor; border-bottom: none;\">completer.earliest =                      <\/span>\r\n            <span style=\"border: solid 1px currentcolor; border-top: none;\">    std::chrono::steady_clock::now() + 1s;<\/span>\r\n            co_return co_await local_maker();\r\n        }();\r\n\r\n        {\r\n            winrt::slim_lock_guard guard(m_mutex);\r\n            m_latest.swap(node);\r\n        }\r\n\r\n        node-&gt;continue_with(suspend.handle);\r\n\r\n        return task;\r\n    }\r\n\r\n    \u27e6 ... \u27e7\r\n};\r\n<\/pre>\n<p>The idea here is that the completer remembers the earliest the next task can start. Initially, the next task can start right away (in case something goes wrong before we even get around to starting the task), but once we commit to starting the task, we update the earliest time for the next task to the current time plus the desired minimum time between task starts (one second).<\/p>\n<p>When the completer destructs, we calculate how long we have to wait until the steady clock reaches <code>earliest<\/code>, and then ask <code>resume_<wbr \/>after<\/code> to wait that amount. The <code>resume_<wbr \/>after<\/code> function already handles delays that are zero or negative (by not delaying at all), so we don&#8217;t need to handle that special case ourselves.<\/p>\n<p>I use the steady clock instead of the wall clock to protect against clock changes caused by time synchronization or the user just going to the Time and Date control panel and manually changing the time. The steady clock always moves forward at an even rate, unaffected by any time adjustments the system or the user may impose.<\/p>\n<p>There is an implicit conversion between durations if the destination duration type has at least as much resolution as the source.\u00b9 If not, then you must use <code>duration_<wbr \/>cast<\/code> to indicate that you&#8217;re okay with rounding.\u00b2<\/p>\n<p>The standard does not specify the resolution of the steady clock, so we are forced to perform a <code>duration_<wbr \/>cast<\/code> to cover the case where the <code>TimeSpan<\/code> resolution is not at least as good as the steady clock resolution.<\/p>\n<p>But wait, we&#8217;re not done yet. We&#8217;ll look into the situation a bit more next time.<\/p>\n<p>\u00b9 Formally, either the period of the source is a positive integer multiple of the period of the destination (so that the conversion is an integer multiplication) or destination uses a floating point type.<\/p>\n<p>\u00b2 Though if the destination uses a floating point type, you&#8217;re getting rounding anyway.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Waiting the right amount of time.<\/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-111043","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-oldnewthing","tag-code"],"acf":[],"blog_post_summary":"<p>Waiting the right amount of time.<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/posts\/111043","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=111043"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/posts\/111043\/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=111043"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/categories?post=111043"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/tags?post=111043"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}