Adding state to the update notification pattern, part 3

Raymond Chen

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

Leave a comment

  • 紅樓鍮 0

    Not something relevant to the original problem, but my first reaction after reading the code was that the combination of the mutex and the option<string> can probably be replaced with a single atomic<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 that atomic<unique_ptr<T>> doesn’t exist. That even though atomic<{shared,weak}_ptr<T>> are in the standard library, and atomic_unique_ptr did appear in Sutter’s original paper, N4058. Somehow P0718 (which I presume is what brought atomic<{shared,weak}_ptr<T>> into C++20) forgot about atomic<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 the std::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 full atomic API: load can’t be implemented for the move-only unique_ptr, and compare_exchange_* mostly don’t make sense either. But other methods on it would still be valuable in my opinion, especially exchange.

  • Neil Rashbrook 0

    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…

Feedback usabilla icon