C# 在使用异步/等待和多线程时保持响应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;

我正在做一个密集的计算,其中包括并行运行的代码。在并行方法中,我们等待对异步方法的调用。因为Parallel.For不能做到这一点,我们有一些基于通道的代码

问题是它似乎阻塞了UI线程,尽管我们正在设置处理程序来避免这种情况。如果我在worker中使用Task.Delay1,它似乎有效,但这只是治标不治本

如何防止UI线程被阻塞

以下是视图模型的代码:

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可以从缓存中完成。