StreamWriter.WriteLineAsync() 不会在新上下文中继续
StreamWriter.WriteLineAsync() does not continue on new context
在我的 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;
}
}
当我单击简单标记为 "Button" 的按钮时,我的 UI 冻结,即使 DoWork() 的主体已等待所有 Async 方法并设置了 ConfigureAwait(false)。
但是,当我单击 Button2 时,我会在等待 DoWork() 方法之前执行 await Task.Delay(1).ConfigureAwait(false)。这似乎正确地将处理转移到另一个上下文,允许 UI 继续。事实上,如果我将 await Task.Delay(1).ConfigureAwait(false) 移动到 DoWork() 方法并在 for 循环之前设置它,一切都会像一个人一样运行expect - UI 保持响应。
看来,无论出于何种原因,StreamWriter.WriteLineAsync() 要么不是真正的异步,要么处理进行得太快以至于框架确定不需要上下文切换并允许继续捕获的上下文,无论如何。我发现如果我删除 Thread.Sleep(100) 而是用更高的数字进行迭代(i<10000 左右,虽然我没有试图找到阈值),它将锁定几秒钟,但最终会切换上下文直到完成。所以我猜测后一种解释更有可能。
最终,我的问题是,"How do I ensure that my StreamWriter.WriteLineAsync() calls continue on another context so that my UI thread can remain responsive?"
了解异步方法的工作原理很重要。所有异步方法都同步启动 运行。魔术发生在作用于不完整 Task
的第一个 await
处,此时 await
将 return 其自身的不完整 Task
向上堆叠。
所有这一切意味着,如果一个方法正在做一些同步的事情(或 CPU-消耗)before 无论它要做什么异步 I/O 请求,那么它仍然会阻塞线程。
至于 WriteLineAsync
在做什么,好吧,源代码是可用的,因此您可以从 here. If I followed it correctly, I think it ends up calling BeginWrite
, which calls BeginWriteInternal
开始,将 serializeAsynchronously
参数设置为 false
.这意味着它最终调用 semaphore.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.
现在我不能说这是否实际上是什么阻碍了它,但这肯定是可能的。
但无论如何,如果您看到此行为,最好的选择是立即使用 Task.Run()
:
将其从 UI 线程中移除
await Task.Run(() => DoWork(writer));
一些异步 API 预计会在一个循环中被调用多次,并且被设计为 return 大多数时间完成的任务。这些 API 的示例是方法 StreamReader.ReadLineAsync
and StreamWriter.WriteLineAsync
. These objects keep an internal buffer so that they can read and write large chunks of data to the underlying Stream
at once, for performance reasons. An async operation is initiated only when this buffer needs to be refilled or flushed. The emergence of APIs like these motivated the introduction of the ValueTask<T>
type, so that the allocation of large numbers of completed Task<T>
objects could be avoided. You can watch an enlightening video about the reasons that led to these decisions here.
在我的 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;
}
}
当我单击简单标记为 "Button" 的按钮时,我的 UI 冻结,即使 DoWork() 的主体已等待所有 Async 方法并设置了 ConfigureAwait(false)。
但是,当我单击 Button2 时,我会在等待 DoWork() 方法之前执行 await Task.Delay(1).ConfigureAwait(false)。这似乎正确地将处理转移到另一个上下文,允许 UI 继续。事实上,如果我将 await Task.Delay(1).ConfigureAwait(false) 移动到 DoWork() 方法并在 for 循环之前设置它,一切都会像一个人一样运行expect - UI 保持响应。
看来,无论出于何种原因,StreamWriter.WriteLineAsync() 要么不是真正的异步,要么处理进行得太快以至于框架确定不需要上下文切换并允许继续捕获的上下文,无论如何。我发现如果我删除 Thread.Sleep(100) 而是用更高的数字进行迭代(i<10000 左右,虽然我没有试图找到阈值),它将锁定几秒钟,但最终会切换上下文直到完成。所以我猜测后一种解释更有可能。
最终,我的问题是,"How do I ensure that my StreamWriter.WriteLineAsync() calls continue on another context so that my UI thread can remain responsive?"
了解异步方法的工作原理很重要。所有异步方法都同步启动 运行。魔术发生在作用于不完整 Task
的第一个 await
处,此时 await
将 return 其自身的不完整 Task
向上堆叠。
所有这一切意味着,如果一个方法正在做一些同步的事情(或 CPU-消耗)before 无论它要做什么异步 I/O 请求,那么它仍然会阻塞线程。
至于 WriteLineAsync
在做什么,好吧,源代码是可用的,因此您可以从 here. If I followed it correctly, I think it ends up calling BeginWrite
, which calls BeginWriteInternal
开始,将 serializeAsynchronously
参数设置为 false
.这意味着它最终调用 semaphore.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.
现在我不能说这是否实际上是什么阻碍了它,但这肯定是可能的。
但无论如何,如果您看到此行为,最好的选择是立即使用 Task.Run()
:
await Task.Run(() => DoWork(writer));
一些异步 API 预计会在一个循环中被调用多次,并且被设计为 return 大多数时间完成的任务。这些 API 的示例是方法 StreamReader.ReadLineAsync
and StreamWriter.WriteLineAsync
. These objects keep an internal buffer so that they can read and write large chunks of data to the underlying Stream
at once, for performance reasons. An async operation is initiated only when this buffer needs to be refilled or flushed. The emergence of APIs like these motivated the introduction of the ValueTask<T>
type, so that the allocation of large numbers of completed Task<T>
objects could be avoided. You can watch an enlightening video about the reasons that led to these decisions here.