CodeGym /课程 /C# SELF /Task

TaskTask<TResult>

C# SELF
第 60 级 , 课程 0
可用

1. 介绍

在异步编程的世界里,我们遵循“带回调的委托”原则。想象一下:你需要下载一个超大文件、分析几GB 日志或向远端服务器发请求。与其像雕像一样僵在那里等,不如对系统说:“你去处理这件事,我先去做别的。做完了记得告诉我!”

这时候 Task 就登场了——它是这种做法的优雅体现。它不仅是个技术抽象,还是一种“聪明的中间人”,承担执行工作并保证结果不会丢失。

Task 像个私人助理,你把重要的事交给它。它点头,把任务记在本子上,说:“你放心去做别的吧,我会在完成后带着结果来找你。” 它真的会带来结果,或者会诚实地说明哪里出了问题。

如果找更生活化的比喻,Task 更像现代的网上预约系统:你在线挂号,拿到确认,就不用在候诊室里耗时间。系统会在就诊时间临近时提醒你,而你 meanwhile 可以正常生活。

Task

Task 是 .NET 异步编程的基本构建块。它表示已启动或将来会完成的操作,其结果将在未来可用。如果方法无需返回值,就用简单的 Task

public async Task BackupToCloudAsync()
{
    // 做备份的魔法,不返回任何东西
}

Task<TResult>

如果需要返回结果(比如字符串、数字、对象…),就用 Task<TResult>

public async Task<string> DownloadHtmlAsync(string url)
{
    // 下载页面并返回 HTML 代码
    return "<html>...</html>";
}

为什么用 Task 而不是 Thread

Thread 管理的是线程本身(这复杂且容易出错),而 Task 是更高层的抽象:它可以在线程池中执行,或在不分配新线程的情况下异步工作(比如 I/O 操作),不需要你操心底层细节。

Task 你只需要描述:“我想运行这个操作”,至于如何执行,让 .NET 去决定吧!

2. Task 对象的结构

需要知道的 Task 属性和方法

