April 19th, 2024

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.

Topics
Code

Author

Raymond has been involved in the evolution of Windows for more than 30 years. In 2003, he began a Web site known as The Old New Thing which has grown in popularity far beyond his wildest imagination, a development which still gives him the heebie-jeebies. The Web site spawned a book, coincidentally also titled The Old New Thing (Addison Wesley 2007). He occasionally appears on the Windows Dev Docs Twitter account to tell stories which convey no useful information.

2 comments

Discussion is closed. Login to edit/delete existing comments.

Newest
Newest
Popular
Oldest
  • Neil Rashbrook

    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…

  • 紅樓鍮 · Edited

    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...

    Read more

Feedback