正确使用async和await

本文关键字:await async | 更新日期: 2023-09-27 17:49:28

我刚开始在c#中处理异步编程,我开始阅读有关异步方法和await的内容。

在下面的代码块中,WPF应用程序接受来自用户的输入,将其保存到Bin目录中的一个文件中,并将其读取回文本框。我必须使用async方法来读写,但我还需要在WriteTextReadText方法内部的方法中实现await

你能给我一个简短的解释我应该如何在这段代码中实现async和await的使用吗?

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

正确使用async和await

让我们一个一个地看:

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

task1在这里干什么?您需要实际运行它并等待它:

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

但等等!我们正在创建一个Task,它创建一个Task,然后等待Task。为什么不直接返回Task呢?

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

请记住,async使我们更容易创建在Task中执行某些操作的方法,但如果你已经有了Task,那么它就是浪费时间。

此外,按照惯例,您应该将异步方法命名为所有以Async结尾的方法。这里更是如此,因为你和其他WriteTextAsync只在签名上有所不同:

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

实际上,如果你有一个非异步void WriteText(string filePath, string text),你会从一个非异步void WriteText()调用它,这是没有什么不同的。这里没有什么新东西

现在,再看WriteTextAsync:

因为我们现在有了任务,我们根本不需要使用旧的BeginWrite(但见下文),我们只是像使用Write一样使用WriteAsync:

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()是好的。让我们看看它调用了什么:

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

这将工作,但它不会获得任何东西。但是,我们可以用ReadAsync:

中的await替换Read
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();
  }
}

对于非异步版本,一个更好的整体解决方案是使用ReadToEnd(),在面对可能由这样的Read分割字符的编码时,更简单,更有弹性。同样,这里更好的版本是使用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();
    }
  }
}

请注意,当我们返回await执行任务的结果时,我们不能在这种情况下将其替换为return rdr.ReadToEndAsync(),因为这样我们将在ReadToEndAsync()实际完成之前离开using。我们需要await来确保我们得到了实际的结果,然后做IDisposable.Dispose()调用,留下using调用。

使用TPL (async)与旧APM (BeginXxxEndXxx):

让我们假设我们没有stream.WriteAsync(),必须使用stream.BeginWrite()stream.EndWrite()。如果您使用的是较旧的库,可能会发生类似的情况。

我们可以使用TaskFactory.FromAsync来创建一个Task,它将旧的方法封装在新的方法中。因此:

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

:

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

这显然比仅仅使用XxxAsync方法更复杂,我们可以await,但它仍然比调用BeginXxx然后在回调中处理EndXxx更简单,特别是在像上面的ReadText这样的情况下,这必须导致另一个循环进入BeginXxx

FileStream类具有用于读写流的异步方法:ReadAsyncWriteAsync,因此您所需要做的就是在代码中替换掉这些方法,并在它们前面加上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();
    }
}

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

我相信这两个方法可以进一步简化,但这应该让您开始使用异步方法。

如果你想使用一个没有async方法的类,并且你想在一个单独的线程上执行那个任务,你仍然可以这样使用async/await:

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