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.
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…Not something relevant to the original problem, but my first reaction after reading the code was that the combination of the and the can probably be replaced with a single (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 that doesn't exist. That even though are in the standard library, and did...