首页 > 解决方案 > StreamWriter.WriteLineAsync() 不会在新上下文中继续

问题描述

在我的 WPF 应用程序中,我有一个问题,我通过 StreamWriter 对象多次写入文本文件,主要使用 WriteLineAsync() 方法。我相信我正在正确地等待所有任务。但是,UI 线程被阻止,而不是被允许处理 UI 更改。我写了一个小应用程序来演示这个问题。

public class MainWindowViewModel : INotifyPropertyChanged
{
    private string _theText;
    private string _theText2;

    public MainWindowViewModel()
    {
        TestCommand = new DelegateCommand(TestCommand_Execute, TestCommand_CanExecute);
        TestCommand2 = new DelegateCommand(TestCommand2_Execute, TestCommand2_CanExecute);
    }

    public ICommand TestCommand { get; }

    private bool TestCommand_CanExecute(object parameter)
    {
        return true;
    }

    private async void TestCommand_Execute(object parameter)
    {
        using (StreamWriter writer = new StreamWriter(new MemoryStream()))
        {
            TheText = "Started";
            await DoWork(writer).ConfigureAwait(true);
            TheText = "Complete";
        }
    }

    public ICommand TestCommand2 { get; }

    private bool TestCommand2_CanExecute(object parameter)
    {
        return true;
    }

    private async void TestCommand2_Execute(object parameter)
    {
        using (StreamWriter writer = new StreamWriter(new MemoryStream()))
        {
            TheText2 = "Started";
            await Task.Delay(1).ConfigureAwait(false);
            await DoWork(writer).ConfigureAwait(true);
            TheText2 = "Complete";
        }
    }

    public string TheText
    {
        get => _theText;
        set => SetValue(ref _theText, value);
    }

    public string TheText2
    {
        get => _theText2;
        set => SetValue(ref _theText2, value);
    }

    private bool SetValue<T>(ref T storage, T value, [CallerMemberName] string propertyName = null)
    {
        if (EqualityComparer<T>.Default.Equals(storage, value)) return false;
        storage = value;
        OnPropertyChanged(propertyName);
        return true;
    }

    public event PropertyChangedEventHandler PropertyChanged;

    [NotifyPropertyChangedInvocator]
    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    private async Task DoWork(StreamWriter writer)
    {
        for (int i = 0; i < 100; i++)
        {
            await writer.WriteLineAsync("test" + i).ConfigureAwait(false);
            Thread.Sleep(100);
        }
    }
}

和 XAML

    <Window x:Class="AsyncWpfToy.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:local="clr-namespace:AsyncWpfToy"
    mc:Ignorable="d"
    Title="MainWindow" Height="450" Width="800">
<Window.DataContext>
    <local:MainWindowViewModel />
</Window.DataContext>
<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="20" />
        <RowDefinition Height="20" />
        <RowDefinition Height="20" />
        <RowDefinition Height="20" />
    </Grid.RowDefinitions>
    <Button Grid.Row="0" Command="{Binding TestCommand}" Content="Button" />
    <TextBlock Grid.Row="1" Text="{Binding TheText}" />
    <Button Grid.Row="2" Command="{Binding TestCommand2}" Content="Button2" />
    <TextBlock Grid.Row="3" Text="{Binding TheText2}" />
</Grid>

而且,为了完整起见,DelegateCommand 的基本实现

public class DelegateCommand : ICommand
{
    private readonly Action<object> _execute;
    private readonly Func<object, bool> _canExecute;

    public DelegateCommand(Action<object> execute, Func<object, bool> canExecute = null)
    {
        _execute = execute;
        _canExecute = canExecute;
    }

    public bool CanExecute(object parameter)
    {
        return _canExecute?.Invoke(parameter) ?? true;
    }

    public void Execute(object parameter)
    {
        _execute(parameter);
    }

    public event EventHandler CanExecuteChanged
    {
        add => CommandManager.RequerySuggested += value;
        remove => CommandManager.RequerySuggested -= value;
    }
}

当我单击简单标记为“按钮”的按钮时,我的 UI 冻结,即使 DoWork() 的主体已等待所有 Async 方法并设置了 ConfigureAwait(false)。

但是,当我单击 Button2 时,我在等待 DoWork() 方法之前执行了await Task.Delay(1).ConfigureAwait(false) 。这似乎正确地将处理转移到另一个上下文,允许 UI 继续。事实上,如果我将await Task.Delay(1).ConfigureAwait(false)移动到 DoWork() 方法并在 for 循环之前设置它,一切都会按预期运行 - UI 保持响应。

无论出于何种原因,StreamWriter.WriteLineAsync() 似乎都不是真正的异步,或者处理发生得如此之快,以至于框架确定不需要上下文切换并允许继续捕获的上下文,无论如何。我发现如果我删除Thread.Sleep(100)而是使用更高的数字进行迭代(i<10000 左右,尽管我没有尝试找到阈值),它会锁定几秒钟但最终切换上下文直到完成。所以我猜测后一种解释更有可能。

最终,我的问题是,“我如何确保我的 StreamWriter.WriteLineAsync() 调用在另一个上下文中继续,以便我的 UI 线程可以保持响应?”

标签: c#wpfasync-awaitstreamwriter.net-4.6.1

解决方案


了解异步方法的工作原理很重要。所有异步方法都开始同步运行。魔术首先发生在await不完整Task的 上,此时将在堆栈中await返回自己的不完整。Task

所有这一切意味着,如果一个方法在发出任何异步 I/O 请求之前正在执行同步(或 CPU 消耗)操作,那么它仍然会阻塞线程。

至于在WriteLineAsync做什么,好吧,源代码是可用的,所以你可以从这里开始。如果我正确地遵循它,我认为它最终会调用BeginWrite,它调用参数设置BeginWriteInternal为. 这意味着它最终会调用同步等待。上面有一条评论:serializeAsynchronouslyfalsesemaphore.Wait()

// To avoid a race with a stream's position pointer & generating ---- 
// conditions with internal buffer indexes in our own streams that 
// don't natively support async IO operations when there are multiple 
// async requests outstanding, we will block the application's main
// thread if it does a second IO request until the first one completes.

现在我不能说这是否真的是阻碍它的原因,但这肯定是可能的。

但无论如何,如果您看到这种行为,最好的选择是立即使用以下命令将其从 UI 线程中移除Task.Run()

await Task.Run(() => DoWork(writer));

推荐阅读