跳转到内容

UWP:多线程问题及其解决

本文将讲述UWP应用中多线程的具体问题,将结合具体界面阻塞/后台线程访问UI线程问题同时给出具体解决方案。

可能最开始接触UWP的异步,是在使用C#的某些函数时候看到提示让我们添加await,然后又会提示我们修改函数为添加修饰async,从字面意思来讲,如果一个函数需要较长的执行时间,为了不阻塞应用的继续执行,一般是还是要提供UI的反馈的,那么我们就不能让程序卡在这里,这时候如果这个函数是async型的,程序就会另起一个线程来执行这个函数,如果这个函数需要返回数据,那么就用await来等待返回的值。

可能有同学不太能理解什么是UI的阻塞,举例来说。

我们随意给界面上加个东西

<!-- Grid 是页面布局容器,Background 绑定系统主题色 -->
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<!-- TextBlock 用于显示文本,x:Name 给控件命名方便在 C# 代码中引用 -->
<TextBlock x:Name="output" Text="计算中" Style="{StaticResource HeaderTextBlockStyle}"></TextBlock>
</Grid>

然后把这个虽然没什么意义但是很耗时间的东西放在页面的初始化里

// 同步计算:直接在 UI 线程上执行,循环次数极大(约 21 亿次)
int ans = 0;
for (var i = 0; i < Int32.MaxValue; i++)
{
ans++; // 累加操作本身很快,但循环次数太多导致 UI 长时间卡死
}
// 只有在整个循环结束后,UI 才会更新,期间用户无法进行任何操作
output.Text = ans.ToString();

这样页面是需要很长时间才能加载完成的,我们是看不到“计算中”这样的输出的,这个例子还好,计算的时间可能会停留在加载界面,但是如果是界面上还有其他按钮,此时UI线程阻塞了,我们对其他按钮的事件的触发就会没有反馈。这自然不是我们想要的结果。

那上面都说了有异步,那我把这个计算的环节写成异步函数不就好了吗?

// async void 表示异步方法,调用后立即返回,不阻塞调用方
async void CalAns()
{
// ⚠ 注意:async 方法在没有 await 时默认在调用线程上执行,不会自动开新线程
int ans = 0;
// 以下循环仍然运行在 UI 线程上,所以界面会继续卡死
for (var i = 0; i < Int32.MaxValue; i++)
{
ans++;
}
// 就算这里能成功更新 UI,前面的循环也已经把界面卡住了
output.Text = ans.ToString();
}

道理是这个道理,这样我们是能看到“计算中”的,但是很快计算结果出来的时候就会出错,报错的提示大概是使用了另一个线程的接口。

第一次遇见这个现象的时候我是很迷茫,现在看来也就是后台线程是不能直接访问UI线程的,进程之间的通讯是需要使用给定的接口的,如果我们非要访问,那可以使用Dispatcher,比如

async void CalAns()
{
// 计算仍然在 UI 线程上执行,所以循环阶段界面依然卡顿
int ans = 0;
for (var i = 0; i < Int32.MaxValue; i++)
{
ans++;
}
// Dispatcher.RunAsync 将 lambda 投递到 UI 线程的消息队列中执行
// 这里是安全的,因为 Dispatcher 确保了代码在 UI 线程上运行
// CoreDispatcherPriority.Normal 指定任务的优先级,Normal 为普通优先级
await this.Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () => {
// 此时已经在 UI 线程上下文中,可以安全地更新界面控件
output.Text = ans.ToString();
});
}

嗯如果你没有上面的this怎么办?嗯这是个问题,可以考虑开个静态属性存下page,或者使用MVVM Light,MVVM Light也有给的解决方法。

那你可以这么写

async void CalAnsAsync()
{
// 定义一个返回 int 的委托,封装耗时的计算逻辑
// Func<int> 是无参数、返回 int 的委托类型
Func<int> CalAnsFunc = () =>
{
int ans = 0;
// 这段循环将在后台线程池线程上执行,不会卡住 UI
for (var i = 0; i < Int32.MaxValue; i++)
{
ans++;
}
return ans; // 将计算结果返回
};
// Task.Run 将委托投递到线程池中执行,返回 Task<int>
// await 等待后台任务完成后取出结果,同时自动切换回调用线程(UI 线程)
// 因此这里可以直接更新 output.Text,无需 Dispatcher
output.Text = (await Task.Run(CalAnsFunc)).ToString();
}

用Task.Run()开新线程跑一个本来不是异步的函数,然后等待跑完把值返回来更新值。

上面的大概只能起参考左右,如果有兴趣可以看下面的这些博文。