Alexander P.
Alexander P.

Reputation: 392

Correct use of async and await

I'm just starting to deal with Asynchronous programming in c#, and I started reading about async methods and await.

In this block of code below, the WPF application takes an input from the user, saves it to a file in the Bin directory, and reads it back to the textbox. I had to use async methods to read and write, but I also need to implement await in the methods inside the WriteText and ReadText methods.

Can you give me a brief explanation on how I should implement the use of async and await in this code?

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }

    private async void btnWriteFile_Click(object sender, RoutedEventArgs e)
    {
       await WriteFile();
    }

    private async void btnReadFile_Click(object sender, RoutedEventArgs e)
    {
        await ReadFile();
    }

    public async Task WriteFile()
    {
        string filePath = @"SampleFile.txt";
        string text = txtContents.Text;

        Task task1 = new Task( () =>  WriteTextAsync(filePath, text));
    }

    private async Task WriteTextAsync(string filePath, string text)
    {
        byte[] encodedText = Encoding.Unicode.GetBytes(text);

        using (FileStream sourceStream = new FileStream(filePath,
            FileMode.Create, FileAccess.Write, FileShare.None, 
            bufferSize: 4096, useAsync: true))
        {
              //sourceStream.BeginWrite(encodedText, 0, encodedText.Length);
             await ?? sourceStream.BeginWrite(encodedText, 0, encodedText.Length, null, null);
        };
    }

    public async Task ReadFile()
    {
        string filePath = @"SampleFile.txt";

        if (File.Exists(filePath) == false)
        {
            MessageBox.Show(filePath + " not found", "File Error", MessageBoxButton.OK);
        }
        else
        {
            try
            {
                string text = await ReadText(filePath);
                txtContents.Text = text;
            }
            catch (Exception ex)
            {
                Debug.WriteLine(ex.Message);
            }
        }
    }

    private async Task<string> ReadText(string filePath)
    {
         using (FileStream sourceStream = new FileStream(filePath,
            FileMode.Open, FileAccess.Read, FileShare.Read,
            bufferSize: 4096))
        {
            StringBuilder sb = new StringBuilder();

            byte[] buffer = new byte[0x1000];
            int numRead;
            while ((numRead = sourceStream.Read(buffer, 0, buffer.Length)) != 0)
            {
                string text = Encoding.Unicode.GetString(buffer, 0, numRead);
                sb.Append(text);
            }

            return sb.ToString();
        }
    }
}

Upvotes: 1

Views: 922

Answers (2)

Jon Hanna
Jon Hanna

Reputation: 113392

Let's take them one at a time:

public async Task WriteFile()
{
  string filePath = @"SampleFile.txt";
  string text = txtContents.Text;
  Task task1 = new Task( () =>  WriteTextAsync(filePath, text));
}

What's task1 doing here? You need to actually have run it and await on it:

public async Task WriteFile()
{
  string filePath = @"SampleFile.txt";
  string text = txtContents.Text;
  Task task1 = new Task( () =>  await WriteTextAsync(filePath, text));
  await task1;
}

But wait! We're creating a Task that creates a Task and then waits on that Task. Why not just return the Task in the first place?

public Task WriteFile()
{
  string filePath = @"SampleFile.txt";
  string text = txtContents.Text;
  return WriteTextAsync(filePath, text);
}

Remember, async makes it easier for us to create methods that perform something in a Task, but if you have already have a Task then it's a waste of time.

Also, as a matter of convention you should name your asynchronous methods to all end in Async. This is even more so here, because you differ from the other WriteTextAsync only in signature:

public Task WriteTextAsync()
{
  return WriteTextAsync(@"SampleFile.txt", txtContents.Text);
}

Really this is no different to how if you had a non-async void WriteText(string filePath, string text) you would call it from a non-async void WriteText(). Nothing actually new here.

Now, onto that more involved WriteTextAsync:

Because we've now got Tasks we don't need to use the old BeginWrite at all (but see below), we just use WriteAsync analogously to how we use Write:

private async Task WriteTextAsync(string filePath, string text)
{
  byte[] encodedText = Encoding.Unicode.GetBytes(text);
  using (FileStream sourceStream = new FileStream(filePath,
    FileMode.Create, FileAccess.Write, FileShare.None, 
    bufferSize: 4096, useAsync: true))
  {
    await sourceStream.WriteAsync(encodedText, 0, encodedText.Length);
  }
}

ReadFile() is fine. Let's look at what it calls into:

private async Task<string> ReadText(string filePath)
{
  using (FileStream sourceStream = new FileStream(filePath,
    FileMode.Open, FileAccess.Read, FileShare.Read,
    bufferSize: 4096))
  {
    StringBuilder sb = new StringBuilder();

    byte[] buffer = new byte[0x1000];
    int numRead;
    while ((numRead = sourceStream.Read(buffer, 0, buffer.Length)) != 0)
    {
      string text = Encoding.Unicode.GetString(buffer, 0, numRead);
      sb.Append(text);
    }

    return sb.ToString();
  }
}

