惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

aimingoo的专栏
aimingoo的专栏
WordPress大学
WordPress大学
阮一峰的网络日志
阮一峰的网络日志
博客园 - 司徒正美
月光博客
月光博客
宝玉的分享
宝玉的分享
Recent Announcements
Recent Announcements
小众软件
小众软件
H
Hackread – Cybersecurity News, Data Breaches, AI and More
美团技术团队
博客园 - 三生石上(FineUI控件)
A
About on SuperTechFans
J
Java Code Geeks
云风的 BLOG
云风的 BLOG
罗磊的独立博客
大猫的无限游戏
大猫的无限游戏
IT之家
IT之家
Vercel News
Vercel News
量子位
Martin Fowler
Martin Fowler
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
V
Visual Studio Blog
腾讯CDC
有赞技术团队
有赞技术团队

The Old New Thing

Why didn't Read­Directory­ChangesW provide a way to correlate the two sides of a rename operation? - The Old New Thing How can I remove the Close button from my window caption? - The Old New Thing Why is the x86 undefined instruction called ud2? Why 2? - The Old New Thing What algorithm did Windows XP use to choose your initial user picture? - The Old New Thing A sample use of the winstart.bat file in Windows 95 - The Old New Thing Why don't we allow stacks to be sparse, instead of forcing them to be contiguous? - The Old New Thing What happens if you change a window class's GCL_CB­WND­EXTRA? - The Old New Thing The case of the progress callback that never got called when progress happened - The Old New Thing The perils of binding to value types in XAML - The Old New Thing Microspeak: Funded / unfunded - The Old New Thing AWE does not require PAE, though PAE makes it much more useful - The Old New Thing On forcing all derived classes to implement a specific non-virtual method, part 2 - The Old New Thing On forcing all derived classes to implement a specific non-virtual method, part 1 - The Old New Thing In the product end game, every change carries significant risk, episode 2 - The Old New Thing Why didn't the Windows Entertainment Pack just run the MS-DOS version inside an emulator? - The Old New Thing Comparing the two holograms on the Windows 95 box - The Old New Thing Reducing C++ template bloat by factoring out the type-dependent portions of the function, practical exam - The Old New Thing Reducing C++ template bloat by factoring out the type-dependent portions of the function - The Old New Thing On wrapping a callable in a lambda that just calls it with the same parameters - The Old New Thing Why did the Microsoft Entertainment Pack for Windows have a special sticker announcing that it also had Tetris? - The Old New Thing How do functions like alloca allocate memory from the stack? - The Old New Thing How do functions like alloca allocate memory from the stack? - The Old New Thing Forcing an ARM64X executable to run as a specific architecture - The Old New Thing A little helper class for managing LPPROC_THREAD_ATTRIBUTE_LISTs - The Old New Thing The comments that go into code versus those that go into the pull request description - The Old New Thing The little-known winstart.bat batch file - The Old New Thing How can I perform a Copy­File&shy in unbuffered mode? - The Old New Thing Creating a fake agile wrapper that is technically agile but is not useful outside its home apartment, part 5 - The Old New Thing Creating a fake agile wrapper that is technically agile but is not useful outside its home apartment, part 4 - The Old New Thing Creating a fake agile wrapper that is technically agile but is not useful outside its home apartment, part 3 - The Old New Thing
Sharing the result of a single Windows Runtime IAsyncOper...
Raymond Chen · 2026-05-27 · via The Old New Thing

Suppose you have a coroutine method called GetThingAsync(), and you want to do the work of getting “something” only once and caching the result. Here’s the version that does no caching:

struct Widget : WidgetT<Widget>
{
    IAsyncOperation<Thing> GetThingAsync()
    {
        co_return co_await GetThingWorkerAsync();
    }

    // The business logic goes here
    IAsyncOperation<Result> GetThingWorkerAsync();
};

Now, if this code were written in C#, we could take advantage of the fact that C# projects the Windows Runtime IAsyncOperation as a Task, and Task objects support being awaited on multiple times.

async Task<Thing> GetThingAsync()
{
    lock (m_lock) {
        // First person to call GetThingAsync starts the task.
        if (m_task == null) {
            m_task = GetThingWorker();
        }
    }
    return await m_getThingTask;
}

But we don’t have that in C++/WinRT.

One customer tried to build a solution out of winrt::resume_on_signal, perhaps inspired by one of my earlier explorations of this topic.

// Don't use this code. See discussion.
struct Widget : WidgetT<Widget>
{
    winrt::Thing m_thing{ nullptr };
    winrt::IAsyncOperation<winrt::Thing> m_task{ nullptr };
    wil::unique_event m_finished{ wil::EventOptions::ManualReset }; /* initially unsignaled */
    bool m_busy{ false };
    std::mutex m_mutex;

    IAsyncOperation<winrt::Thing> GetThingAsync()
    {
        bool shouldStart;
        {
            std::lock_guard guard(m_mutex);
            if (m_thing != nullptr) {
                // Operation has finished.
                co_return m_thing;
            } else if (m_busy) {
                // Operation has started but not yet finished.
                shouldStart = false;
            } else {
                // Operation hasn't even started.
                m_busy = true;
                shouldStart = true;
            }
        }

        auto lifetime = get_strong();

        if (shouldStart) {
            auto task = GetThingWorker();
            m_finished.ResetEvent();
            task.Completed([weak = get_weak(), this](auto&&, auto&&) {
                if (auto strong = weak.get()) {
                    m_busy = false;
                    m_finished.SetEvent();
                }
            });
            m_task = std::move(task);
        }

        co_await winrt::resume_on_signal(m_finished.get());

        {
            std::lock_guard guard(m_mutex);
            if (m_thing == nullptr && m_task) {
                m_thing = m_task.GetResults();
            }
        }

        co_return m_thing;
    }

};

The idea is that the first time GetThingAsync() is called, we start the real task and then arrange to clear the busy flag and set the m_finished event when the task completes. Subsequent callers will see that the task has already started and will not start it again. And subsequent calls which occur after the task has completed will see that we already have a m_thing and return it immediately.

Everybody then waits for the m_finished event, and then whoever manages to enter the mutex first gets the result and saves it. Finally, everybody returns whatever is in m_thing, which should be the result of the task.

From the observation that they set m_busy back to false when the task completes, and they reset the m_finished event each time they start the task, I conclude that their intention was to allow multiple attempts to get the “something” if a previous attempt fails.

Okay, so let’s see what could go wrong.

For one thing, we see a data race because the completion lambda modifies m_busy outside the mutex. So we should at least protect that with a mutex.

Another problem is that this code is not exception-safe. If GetThingWorkerAsync throws an exception before returning an IAsyncOperation, then the m_busy flag is set and gets stuck there. This means that nobody else will try to start the task, and the m_task remains null, so all subsequent callers just fall through and return a null Thing, which may not be something that the callers are expecting. (I mean, this code certainly doesn’t handle the case where GetThingWorkerAsync produces a null result because it thinks that a null m_thing means that we should try again instead of “I successfully got nothing.”)

There’s also a race condition if the task completes just as somebody calls GetThingAsync:

Thread 1 Thread 2
GetThingAsync called
m_busy = true
shouldStart = true
GetThingWorkerAsync()
m_finished.ResetEvent()
task.Completed(...)
m_task = std::move(task)
co_await resume_on_signal
 
(task completes)
m_busy = false
 
  GetThing called
m_busy = true
shouldStart = true
GetThingWorkerAsync
m_finished.ResetEvent()
task.Completed(...)
m_task = std::move(task)
co_await resume_on_signal
m_finished.SetEvent()
(completion handler returns)
 
m_thing = m_task.GetResults()  

Notice that the second call to GetThingAsync happens after m_busy has been reset, but before we have signaled the event. This creates a window inside which another thread calls GetThingAsync and tries to start the task again. The second calls assignment to m_task overwrites the one from the first call, and then when the first caller tries to get the results, it gets them from the wrong task.

But really, this code is trying too hard. We’ll look at a simpler version next time.

Category

Topics

Author

Raymond Chen

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.