进度回调在进度发生时从未被调用的案例

The Old New Thing (Raymond Chen) 工具

摘要

本文描述了一个调试场景,其中C#/C++/WinRT应用程序中的进度回调由于中间方法未报告进度而未被触发,并提出了一个绕过中间人的解决方案。

<p>一位同事试图弄清楚为什么他们的进度处理程序没有被调用。</p> <pre>// C# async Task&lt;bool&gt; DownloadItemAsync(string id) { var op = item.DownloadAsync(id); op.Progress += (s, pct) UpdateProgress(pct); var result = await op; ClearProgress(); return result; } </pre> <p>这是相当标准的操作。启动操作,挂接进度,然后等待操作完成。但他们从未收到任何进度。</p> <p>我让他们检查是否项目下载得太快,以至于他们错过了所有进度。但没有,即使下载花费很长时间,他们也从未收到任何进度。</p> <p>我建议他们单步执行<code>Download­Async</code>方法,查看它在哪里引发进度,然后跟踪执行到应该调用进度回调的点,看看为什么没有调用。(公平地说,这是一个跨语言调试问题,所以比看起来更难。我建议只关注C++端:等待COM可调用包装器生成并设置为进度回调,然后在该包装器上设置断点。如果该断点被命中,但C#代码未运行,则映射中存在问题。如果断点从未被命中,则问题在C++端。)</p> <p>我的同事带回了答案。以下是<code>Download­Async</code>的代码:</p> <pre>// C++/WinRT winrt::IAsyncOperationWithProgress&lt;bool, double&gt; AggregateSource::DownloadAsync(winrt::hstring id) { std::wstring_view idview { id }; auto pos = idview.find(L':'); if (pos == std::wstring_view::npos) { co_return false; } auto providerId = Unescape(idview.substr(0, pos - 1)); auto provider = GetProvider(providerId); if (!provider) { co_return false; } auto providerItemId = Unescape(idview.substr(pos + 1)); co_return co_await provider.DownloadAsync(providerItemId); } </pre> <p><code>Aggregate­Source</code>从多个提供程序收集项目。<code>id</code>的格式是提供程序、冒号,然后是ID。(提供程序ID和项目ID经过转义,以防它们本身包含冒号。)</p> <p>我们查找提供程序,然后要求提供程序下载项目。</p> <p>你看到问题了吗?</p> <p><code>Download­Async</code>没有生成任何进度报告!</p> <p>它从未调用<code>co_await winrt::get_progress_token()</code>,更不用说使用进度值调用令牌来生成进度报告了。</p> <p>很明显,当代码附加进度回调时,它想做的是接收来自<i>内部</i>操作的回调,即来自提供程序的操作。然而,它唯一能访问的<code>IAsync­Operation­With­Progress</code>是由<code>Aggregate­Source::<wbr />Download­Async</code>方法返回的那个。</p> <p>这里的简单解决方案是摆脱中间人,直接返回提供程序的<code>IAsync­Operation­With­Progress</code>。这样,调用者可以连接到基础操作的进度。</p> <pre>winrt::IAsyncOperationWithProgress&lt;bool, double&gt; AggregateSource::DownloadAsync(winrt::hstring id) { std::wstring_view idview { id }; auto pos = idview.find(L':'); if (pos == std::wstring_view::npos) { <span style="border: solid 1px currentcolor;">return <a title="在C++/WinRT中创建已完成的异步活动,第8部分" href="https://devblogs.microsoft.com/oldnewthing/20240718-00/?p=109977">completed_async</a>(false);</span> } auto providerId = Unescape(idview.substr(0, pos - 1)); auto provider = GetProvider(providerId); if (!provider) { <span style="border: solid 1px currentcolor;">return completed_async(false);</span> } auto providerItemId = Unescape(idview.substr(pos + 1)); <span style="border: solid 1px currentcolor;">return provider.DownloadAsync(providerItemId);</span> } </pre> <p>如果你不相信<code>completed_<wbr />async</code>,你可以直接写</p> <pre> return [] -&gt; winrt::IAsyncOperationWithProgress&lt;bool, double&gt; { return false; }(); </pre> <p>我说这是简单的解决方案。还有一个困难的解决方案,我们稍后必须查看,因为我还没有写出来。</p> <p>这篇文章<a href="https://devblogs.microsoft.com/oldnewthing/20260903-00/?p=112672">当进度发生时从未被调用的进度回调案例</a>最初出现在<a href="https://devblogs.microsoft.com/oldnewthing">The Old New Thing</a>上。</p>
查看原文
查看缓存全文

缓存时间: 2026/09/04 11:45

