CodeGym /课程 /C# SELF /类 Task 和 Task<TResult>

类 Task 和 Task<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,并返回 Task 或 Task<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。

关于 Task 和 Task<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 并至少在内部记录异常!

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

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

常见问答和意外情况

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

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

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

2
任务
C# SELF, 第 60 级, 课程 0
已锁定
使用 Task 返回结果
使用 Task 返回结果
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION