c# - 正确使用异步和等待

标签 c# async-await

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

在下面的代码块中,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();
        }
    }
}

最佳答案

让我们一次一个地处理它们:

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(但见下文),我们只需使用 WriteAsync 类似于我们如何使用 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() 没问题。让我们看看它调用了什么:

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

这会奏效,但不会有任何收获。但是,我们可以将 Read 替换为 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();
  }
}

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

这显然比我们可以 awaitXxxAsync 方法复杂得多,但它仍然比调用 BeginXxx 和然后在回调中处理 EndXxx,尤其是在像上面的 ReadText 这样的情况下,这会导致另一个循环进入 BeginXxx

关于c# - 正确使用异步和等待,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/30263978/

相关文章:

c# - Prop 和完整属性(property)有什么区别?

c# - 如何更改文本框特定的文本颜色?

c# - 对多个任务使用 async/await

C# - 编写方法 block

c# - 如何在.net winforms中执行在运行时生成的代码事件?

firebase - 异步函数中的异步操作

c# - 使用 HttpClient 的异步 NancyFX - fbcdn 的 GetAsync 返回 403 禁止?

javascript - 在继续 for 循环之前等待异步执行

c# - 如何枚举类声明的所有事件?

javascript - try catch 中未定义的值