从 PPL 任务继续更新 UWP UI - 警告 C4451



我有一个包含C++和Visual Studio 2017社区版的示例UWP应用,我正在努力了解PPL功能。

我生成了一个 UWP 应用程序,然后对 MainPage.xaml.cpp 文件进行了以下修改。这些更改的目的是模拟需要几秒钟的异步操作,并在操作完成各个阶段时更新显示的 UI。

这有效,并且 UI 已更新。

但是,我在编译时确实看到以下警告。

1> ... appuwp1mainpage.xaml.cpp(46): warning C4451: 'AppUwp1::MainPage::{ctor}::<lambda_df2e69e2b6fe4b1dfba3f26ad0398a3e>::myThread': Usage of ref class 'Windows::UI::Core::CoreWindow' inside this context can lead to invalid marshaling of object across contexts
1> ... appuwp1mainpage.xaml.cpp(46): note: Consider using 'Platform::Agile<Windows::UI::Core::CoreWindow>' instead
1> ... appuwp1mainpage.xaml.cpp(56): warning C4451: 'AppUwp1::MainPage::{ctor}::<lambda_c1468d2f6468239bd456bea931167a21>::myThread': Usage of ref class 'Windows::UI::Core::CoreWindow' inside this context can lead to invalid marshaling of object across contexts
1> ... appuwp1mainpage.xaml.cpp(56): note: Consider using 'Platform::Agile<Windows::UI::Core::CoreWindow>' instead

这些警告是什么意思?

我确实找到了线程和封送处理(C++/CX)的解释,其中提到了警告"使用非敏捷类时的编译器警告(C4451)",但是我不确定我是否有实际问题。

是否有其他更可接受的方法可以从任务延续更新 UI?

我正在使用DispatchedHandler()以便从任务延续中获得对 UI 线程的访问权限。如果我尝试使用myTextBlock->Text = "this is my text and some more text after sleep";而不将其包装在DispatchedHandler()中,则会出现异常。异常是可以理解的,因为then任务延续不再在 UI 线程中运行。

此堆栈溢出,警告 C4451:使用 ref 类 BackgroundTaskDeferral 可能导致无效封送处理,表明使用Platform:Agile确实解决了警告。

但是,没有解释警告的实际含义

初始任务创建除了启动处理异步操作的线程外,不执行任何其他操作。每个then继续子句都执行一个Sleep()来表示一些操作,这些操作需要时间,然后使用消息更新显示的 UI 屏幕。

MainPage::MainPage()
{
InitializeComponent();
myTextBlock->Text = "this is my text and some more text";
auto myThread = CoreWindow::GetForCurrentThread();
concurrency::create_task ([=]() {
// we are wanting to spin off a task that will be
// performed asynchronously and the real work is done in the
// following task continuations.
Sleep(5000);
}).then([=]()
{
Sleep(5000);
myThread->Dispatcher->RunAsync(
CoreDispatcherPriority::Normal,
ref new DispatchedHandler([=]()
{
// Do stuff on the UI Thread
myTextBlock->Text = "this is my text and some more text after sleep";
}));
}).then([=]()        // warning C4451 for this line
{
Sleep(5000);
myThread->Dispatcher->RunAsync(
CoreDispatcherPriority::Normal,
ref new DispatchedHandler([=]()
{
// Do stuff on the UI Thread
myTextBlock->Text = "this is my text and some more text after sleep after sleep again";
}));
});             // warning C4451 for this line
}

其他探索 #1

通过以下更改MainPage::MainPage()我看到UI窗口中显示的预期消息系列。在几秒钟内显示的是一系列文本字符串,包括一系列字符串,以第一个任务继续的循环中生成的iCount递增值开头。

似乎如果将for (int iCount = 0; iCount < 3; iCount++) {放置在new DispatchedHandler()lambda 中,则会导致 UI 线程阻塞几秒钟,并且 UI 变得无响应,然后显示第二个任务延续的文本字符串,并且 UI 再次变得响应。如果for位于外部,如本源代码示例中所示,则不会阻止 UI 线程,并且 UI 保持响应。

这是否意味着new DispatchedHandler()中包含的 lambda 将移交给 UI 线程运行?

MainPage::MainPage()
{
InitializeComponent();
myTextBlock->Text = "this is my text and some more text";
auto myThread = CoreWindow::GetForCurrentThread();
concurrency::create_task ([=]() {
Sleep(2000);
myThread->Dispatcher->RunAsync(
CoreDispatcherPriority::Normal,
ref new DispatchedHandler([=]()
{
myTextBlock->Text = "start of task";
// Do stuff on the UI Thread
}));
}).then([=]()
{
Sleep(5000);
for (int iCount = 0; iCount < 3; iCount++) {
myThread->Dispatcher->RunAsync(
CoreDispatcherPriority::Normal,
ref new DispatchedHandler([=]()
{
// Do stuff on the UI Thread
std::wstringstream ss;
ss << iCount << " text first";
myTextBlock->Text = ref new Platform::String(ss.str().c_str());
}   )   // close off the DispatchedHandler() lambda
);          // close off the RunAsync()
Sleep(2000);
}               // close off for loop
}).then([=]()
{
Sleep(5000);
myThread->Dispatcher->RunAsync(
CoreDispatcherPriority::Normal,
ref new DispatchedHandler([=]()
{
// Do stuff on the UI Thread
myTextBlock->Text = "this is my text and some more text after sleep after sleep again";
}));
});
}

其他说明

MVVM 和访问 Windows 应用商店应用中的 UI 线程

使用多个 UI 线程运行 WPF 应用程序

另请参阅其他堆栈溢出帖子:

  • 在 WinRT 中的 UI 线程上运行代码
  • 如何确定是否需要调度到 WinRT/Metro 中的 UI 线程?

MSDN 文章:并发运行时

任务并行性(并发运行时)提供了并发运行时和各种选项的概述。几个例子和大量指向其他材料的链接。

来自编译器的这些警告并不意味着你做错了什么,而是意味着你可能做错了什么。

1> ... appuwp1mainpage.xaml.cpp(46): warning C4451: 'AppUwp1::MainPage::{ctor}::<lambda_df2e69e2b6fe4b1dfba3f26ad0398a3e>::myThread': Usage of ref class 'Windows::UI::Core::CoreWindow' inside this context can lead to invalid marshaling of object across contexts
1> ... appuwp1mainpage.xaml.cpp(46): note: Consider using 'Platform::Agile<Windows::UI::Core::CoreWindow>' instead

CoreWindow 类,具有以下注释:

此类不是敏捷的,这意味着您需要考虑其 线程模型和封送处理行为。有关详细信息,请参阅线程处理 和编组 (C++/CX)。

在 C++ 中为 Windows 应用商店应用创建异步操作

Windows 运行时使用 COM 线程模型。在这个模型中, 对象托管在不同的单元中,具体取决于它们的方式 处理它们的同步。线程安全对象托管在 多线程单元 (MTA)。必须由 单线程托管在单线程单元 (STA) 中。

在具有 UI 的应用中,ASTA(应用程序 STA)线程为 负责泵送窗口消息,并且是 可以更新 STA 托管的 UI 控件的进程。这有两个 后果。首先,为了使应用保持响应,所有 CPU 密集型和 I/O 操作不应在 ASTA 线程上运行。 其次,必须封送来自后台线程的结果 返回到 ASTA 以更新 UI。在C++ Windows 8.x 应用商店应用中, 主页和其他 XAML 页面都在 ATSA 上运行。因此,任务 默认情况下,在 ASTA 上声明的延续在那里运行 因此,您可以直接在延续正文中更新控件。然而 如果将一个任务嵌套在另一个任务中,则嵌套的任何延续 任务在 MTA 中运行。因此,您需要考虑是否 显式指定这些延续在哪个上下文上运行。

是的,编写源代码的方法略有不同,以消除警告。

消除警告

如果我修改 MainPage::MainPage() 的源代码,以便不使用CoreWindow::GetForCurrentThread();在由concurrency::create_task()启动的工作线程中提供的Windows::UI::Core::CoreWindow ^,而是从 UI 线程本身获取Dispatcher,然后在工作线程中使用Dispatcher对象,我不再收到警告。这是因为Windows::UI::Core::CoreWindow不是敏捷的,所以CoreWindow对象来自的线程必须是一个考虑因素。但是,Dispatcher对象是敏捷。

编译器警告与从工作线程中通过非敏捷CoreWindow对象访问 UI 线程的Dispatcher有关,而对于此版本获取对 UI 线程调度程序的引用,然后使用该Dispatcher引用,编译器可以做到这一点。

此版本的源代码如下所示:

MainPage::MainPage()
{
InitializeComponent();
myTextBlock->Text = "this is my text and some more text";
auto myDispatcher = CoreWindow::GetForCurrentThread()->Dispatcher;
concurrency::create_task([=]() {
Sleep(2000);
myDispatcher->RunAsync(
CoreDispatcherPriority::Normal,
ref new DispatchedHandler([=]()
{
myTextBlock->Text = "start of task";
// Do stuff on the UI Thread
}));
}).then([=]()
{
Sleep(5000);
for (int iCount = 0; iCount < 3; iCount++) {
myDispatcher->RunAsync(
CoreDispatcherPriority::Normal,
ref new DispatchedHandler([=]()
{
// Do stuff on the UI Thread
std::wstringstream ss;
ss << iCount << " text first";
myTextBlock->Text = ref new Platform::String(ss.str().c_str());
})   // close off the DispatchedHandler() lambda
);          // close off the RunAsync()
Sleep(2000);
}               // close off for loop
}).then([=]()
{
Sleep(5000);
myDispatcher->RunAsync(
CoreDispatcherPriority::Normal,
ref new DispatchedHandler([=]()
{
// Do stuff on the UI Thread
myTextBlock->Text = "this is my text and some more text after sleep after sleep again";
}));
});
}

CoreDispatcher.RunAsync(CoreDispatcherPriority, DispatchedHandler) 方法的备注如下:

如果您在工作线程上,并且想要在 UI 上安排工作 线程,请使用CoreDispatcher::RunAsync.始终将优先级设置为CoreDispatcherPriority::NormalCoreDispatcherPriority::Low,以及 确保任何链式回调也使用CoreDispatcherPriority::NormalCoreDispatcherPriority::Low.

线程、异步和敏捷方法的背景

许多 .NET 功能和 Windows 运行时功能以及越来越多的常规用途功能都以 COM 控件和功能的形式提供。使用 COM 技术允许各种语言、平台和技术使用相同的功能。

然而,与COM技术一起是一个巨大的复杂性,幸运的是,可以通过用各种语言特定的包装器封装它来在很大程度上隐藏它。

COM技术的一个考虑因素是公寓的想法。MSDN 文章进程、线程和单元提供了对该主题的技术性介绍。

在 C++ 中为 Windows 应用商店应用创建异步操作

Windows 运行时使用 COM 线程模型。在这个模型中, 对象托管在不同的单元中,具体取决于它们的方式 处理它们的同步。线程安全对象托管在 多线程单元 (MTA)。必须由 单线程托管在单线程单元 (STA) 中。

在具有 UI 的应用中,ASTA(应用程序 STA)线程为 负责泵送窗口消息,并且是 可以更新 STA 托管的 UI 控件的进程。这有两个 后果。首先,为了使应用保持响应,所有 CPU 密集型和 I/O 操作不应在 ASTA 线程上运行。 其次,必须封送来自后台线程的结果 返回到 ASTA 以更新 UI。在C++ Windows 8.x 应用商店应用中, 主页和其他 XAML 页面都在 ATSA 上运行。因此,任务 默认情况下,在 ASTA 上声明的延续在那里运行 因此,您可以直接在延续正文中更新控件。然而 如果将一个任务嵌套在另一个任务中,则嵌套的任何延续 任务在 MTA 中运行。因此,您需要考虑是否 显式指定这些延续在哪个上下文上运行。

在 Windows 运行时中,引入了敏捷和非敏捷线程的概念。Microsoft Docs 文章线程和封送处理 (C++/CX) 为C++程序员提供了介绍。

在绝大多数情况下,Windows 运行时类的实例, 与标准C++对象一样,可以从任何线程访问。这样 类被称为"敏捷"。但是,少数窗口 Windows 附带的运行时类是非敏捷的,并且必须是 与标准 C++ 对象相比,使用 COM 对象更像 COM 对象。你没有 需要成为 COM 专家才能使用非敏捷类,但您确实需要 考虑类的线程模型及其封送处理 行为。本文为那些罕见的人提供了背景和指导 需要使用非敏捷实例的方案 .class。

参见

线程模型,用于讨论 WPF 线程模型。

从历史上看,Windows 仅允许 创建它们的线程。这意味着后台线程 某些长时间运行的任务的收费无法在以下情况下更新文本框 完成。Windows 这样做是为了确保 UI 组件的完整性。 如果列表框的内容由 绘画时的背景线。

来自开放组的 COM 线程模型的一个很好的解释

最新更新