进度回调在进度发生时从未被调用的案例
摘要
本文描述了一个调试场景,其中C#/C++/WinRT应用程序中的进度回调由于中间方法未报告进度而未被触发,并提出了一个绕过中间人的解决方案。
<p>一位同事试图弄清楚为什么他们的进度处理程序没有被调用。</p>
<pre>// C#
async Task<bool> 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>DownloadAsync</code>方法,查看它在哪里引发进度,然后跟踪执行到应该调用进度回调的点,看看为什么没有调用。(公平地说,这是一个跨语言调试问题,所以比看起来更难。我建议只关注C++端:等待COM可调用包装器生成并设置为进度回调,然后在该包装器上设置断点。如果该断点被命中,但C#代码未运行,则映射中存在问题。如果断点从未被命中,则问题在C++端。)</p>
<p>我的同事带回了答案。以下是<code>DownloadAsync</code>的代码:</p>
<pre>// C++/WinRT
winrt::IAsyncOperationWithProgress<bool, double>
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>AggregateSource</code>从多个提供程序收集项目。<code>id</code>的格式是提供程序、冒号,然后是ID。(提供程序ID和项目ID经过转义,以防它们本身包含冒号。)</p>
<p>我们查找提供程序,然后要求提供程序下载项目。</p>
<p>你看到问题了吗?</p>
<p><code>DownloadAsync</code>没有生成任何进度报告!</p>
<p>它从未调用<code>co_await winrt::get_progress_token()</code>,更不用说使用进度值调用令牌来生成进度报告了。</p>
<p>很明显,当代码附加进度回调时,它想做的是接收来自<i>内部</i>操作的回调,即来自提供程序的操作。然而,它唯一能访问的<code>IAsyncOperationWithProgress</code>是由<code>AggregateSource::<wbr />DownloadAsync</code>方法返回的那个。</p>
<p>这里的简单解决方案是摆脱中间人,直接返回提供程序的<code>IAsyncOperationWithProgress</code>。这样,调用者可以连接到基础操作的进度。</p>
<pre>winrt::IAsyncOperationWithProgress<bool, double>
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 [] -> winrt::IAsyncOperationWithProgress<bool, double> {
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 账号上分享一些不传达任何有用信息的故事。
相似文章
理解在试图绕过规则时规则背后的原理
本文来自微软的《老东西》博客,解释了Windows内核回调函数最佳实践背后的原理,特别是为什么阻塞或等待工作项会违背其目的,并通过一个关于驱动程序导致系统挂起的警示故事来说明。
Windows Runtime 活动的取消是异步的
本文通过代码示例解释了为什么 Windows Runtime 异步活动的取消是异步的,以及它如何避免死锁,尤其是在进度回调触发取消时。
在C++/WinRT中创建Windows Runtime委托的敏捷版本,第8部分
本文讨论了在C++/WinRT中创建敏捷委托时修复异常安全性问题,解决了未指定lambda捕获构造顺序导致的引用泄漏。
关于线程从已卸载的第三方DLL中执行的问题
微软Raymond Chen的一则技术调试故事,讲述了Windows资源管理器崩溃由从已卸载第三方DLL中执行的线程引起,展示了开发者如何调查此类问题。
用户更改键盘布局时程序挂起的问题
一个调试故事,讲述了当用户更改键盘布局(例如使用 Win+Space 快捷键)时,Windows 程序挂起的原因,是由于一个后台线程创建了窗口但没有泵送消息。修复方法是要么泵送消息,要么销毁窗口。