Adding state to the update notification pattern, part 3
Last time, we developed a stateful but coalescing update notification, and we noted that the code does a lot of unnecessary work because the worker thread calculates all the matches, even if the work has been superseded by another request.
We can add an optimization to abandon the background work if it notices that its efforts are going to waste: Periodically check whether there is any pending text. This will cost us a mutex, however, to protect access to m_pendingText
from multiple threads.
class EditControl { ⟦ ... existing class members ... ⟧ bool m_busy = false; std::mutex m_mutex; std::optional<string> m_pendingText; }; winrt::fire_and_forget EditControl::TextChanged(std::string text) { auto lifetime = get_strong(); ExchangePendingText(std::move(text)); if (std::exchange(m_busy, true)) { co_return; } while (auto pendingText = ExchangePendingText(std::nullopt); pendingText) { co_await winrt::resume_background(); auto matches = BuildMatches(*pendingText); co_await winrt::resume_foreground(Dispatcher()); if (matches) { SetAutocomplete(*matches); } } m_busy = false; } template<typename T = std::optional<std::string>> std::optional<std::string> EditControl::ExchangePendingText(T&& pending) { auto lock = std::unique_lock(m_mutex); return std::exchange(m_pendingText, std::forward<T>(pending)); } std::optional<std::vector<std::string>> EditControl::BuildMatches(std::string const& text) { std::vector<std::string> matches; for (auto&& candidate : FindCandidates(text)) { if (candidate.Verify()) { matches.push_back(candidate.Text()); } if (auto lock = std::unique_lock(m_mutex); m_pendingText) { return std::nullopt; } } return matches; }
Last time, I noted that the UI thread is doing a lot of work for us, since it is implicitly ensuring that the updates to m_busy
and m_pendingText
are atomic.
If our background work doesn’t dispatch back to the UI thread, then we will be responsible for our own locking. We’ll look at that next time.
2 comments
Not something relevant to the original problem, but my first reaction after reading the code was that the combination of the
mutex
and theoption<string>
can probably be replaced with a singleatomic<unique_ptr<string>>
(provided you tolerate the use of raw atomics), which would likely be a lock-free atomic on any realistic platform. Then I came to the dreadful realization thatatomic<unique_ptr<T>>
doesn’t exist. That even thoughatomic<{shared,weak}_ptr<T>>
are in the standard library, andatomic_unique_ptr
did appear in Sutter’s original paper, N4058. Somehow P0718 (which I presume is what broughtatomic<{shared,weak}_ptr<T>>
into C++20) forgot aboutatomic<unique_ptr<T>>
completely.(I know it is possible to use the raw
atomic<string *>
for this purpose, provided you also tolerate the use of raw pointers that need to be manually freed.)Bonus chatter: Even without going for a lock-free atomics-based solution, I wouldn’t use
std::mutex
if I can use dr-m/atomic_sync, because last time I checked thestd::mutex
is 40 bytes large on x86-64 Linux and 80 bytes large on x64 Windows, and since no one will break the ABI, it looks like it’ll stay that way for quite some time.Edit: After a bit of thinking I suspect the reason for not including
atomic<unique_ptr<T>>
is due to it not being able to support the fullatomic
API:load
can’t be implemented for the move-onlyunique_ptr
, andcompare_exchange_*
mostly don’t make sense either. But other methods on it would still be valuable in my opinion, especiallyexchange
.I’m not convinced you need a whole mutex just to protect the “has value” state, just an atomic boolean (either with the default or custom memory barriers; sadly you can’t apply memory barriers to the “has value” part of a
std::optional
). Although maybe part 4 will use the mutex for something else…