# 进度回调在发生进度更新时从未被调用的案例 - The Old New Thing 来源:https://devblogs.microsoft.com/oldnewthing/20260903-00?p=112672 一位同事试图弄清楚为什么他们的进度处理器没有被调用。 ```csharp // C# async Task DownloadItemAsync(string id) { var op = item.DownloadAsync(id); op.Progress += (s, pct) => UpdateProgress(pct); var result = await op; ClearProgress(); return result; } ``` 这是非常标准的做法。启动操作,挂接进度回调,然后等待操作完成。但他们从未收到任何进度更新。我建议他们检查是否因为下载速度太快而错过了所有进度回调。但即便如此,即使下载耗时很长,他们也从未收到任何进度回调。我建议他们单步调试 `DownloadAsync` 方法,观察它在哪里触发进度更新,然后跟踪执行流程到应该调用进度回调的位置,看看为什么回调没有被执行。 (公平地说,这是一个跨语言调试问题,比看起来更复杂。我建议先专注于 C++ 端:等待生成 COM 可调用包装器并将其设置为进度回调,然后在该包装器上设置断点。如果断点被命中但 C# 代码没有执行,那么问题出在投影层;如果断点从未被命中,那么问题在 C++ 端。) 我的同事带来了答案。以下是 `DownloadAsync` 的代码: ```cpp // C++/WinRT winrt::IAsyncOperationWithProgress AggregateSource::DownloadAsync(winrt::hstring id) { std::wstring_view idview { id }; auto pos = idview.find(L':'); if (pos == std::wstring_view::npos) { co_return false; } auto providerId = Unescape(idview.substr(0, pos - 1)); auto provider = GetProvider(providerId); if (!provider) { co_return false; } auto providerItemId = Unescape(idview.substr(pos + 1)); co_return co_await provider.DownloadAsync(providerItemId); } ``` `AggregateSource` 负责从多个提供者处收集项目。`id` 的格式为“提供者ID:项目ID”。(提供者ID和项目ID都经过转义处理,以防它们自身包含冒号。)我们查找提供者,然后请求提供者下载项目。 发现问题了吗?`DownloadAsync` 并未生成任何进度报告!它从未调用 `co_await winrt::get_progress_token()`,更不用说通过令牌传递进度值来生成进度报告了。 显然,当代码挂接进度回调时,它的意图是接收来自*内部*操作(即提供者返回的操作)的回调。但它唯一可访问的 `IAsyncOperationWithProgress` 是由 `AggregateSource::DownloadAsync` 方法返回的那个。 简单的解决方案是去掉中间层,直接返回提供者的 `IAsyncOperationWithProgress`。这样调用者就可以连接到实际操作的进度。 ```cpp winrt::IAsyncOperationWithProgress AggregateSource::DownloadAsync(winrt::hstring id) { std::wstring_view idview { id }; auto pos = idview.find(L':'); if (pos == std::wstring_view::npos) { return completed_async(false); } auto providerId = Unescape(idview.substr(0, pos - 1)); auto provider = GetProvider(providerId); if (!provider) { return completed_async(false); } auto providerItemId = Unescape(idview.substr(pos + 1)); return provider.DownloadAsync(providerItemId); } ``` 如果你不使用 `completed_async`,也可以这样写: ```cpp return [] -> winrt::IAsyncOperationWithProgress { return false; }(); ``` 我说这是简单的解决方案。还有一个复杂的解决方案,我们之后会探讨,因为我还没写完。 ### 分类 ### 主题 ## 作者 Raymond Chen Raymond 参与 Windows 的演进已经超过 30 年。2003 年,他创建了一个名为 The Old New Thing 的网站,其受欢迎程度远超他的想象——这种发展至今仍让他感到不安。该网站后来催生了一本书,书名也巧合地叫做 The Old New Thing(Addison Wesley 2007)。他偶尔会在 Windows Dev Docs Twitter 账号上分享一些不传达任何有用信息的故事。

相似文章

理解在试图绕过规则时规则背后的原理

The Old New Thing (Raymond Chen)

本文来自微软的《老东西》博客,解释了Windows内核回调函数最佳实践背后的原理,特别是为什么阻塞或等待工作项会违背其目的,并通过一个关于驱动程序导致系统挂起的警示故事来说明。

Windows Runtime 活动的取消是异步的

The Old New Thing (Raymond Chen)

本文通过代码示例解释了为什么 Windows Runtime 异步活动的取消是异步的,以及它如何避免死锁,尤其是在进度回调触发取消时。

用户更改键盘布局时程序挂起的问题

The Old New Thing (Raymond Chen)

一个调试故事,讲述了当用户更改键盘布局(例如使用 Win+Space 快捷键)时,Windows 程序挂起的原因,是由于一个后台线程创建了窗口但没有泵送消息。修复方法是要么泵送消息,要么销毁窗口。