c# - 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 线程可以保持响应?”
解决方案
了解异步方法的工作原理很重要。所有异步方法都开始同步运行。魔术首先发生在await
不完整Task
的 上,此时将在堆栈中await
返回自己的不完整。Task
所有这一切意味着,如果一个方法在发出任何异步 I/O 请求之前正在执行同步(或 CPU 消耗)操作,那么它仍然会阻塞线程。
至于在WriteLineAsync
做什么,好吧,源代码是可用的,所以你可以从这里开始。如果我正确地遵循它,我认为它最终会调用BeginWrite
,它调用参数设置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.
现在我不能说这是否真的是阻碍它的原因,但这肯定是可能的。
但无论如何,如果您看到这种行为,最好的选择是立即使用以下命令将其从 UI 线程中移除Task.Run()
:
await Task.Run(() => DoWork(writer));
推荐阅读
- qt - 在 .pro 文件中添加多个库的问题 - qmake
- r - 使用显示 NA 值的 tab_cells 在 R 中创建交叉表
- css - 如何在 Gmail 附件中实现“悬停”?
- julia - 当您首先不知道数组的尺寸时如何在 Julia 中初始化数组
- list - 如何从列表列表的某些元素之间的组合生成多个列表(在java或C中)
- android - 出现 qr scannig Fragment 但未进行扫描
- wordpress - 使用 Wordpress 身份验证解析平台
- android - AlarmManager setRepeating() 在不正确的时间触发
- python-3.x - 如何编码具有高基数的列?
- python - 如何纠正错误:迭代器应该返回字符串,而不是字节(您是否以文本模式打开文件?)在 csv e netcdf 文件中?