C# 在使用异步/等待和多线程时保持响应UI
我正在做一个密集的计算,其中包括并行运行的代码。在并行方法中,我们等待对异步方法的调用。因为Parallel.For不能做到这一点,我们有一些基于通道的代码 问题是它似乎阻塞了UI线程,尽管我们正在设置处理程序来避免这种情况。如果我在worker中使用Task.Delay1,它似乎有效,但这只是治标不治本 如何防止UI线程被阻塞 以下是视图模型的代码:C# 在使用异步/等待和多线程时保持响应UI,c#,wpf,multithreading,async-await,C#,Wpf,Multithreading,Async Await,我正在做一个密集的计算,其中包括并行运行的代码。在并行方法中,我们等待对异步方法的调用。因为Parallel.For不能做到这一点,我们有一些基于通道的代码 问题是它似乎阻塞了UI线程,尽管我们正在设置处理程序来避免这种情况。如果我在worker中使用Task.Delay1,它似乎有效,但这只是治标不治本 如何防止UI线程被阻塞 以下是视图模型的代码: using Prism.Commands; using Prism.Mvvm; using Extensions.ParallelAsync;
using Prism.Commands;
using Prism.Mvvm;
using Extensions.ParallelAsync;
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace MVVMAwaitUiThread
{
public class MainWindowViewModel : BindableBase
{
public MainWindowViewModel()
{
DoSomethingGoodCommand = new DelegateCommand(DoSomethingGood);
DoSomethingBadCommand = new DelegateCommand(DoSomethingBad);
}
private ProgressViewModel _progressViewModel;
public ProgressViewModel ProgressViewModel
{
get => _progressViewModel;
set => SetProperty(ref _progressViewModel, value);
}
private bool _isBusy = false;
public bool IsBusy
{
get => _isBusy;
set => SetProperty(ref _isBusy, value);
}
private string _workText = "";
public string WorkText
{
get => _workText;
set => SetProperty(ref _workText, value);
}
public DelegateCommand DoSomethingGoodCommand { get; private set; }
public async void DoSomethingGood()
{
IsBusy = true;
try
{
ProgressViewModel = new ProgressViewModel();
double sum = await ReallyDoSomething(1, ProgressViewModel.Progress, ProgressViewModel.CancellationToken);
WorkText = $"Did work {DateTime.Now} -> {sum}.";
}
catch (OperationCanceledException)
{
// do nothing
}
finally
{
IsBusy = false;
}
}
public DelegateCommand DoSomethingBadCommand { get; private set; }
public async void DoSomethingBad()
{
IsBusy = true;
try
{
ProgressViewModel = new ProgressViewModel();
double sum = await ReallyDoSomething(0, ProgressViewModel.Progress, ProgressViewModel.CancellationToken);
WorkText = $"Did work {DateTime.Now} -> {sum}.";
}
catch (OperationCanceledException)
{
// do nothing
}
finally
{
IsBusy = false;
}
}
/// <summary>
/// Calling this with 0 doesn't work, but 1 does
/// </summary>
private async Task<double> ReallyDoSomething(int delay, IProgress<double> progress, CancellationToken cancellationToken)
{
const double maxIterations = 250;
const int sampleCount = 10;
const int maxDegreeOfParallelism = -1; // this doesn't seem to have any effect
const double totalIterations = sampleCount * maxIterations;
int completedIterations = 0;
ConcurrentBag<double> bag = new ConcurrentBag<double>();
// In reality, I have calculations that make calls to async/await methods, but each iteration can be parallel
// Can't make async calls in parallel.for, so this is what we have come up with
await ParallelChannelsAsync.ForAsync(0, sampleCount, maxDegreeOfParallelism, cancellationToken, Eval).ConfigureAwait(false);
async Task Eval(int seed, CancellationToken cancellationToken)
{
double sum = seed;
for (int i = 0; i < maxIterations; ++i)
{
sum += i * (i + 1.0); // simulate computation
await Task.Delay(delay); // simulate an async call
Interlocked.Increment(ref completedIterations);
progress?.Report(completedIterations / totalIterations);
cancellationToken.ThrowIfCancellationRequested();
}
bag.Add(sum / maxIterations);
};
return bag.Sum();
}
}
}
这是一个非常简化的VS2019项目,它演示了问题:
不清楚您的代码实际上做了什么,但仅仅因为方法具有异步API并不一定意味着它实现为不阻塞 以下面的方法为例。从调用方的角度来看,它似乎是异步的,但显然不是:
public Task<int> MyNotSoAsyncMethod()
{
//I'm actually blocking...
Thread.Sleep(3000);
return Task.FromResult(0);
}
不清楚您的代码实际上做了什么,但仅仅因为一个方法有一个异步API并不一定意味着它实现为不阻塞 以下面的方法为例。从调用方的角度来看,它似乎是异步的,但显然不是:
public Task<int> MyNotSoAsyncMethod()
{
//I'm actually blocking...
Thread.Sleep(3000);
return Task.FromResult(0);
}
使用来自@JonasH和@mm8的信息以及大量调试,问题实际上是UI线程被INotifyPropertyChanged.PropertyChanged引发的事件耗尽,因为我们使用的是MVVM 我们用了两件东西来解决这个问题。在调用主算法的地方,我们确实需要使用Task.Run在单独的线程上获取它 但是我们也接到了很多关于IProgress.Report的电话,用户界面实际上做了比需要做的更多的工作。通常,如果在UI绘制多个报告之前,您得到了多个报告,那么您实际上只需要最后一个报告。因此,我们编写这段代码基本上是为了丢弃“排队”的报告调用
/// <summary>
/// Assuming a busy workload during progress reports, it's valuable to only keep the next most recent
/// progress value, rather than back pressuring the application with tons of out-dated progress values,
/// which can result in a locked up application.
/// </summary>
/// <typeparam name="T">Type of value to report.</typeparam>
public class ThrottledProgress<T> : IProgress<T>, IAsyncDisposable
{
private readonly IProgress<T> _progress;
private readonly Channel<T> _channel;
private Task _completion;
public ThrottledProgress(Action<T> handleReport)
{
_progress = new Progress<T>(handleReport);
_channel = Channel.CreateBounded<T>(new BoundedChannelOptions(1)
{
AllowSynchronousContinuations = false,
FullMode = BoundedChannelFullMode.DropOldest,
SingleReader = true,
SingleWriter = true
});
_completion = ConsumeAsync();
}
private async Task ConsumeAsync()
{
await foreach (T value in _channel.Reader.ReadAllAsync().ConfigureAwait(false))
{
_progress.Report(value);
}
}
void IProgress<T>.Report(T value)
{
_channel.Writer.TryWrite(value);
}
public async ValueTask DisposeAsync()
{
if (_completion is object)
{
_channel.Writer.TryComplete();
await _completion.ConfigureAwait(false);
_completion = null;
}
}
}
使用来自@JonasH和@mm8的信息以及大量调试,问题实际上是UI线程被INotifyPropertyChanged.PropertyChanged引发的事件耗尽,因为我们使用的是MVVM 我们用了两件东西来解决这个问题。在调用主算法的地方,我们确实需要使用Task.Run在单独的线程上获取它 但是我们也接到了很多关于IProgress.Report的电话,用户界面实际上做了比需要做的更多的工作。通常,如果在UI绘制多个报告之前,您得到了多个报告,那么您实际上只需要最后一个报告。因此,我们编写这段代码基本上是为了丢弃“排队”的报告调用
/// <summary>
/// Assuming a busy workload during progress reports, it's valuable to only keep the next most recent
/// progress value, rather than back pressuring the application with tons of out-dated progress values,
/// which can result in a locked up application.
/// </summary>
/// <typeparam name="T">Type of value to report.</typeparam>
public class ThrottledProgress<T> : IProgress<T>, IAsyncDisposable
{
private readonly IProgress<T> _progress;
private readonly Channel<T> _channel;
private Task _completion;
public ThrottledProgress(Action<T> handleReport)
{
_progress = new Progress<T>(handleReport);
_channel = Channel.CreateBounded<T>(new BoundedChannelOptions(1)
{
AllowSynchronousContinuations = false,
FullMode = BoundedChannelFullMode.DropOldest,
SingleReader = true,
SingleWriter = true
});
_completion = ConsumeAsync();
}
private async Task ConsumeAsync()
{
await foreach (T value in _channel.Reader.ReadAllAsync().ConfigureAwait(false))
{
_progress.Report(value);
}
}
void IProgress<T>.Report(T value)
{
_channel.Writer.TryWrite(value);
}
public async ValueTask DisposeAsync()
{
if (_completion is object)
{
_channel.Writer.TryComplete();
await _completion.ConfigureAwait(false);
_completion = null;
}
}
}
如果这是计算受限的,有什么理由不使用Task.Run吗?在实际算法中有I/O等待调用。是哪一行导致了问题?我假设异步任务Eval?没有特定的行。尽管我们尽可能地遵循了async/await准则,但UI线程似乎仍然被阻塞。如果您在UI线程上执行与计算绑定的任务,那么阻塞是不可避免的。解决方案是在后台线程上进行大量计算。您仍然可以从后台线程使用异步IO,但好处不大,因为您没有阻塞UI线程。在报告进度时,您还需要注意不要从后台线程更新UI。请注意,异步调用仍然可以同步完成,例如,如果IO可以从缓存中完成。如果这是计算限制,是否有理由不使用Task.Run?实际算法中有I/O等待调用。是哪一行导致了问题?我假设异步任务Eval?没有特定的行。尽管我们尽可能地遵循了async/await准则,但UI线程似乎仍然被阻塞。如果您在UI线程上执行与计算绑定的任务,那么阻塞是不可避免的。解决方案是在后台线程上进行大量计算。您仍然可以从后台线程使用异步IO,但好处不大,因为您没有阻塞UI线程。在报告进度时,您还需要注意不要从后台线程更新UI。请注意,异步调用仍然可以同步完成,例如,如果IO可以从缓存中完成。