This will work, but it won't gain anything. However, we can replace the Read with awaiting ReadAsync:

private async Task<string> ReadText(string filePath)
{
  using (FileStream sourceStream = new FileStream(filePath,
    FileMode.Open, FileAccess.Read, FileShare.Read,
    bufferSize: 4096))
  {
    StringBuilder sb = new StringBuilder();

    byte[] buffer = new byte[0x1000];
    int numRead;
    while ((numRead = await sourceStream.ReadAsync(buffer, 0, buffer.Length)) != 0)
    {
      string text = Encoding.Unicode.GetString(buffer, 0, numRead);
      sb.Append(text);
    }

    return sb.ToString();
  }
}

A better overall solution to the non-async version, simpler and more resilient in the face of encodings that might have characters split by such a Read, would have been to use ReadToEnd(). Likewise a better version here is to use ReadToEndAsync():

private async Task<string> ReadText(string filePath)
{
  using (FileStream sourceStream = new FileStream(filePath,
    FileMode.Open, FileAccess.Read, FileShare.Read,
    bufferSize: 4096))
    {
    using(var rdr = new StreamReader(sourceStream, Encoding.Unicode))
    {
      return await rdr.ReadToEndAsync();
    }
  }
}

Note that while we're returning the result of awaiting a task, we cannot in this case just replace this with return rdr.ReadToEndAsync() because then we'll leave the using before the ReadToEndAsync() has actually finished. We need await to make sure we've got the actual result of that and then do the IDisposable.Dispose() calls that leaving a using invokes.

Using TPL (async) with the old APM (BeginXxxEndXxx):

Let's imagine we didn't have stream.WriteAsync() and had to use stream.BeginWrite() and stream.EndWrite(). Something like this can happen if you are using an older library.

We can use TaskFactory.FromAsync to create a Task that wraps the old approach in the new. Hence:

private async Task WriteTextAsync(string filePath, string text)
{
  byte[] encodedText = Encoding.Unicode.GetBytes(text);
  using (FileStream sourceStream = new FileStream(filePath,
    FileMode.Create, FileAccess.Write, FileShare.None, 
    bufferSize: 4096, useAsync: true))
  {
    await Task.Factory.FromAsync(sourceStream.BeginWrite, sourceStream.EndWrite, encodedText, 0, encodedText.Length, null);
  }
}

And:

private async Task<string> ReadText(string filePath)
{
  using(FileStream sourceStream = new FileStream(filePath,
    FileMode.Open, FileAccess.Read, FileShare.Read,
    bufferSize:4096))
  {
    StringBuilder sb = new StringBuilder();

    byte[] buffer = new byte[0x1000];
    int numRead;
    while((numRead = await Task<int>.Factory.FromAsync(sourceStream.BeginRead, sourceStream.EndRead, buffer, 0, buffer.Length, null)) != 0)
    {
      sb.Append(Encoding.Unicode.GetString(buffer, 0, numRead);
    }

    return sb.ToString();
  }
}

This is a clearly a lot more convoluted than just having an XxxAsync method we can await, but it's still simpler than calling BeginXxx and then handling EndXxx in a callback, especially in cases like the ReadText above where this then has to lead to another loop into BeginXxx.

Upvotes: 4

olitee
olitee

Reputation: 1683

The FileStream class has asynchronous methods for reading and writing to the stream: ReadAsync and WriteAsync, so all you need to do is swap out those methods in your code, and prefix them with await:

private async Task<string> ReadText(string filePath)
{
    using (FileStream sourceStream = new FileStream(filePath,
        FileMode.Open, FileAccess.Read, FileShare.Read,
        bufferSize: 4096))
    {
        StringBuilder sb = new StringBuilder();

        byte[] buffer = new byte[0x1000];
        int numRead;
        while ((numRead = await sourceStream.ReadAsync(buffer, 0, buffer.Length)) != 0)
        {
            string text = Encoding.Unicode.GetString(buffer, 0, numRead);
            sb.Append(text);
        }

        return sb.ToString();
    }
}

and

private async Task WriteTextAsync(string filePath, string text)
{
    byte[] encodedText = Encoding.Unicode.GetBytes(text);

    using (FileStream sourceStream = new FileStream(filePath,
        FileMode.Create, FileAccess.Write, FileShare.None, 
        bufferSize: 4096, useAsync: true))
    {
        await sourceStream.WriteAsync(encodedText, 0, encodedText.Length);
    };
}

I'm sure these two methods can be simplified further, but this should get you started on the async methods.

And just so you're aware, if you were trying to use a class that didn't have async methods, and you wanted to perform that task on a separate thread, you could still make use of async/await in this fashion:

private async Task<string> ReadText(string filePath)
{
    return await Task.Run(() =>
    {
        return File.ReadAllText("textfilepath.txt");
    });
}

Upvotes: 0

Related Questions