1

In my WPF application I have an issue where I am writing numerous times to a text file via a StreamWriter object, primarily using the WriteLineAsync() method. I am confident that I am correctly awaiting all of the tasks. However, the UI thread is being blocked, instead of being allowed to process UI changes. I have written a small application that demonstrates the issue.

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);
        }
    }
}

And the 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>

And, for the sake of being complete, a basic implementation of 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;
    }
}

When I click the button simply labeled "Button", my UI freezes even though the body of DoWork() has all Async methods awaited with ConfigureAwait(false) set.

When I click Button2, however, I am performing an await Task.Delay(1).ConfigureAwait(false) before awaiting the DoWork() method. This appears to correctly shift processing to another context, allowing the UI to continue. Indeed, if I move the await Task.Delay(1).ConfigureAwait(false) to the DoWork() method and set it up before the for loop, everything runs as one would expect - the UI remains responsive.

It appears that, for whatever reason, StreamWriter.WriteLineAsync() is either not truly async, or the processing is happening so fast that the framework determines that there is no need for a context switch and allows continuation on the captured context, regardless. I have found that if I remove the Thread.Sleep(100) and, instead, iterate with a much higher number (i<10000 or so, though I have not tried to find the threshold), it will lock for a few seconds but eventually switch contexts until it completes. So I'm guessing that the latter explanation is more likely.

Ultimately, the question I have is, "How do I ensure that my StreamWriter.WriteLineAsync() calls continue on another context so that my UI thread can remain responsive?"

Lassanter
  • 23
  • 5
  • Don't use Thread.Sleep. Use Task.Delay instead. – ckuri Dec 18 '19 at 17:21
  • When you don't need to return any results to the main thread, it's easiest to offload the work to a background thread explicitly: `await Task.Run(() => DoWork(writer));` And you can remove all `configureAwait` code. – Funk Dec 18 '19 at 17:33
  • @ckuri The Thread.Sleep() is only there to demonstrate the blocking of the UI thread. If the context were switching as expected, the Thread.Sleep() would have no impact on the UI thread. – Lassanter Dec 18 '19 at 17:46
  • @funk That does seem to be the most realistic choice, though I was hoping that the async/await framework would have been able to function for me. – Lassanter Dec 18 '19 at 17:46
  • async/await works fantatic in WPF... The question here is till the Thread.Sleep(). When you do have sync (blocking) work to do, use Task.Run(). Witout the Sleep() this would work great, __running on a single thread__ as intended. But you don't want to Sleep() on that thread. You should almost never need ConfigureAwait(). – H H Dec 18 '19 at 18:19
  • According to the [guidelines](https://learn.microsoft.com/en-us/dotnet/standard/asynchronous-programming-patterns/task-based-asynchronous-pattern-tap#naming-parameters-and-return-types) your `DoWork` method should be named `DoWorkAsync`. – Theodor Zoulias Dec 18 '19 at 18:43

2 Answers2

2

It's important to understand how asynchronous methods work. All asynchronous methods start running synchronously. The magic happens at the first await that acts on an incomplete Task, at which point await will return its own incomplete Task up the stack.

All that means that if a method is doing something synchronous (or CPU-consuming) before whatever asynchronous I/O request it is going to make, then that will still block the thread.

As to what WriteLineAsync is doing, well, the source code is available, so you can walk through it starting here. If I followed it correctly, I think it ends up calling BeginWrite, which calls BeginWriteInternal with the serializeAsynchronously parameter set to false. That means it ends up calling semaphore.Wait(), a synchronous wait. There's a comment above that:

// 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.

Now I can't say if that's actually what's holding it up, but it's certainly possible.

But in any case, the best option if you're seeing this behaviour is to just get it off the UI thread right away by using Task.Run():

await Task.Run(() => DoWork(writer));
Gabriel Luci
  • 38,328
  • 4
  • 55
  • 84
  • That could, indeed, be the culprit. Which seems to suggest to me that it's not QUITE a truly async method. At any rate, Task.Run() still appears to be the best bet. It at least LOOKS like correct code, while an "await Task.Delay(1).ConfigureAwait(false) looks completely out of place, though it does do something useful. I'll simply add that into a comment in my code. Thanks! – Lassanter Dec 18 '19 at 18:18
  • 1
    As a side note, [`Task.Yield()`](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task.yield) is designed to do what you're using `Task.Delay(1)` for (but without a `Timer`). – Gabriel Luci Dec 18 '19 at 18:29
  • Ah, that could be an even better choice instead of manually forcing a new thread with Task.Run(). – Lassanter Dec 18 '19 at 18:33
  • Unfortunately, Task.Yield() doesn't guarantee a context switch, either. It remains on the UI thread and the continuation still blocks. Task.Run() it is, until I find something better. – Lassanter Dec 18 '19 at 18:41
  • @Lassanter calling `Task.Run` with an asynchronous delegate doesn't force a new thread. A thread-pool thread is utilized only for *starting* the task, and it's freed afterwards. In my opinion using `Task.Run` is the preferred way for running async code from event handlers of UI application. You can read my arguments [here](https://stackoverflow.com/questions/38739403/await-task-run-vs-await-c-sharp/58306020#58306020). – Theodor Zoulias Dec 18 '19 at 18:55
  • @Lassanter Yeah, `Task.Run` would still be preferred. I was just mentioning `Task.Yield()` for the sake of random knowledge acquisition. :) – Gabriel Luci Dec 18 '19 at 19:10
  • @Lassanter regarding `await Task.Yield`, according to the [documentation](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task.yield#remarks) it forces the asynchronous completion of an async method. This is guaranteed. What is not guaranteed is that the UI will remain responsive. – Theodor Zoulias Dec 18 '19 at 19:33
  • @TheodorZoulias I like your reasoning to a certain extent. But it seems that if I am going to manually offload to a background thread, anyway, I don't really need to be writing async code to begin with. While I understand your argument that basically says, "You/I don't write good async code", that seems a poor reason to avoid it entirely. My goal is to become competent enough that the Stephens don't look down on me shamefully. Simply offloading to a background thread manually doesn't force me to recognize where my shortcomings are, and fix them. It lets me to continue writing bad async code. – Lassanter Dec 18 '19 at 19:44
  • 1
    @Lassanter You are right about that. There is no meaningful penalty to creating new threads in a UI app. And it's not a big deal to lock a non-UI thread. So yeah, you could just go synchronous on another thread. That's different than ASP.NET, for example, which has a limited number of threads for the whole app. In that case, you'd want to avoid creating new threads if at all possible. – Gabriel Luci Dec 18 '19 at 20:06
  • @Lassanter the cost of offloading the *starting* of a `Task` to a background thread in extremely small. The overhead is around 2 μsec in my PC. By using `Task.Run` with async delegates in event handlers you are buying peace of mind at a cost of 1 sec of CPU time per 500,000 invocations. Even the two Stephens could decide to buy it at such a low cost! :-) – Theodor Zoulias Dec 18 '19 at 23:51
1

Some asynchronous APIs are expected to be called multiple times in a loop, and are designed to return completed tasks most of the time. Examples of these APIs are the methods 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.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
  • Ahhhh! THIS tells me everything. So you're saying that WriteLineAsync is guaranteed to always return a completed Task? Which means that awaiting it will never switch contexts. That's the piece I was missing. Thanks! – Lassanter Dec 18 '19 at 19:54
  • @Lassanter You can test for that by assigning the `Task` to a variable: `var myTask = writer.WriteLineAsync("test" + i);` then set a breakpoint immediately after and inspect `myTask.IsCompleted`. – Gabriel Luci Dec 18 '19 at 20:03
  • @Lassanter not guaranteed, but likely. Any method which returns a Task might return a completed one, if it determines that the operation can be completed synchronously. – canton7 Dec 18 '19 at 23:04
  • @Lassanter not guaranteed. Periodically will certainly return a non-completed `Task`. Implementations of [`TextWriter.WriteLineAsync`](https://learn.microsoft.com/en-us/dotnet/api/system.io.textwriter.writelineasync) that always return a completed `Task` *do* exist though. For example `Console.Out.WriteLineAsync`. – Theodor Zoulias Dec 18 '19 at 23:59