我有一个保存远程图片到本地的方法,代码如下:

public static async Task CacheImageToLocalAsync(string imageUrl, string localPath)
 {
     // 模拟异步操作,比如从 URL 下载图像并缓存到本地
     using (var httpClient = new HttpClient())
     {
         Console.WriteLine("开始下载图片1");
         byte[] imageBytes = await httpClient.GetByteArrayAsync(imageUrl);
         Console.WriteLine("开始下载图片2");
         using (FileStream fileStream = new FileStream(localPath, FileMode.Create, FileAccess.Write, FileShare.None, bufferSize: 4096, useAsync: true))
         {
             await fileStream.WriteAsync(imageBytes, 0, imageBytes.Length);
         }

     }
 }

那调用CacheImageToLocalAsync这个异步方法有哪几种方式呢?

在 C# 中,async 方法可以通过多种方式进行调用,具体取决于调用者是否希望等待该异步操作完成以及如何处理返回的任务。以下是对 CacheImageToLocalAsync 方法的几种常见调用方式:

1. 使用 await 等待任务完成

这是最常见的调用方式,适用于需要等待异步操作完成后再继续执行后续逻辑的情况。


public static async Task Main(string[] args)
{
    string imageUrl = "https://example.com/image.jpg";
    string localPath = "C:\\Temp\\image.jpg";
    // 调用并等待异步方法完成
    await CacheImageToLocalAsync(imageUrl, localPath);
    Console.WriteLine("图片已缓存到本地!");
}

特点

使用 await 关键字等待异步任务完成。

主线程不会被阻塞,程序可以继续执行其他非依赖于该任务的工作。

如果任务抛出异常,await 会将异常传播给调用方。

2. 不等待任务完成(忽略返回值)

如果调用者不关心任务的结果或完成时间,可以直接调用而不使用 await。


public static void Main(string[] args)
{
    string imageUrl = "https://example.com/image.jpg";
    string localPath = "C:\\Temp\\image.jpg";
    // 调用但不等待任务完成
    _ = CacheImageToLocalAsync(imageUrl, localPath);
    Console.WriteLine("图片缓存任务已启动!");
}

注意事项

此方式可能会导致程序在任务完成前退出,尤其是对于短生命周期的应用(如控制台应用)。

如果任务中发生异常,这些异常会被捕获到任务对象中,但如果没有显式处理,可能会被忽略。

3. 使用 .Wait() 或 .Result 强制等待任务完成

这种方式适用于必须等待任务完成但无法使用 await 的场景(例如在同步方法中调用异步方法)。


public static void Main(string[] args)
{
    string imageUrl = "https://example.com/image.jpg";
    string localPath = "C:\\Temp\\image.jpg";
    try
    {
        // 使用 .Wait() 等待任务完成
        CacheImageToLocalAsync(imageUrl, localPath).Wait();
        Console.WriteLine("图片已缓存到本地!");
    }
    catch (AggregateException ex)
    {
        // 处理可能的异常
        Console.WriteLine($"发生错误:{ex.InnerExceptions[0].Message}");
    }
}

注意事项

使用 .Wait() 或 .Result 可能会导致死锁,特别是在 UI 线程或 ASP.NET 请求上下文中22。

推荐尽量避免在现代代码中使用此方式,除非确实必要。

4. 将任务附加到主线程(适用于 WPF/WinForms 应用)

在 WPF 或 WinForms 应用中,可以确保异步任务完成后更新 UI。


public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }
    private async void Button_Click(object sender, RoutedEventArgs e)
    {
        string imageUrl = "https://example.com/image.jpg";
        string localPath = "C:\\Temp\\image.jpg";
        // 调用异步方法并更新 UI
        await CacheImageToLocalAsync(imageUrl, localPath);
        Dispatcher.Invoke(() =>
        {
            Console.WriteLine("图片已缓存到本地!");
        });
    }
}

特点

使用 Dispatcher.Invoke 确保 UI 更新发生在正确的线程上13。

适用于需要与用户界面交互的场景。

5. 使用 Task.Run 在后台线程中执行

如果希望将异步任务放到后台线程中执行,可以结合 Task.Run 使用。


public static async Task Main(string[] args)
{
    string imageUrl = "https://example.com/image.jpg";
    string localPath = "C:\\Temp\\image.jpg";
    // 使用 Task.Run 将任务放入后台线程
    await Task.Run(async () => await CacheImageToLocalAsync(imageUrl, localPath));
    Console.WriteLine("图片已缓存到本地!");
}

特点

将任务交给线程池执行,减少对主线程的影响。

适用于长时间运行的任务。

6. 注册回调函数(使用 .ContinueWith)

如果需要在任务完成后执行某些操作,可以使用 .ContinueWith 方法注册回调函数。

public static void Main(string[] args)
{
    string imageUrl = "https://example.com/image.jpg";
    string localPath = "C:\\Temp\\image.jpg";
    // 调用异步方法并注册回调
    CacheImageToLocalAsync(imageUrl, localPath)
        .ContinueWith(task =>
        {
            if (task.IsCompletedSuccessfully)
            {
                Console.WriteLine("图片已缓存到本地!");
            }
            else if (task.IsFaulted)
            {
                Console.WriteLine($"发生错误:{task.Exception?.InnerException?.Message}");
            }
        });
}

特点

回调函数会在任务完成后自动执行。

适合需要解耦任务逻辑和后续处理逻辑的场景。

调用方式场景描述是否阻塞主线程异常处理
使用 await需要等待任务完成后再继续执行自动传播异常
不等待任务完成不关心任务结果或完成时间异常可能被忽略
使用 .Wait() 或 .Result必须等待任务完成但无法使用 await需要手动捕获异常
附加到主线程(WPF/WinForms)需要在任务完成后更新 UI自动传播异常
使用 Task.Run将任务放到后台线程中执行自动传播异常
注册回调函数(.ContinueWith需要在任务完成后执行某些操作需要手动检查异常状态

根据实际需求选择合适的调用方式,推荐优先使用 await,因为它提供了最简洁、直观的异步编程体验。