属性 / 方法 说明
Status
任务的当前状态
Result
Task<TResult> 的结果(会阻塞 线程)
IsCompleted
任务是否完成
IsFaulted
任务是否抛出异常
IsCanceled
任务是否被取消
Wait()
阻塞当前线程直到完成(危险
ContinueWith()
在完成后再启动另一个任务
Exception
如果 Task 以错误结束,访问异常信息
Id
任务的唯一标识符

Task 的异步方法如何工作

sequenceDiagram
    participant Main as Main 线程
    participant Task as Task (后台任务)
    Main->>Task: 启动 Task.Run(() => ...)
    Note right of Task: 后台执行
(CPU 或 I/O) alt 任务完成 Task->>Main: await 完成,继续执行 else 错误 Task->>Main: await 抛出异常 end

3. 创建和启动任务:Task 是怎么工作的

async 的异步方法

最常见的情况是把方法声明为 async,并返回 TaskTask<TResult>(就像上面看到的)。

Task.Run:在线程池中执行

如果需要在后台执行耗时工作(比如计算大数或转码视频),可以用 Task.Run

Task work = Task.Run(() =>
{
    // 复杂计算 — 不要阻塞主线程!
    Console.WriteLine("后台计算开始...");
    Thread.Sleep(2000); // 模拟耗时工作
    Console.WriteLine("后台计算完成!");
});

如果需要得到结果:

Task<int> calculateTask = Task.Run(() =>
{
    // 比如计算前 100 个数的和
    int sum = 0;
    for (int i = 1; i <= 100; i++) sum += i;
    return sum;
});

Task.Factory.StartNew

这是更低层更灵活的方式,允许精细配置任务启动(比如指定调度器、传递参数等)。在现代代码中通常推荐用 Task.Run,因为它更简单并能避免错误。

4. 今天的应用示例:我们的图书目录

假设我们有个图书目录应用,需要从“云”源加载书籍——这会是 I/O-bound 操作(慢的 HTTP 请求或文件读取)。

添加一个异步“加载”书籍的方法(模拟延迟):

public class Book
{
    public string Title { get; set; }
    public string Author { get; set; }
}

public class BookCatalog
{
    public List<Book> Books { get; set; } = new();

    public async Task LoadBooksAsync()
    {
        Console.WriteLine("正在加载书籍...");
        await Task.Delay(2000); // 模拟耗时加载(例如 HTTP 或 文件)
        Books = new List<Book>
        {
            new Book { Title = "CLR via C#", Author = "Jeffrey Richter" },
            new Book { Title = "C# in Depth", Author = "Jon Skeet" }
        };
        Console.WriteLine("书籍已成功加载。");
    }
}

Main 中调用异步加载(使用 await):

var catalog = new BookCatalog();
await catalog.LoadBooksAsync();
Console.WriteLine($"目录中有 {catalog.Books.Count} 本书。");

表格:创建和启动 Task 的主要方式

创建方法 如何使用 结果 用途
async 方法
async Task / async Task<T>
异步操作 通常用于 I/O,方便
Task.Run
Task.Run(() => { ... })
后台任务 CPU-bound(计算密集型)
TaskCompletionSource<T>
手动创建并完成 Task 完全由程序员控制 少用,适合底层场景

5. Task 的生命周期

Task 可以处于不同状态:

  • Created — 任务已创建,但未启动(适用于需要显式启动的 Task)。
  • WaitingToRun — 在线程池队列中等待。
  • Running — 正在执行。
  • WaitingForActivation — 等待启动或外部激活。
  • RanToCompletion — 已成功完成。
  • Faulted — 以错误(异常)结束。
  • Canceled — 被取消(如果支持取消)。

图示

flowchart LR
    Start -->|启动任务| Running
    Running -->|成功| Completed
    Running -->|错误| Faulted
    Running -->|取消| Canceled

实践验证

Task task = Task.Run(() =>
{
    Thread.Sleep(1000);
});
Console.WriteLine(task.Status); // 通常: Running 或 WaitingToRun
await task;
Console.WriteLine(task.Status); // 完成后是 RanToCompletion

6. 如何从 Task<TResult> 获取结果?

Task<TResult> 是对未来会出现结果的包装。当需要等待结果时,用 await

Task<int> sumTask = Task.Run(() =>
{
    int sum = 0;
    for (int i = 1; i <= 5; i++) sum += i;
    return sum;
});

int result = await sumTask;
Console.WriteLine(result); // 15

如果忘了写 await,你会得到一个 Task(一个承诺),而不是结果。这是常见的“异步坑”。

替代:同步获取结果(不要在 UI 中这样做!)

有时(比如在测试中)需要不使用 await 就取结果。可以用属性 .Result

int result = sumTask.Result;

但如果 Task 还没完成,这行代码会 阻塞 线程,如果是在 UI 线程,应用会卡死!所以:尽量总是用 await

关于 TaskTask<TResult> 的常见错误

忘记返回 Task,方法变成 void。如果方法没有返回值——请返回 Task,不要用 void,否则无法捕获错误。

忽略 await。只是调用方法不等待,任务就会“自顾自”地运行(“fire and forget”)。你无法知道它什么时候完成或是否报错。

通过 .Result .Wait() 进行阻塞等待。特别容易在 UI 和 ASP.NET 中造成 deadlock。尽量只用 await

7. Task 的高级功能

任务链:ContinueWith

可以在任务完成后“挂”上要执行的动作,使用 ContinueWith

Task.Run(() => 10)
    .ContinueWith(t =>
    {
        Console.WriteLine($"完成了!结果: {t.Result}");
    });

但在现代 C# 中通常用 async/await 来实现,阅读起来更清晰。

示例:并行与顺序加载数据

假设你要从不同来源加载两本书。可以并行启动两个 Task 并等待它们都完成:

public async Task LoadBooksFromMultipleSourcesAsync()
{
    Task<List<Book>> t1 = LoadFromCloudAsync();
    Task<List<Book>> t2 = LoadFromLocalAsync();

    // 并行等待两者完成
    await Task.WhenAll(t1, t2);

    // 合并结果
    Books = t1.Result.Concat(t2.Result).ToList();
}

private async Task<List<Book>> LoadFromCloudAsync()
{
    await Task.Delay(2000); // "云"
    return new List<Book> { new Book { Title = "Cloud Book", Author = "Cloud Author" } };
}

private async Task<List<Book>> LoadFromLocalAsync()
{
    await Task.Delay(1000); // "本地磁盘"
    return new List<Book> { new Book { Title = "Local Book", Author = "Local Author" } };
}

注意:用 await Task.WhenAll(...) 时,两个请求会同时启动并并行执行(如果可能),应用会等到两者都完成。

8. 有用的细节

Task 和 Fire-and-forget

有时候你想启动一个任务但不等它完成(比如发送日志到云或“烤个吐司”,用户继续操作):

async void LogToCloudAsync(string message)
{
    await Task.Run(() =>
    {
        // 发送日志的耗时操作
        Thread.Sleep(1000);
        Console.WriteLine($"日志已发送: {message}");
    });
}

但记住:这种任务如果发生错误,很难发现。所以如果可能,返回 Task 并至少在内部记录异常!

TaskTask<TResult> 在真实场景中的用法

  • 在客户端 UWP/WPF/WinForms 应用中不要阻塞 UI——对长时间操作(文件、网络)使用 Task
  • 在 WebAPI/ASP.NET 中 Task 有助于不浪费线程等待网络/数据库,从而提升性能。
  • 组织“并行”执行:同时下载、处理和保存。
  • 几乎所有耗时的方法都有 Async 版本:File.ReadAllTextAsyncHttpClient.GetStringAsync 等。

常见问答和意外情况

问题:为什么 Task 有时会同步执行?
回答:如果操作已经完成(比如结果被缓存),编译器或调度器可能会在同一线程上同步完成方法。这是正常的,可以加速重复调用。

问题:为什么不能使用 async void
回答:这样的函数无法被等待,无法捕获其异常,也无法跟踪完成状态。除非是 EventHandler(例如 Button_Click),否则请使用 Task

问题:能否启动多个任务然后只等待其中一个?
回答:可以——使用 Task.WhenAny

评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION