ZetCode

C# Task

最后修改于 2023 年 7 月 5 日

在本文中,我们将展示如何在 C# 中使用 Task 进行并发操作。

并发编程用于两种任务:I/O 密集型任务和 CPU 密集型任务。从网络请求数据、访问数据库或读取和写入是 IO 密集型任务。CPU 密集型任务是计算量大的任务,例如数学计算或图形处理。

异步操作适用于 I/O 密集型任务。并行操作适用于 CPU 密集型任务。与其他语言不同,Task 可用于异步和并行操作。

注意: MSDN 文档不正确地使用了术语“异步”,而不是标准使用的“并发”。

Task 表示一个并发操作。

Task
Task<TResult>

Task 表示一个并发操作,而 Task<TResult> 表示一个可以返回值的并发操作。

Task.Run 方法用于并发地(理想情况下是并行地)运行 CPU 密集型代码。它将指定的工作排队以在 ThreadPool 上运行,并返回该工作的 Task 或 Task<TResult> 句柄。

.NET 包含许多方法,例如 StreamReader.ReadLineAsyncHttpClient.GetAsync,它们异步执行 I/O 密集型代码。它们与 async/await 关键字一起使用。

注意: 建议使用 Task.Run 而不是使用 new Task(); task.Start()

C# Task.Run

Task.Run 方法将任务放在不同的线程上。它适用于 CPU 密集型任务。

Program.cs
Console.WriteLine($"Main thread {getThreadId()} begin");

Task.Run(() =>
{
    Console.WriteLine($"Thread {getThreadId()} begin");

    Thread.Sleep(3000);

    Console.WriteLine($"Thread {getThreadId()} end");
});

Console.WriteLine($"Main thread {getThreadId()} end");

Console.ReadLine();

int getThreadId() 
{
    return Thread.CurrentThread.ManagedThreadId;
}

主线程在生成的任务完成之前完成。为了看到任务完成,我们使用 Console.ReadLine 等待用户输入。

$ dotnet run
Main thread 1 begin
Main thread 1 end
Thread 4 begin
Thread 4 end

Task<TResult> 表示一个返回结果的任务。

Program.cs
Task<int> task = Task.Run(() =>
{
    Thread.Sleep(3000);
    return 2 + 3;
});

var res = await task;
Console.WriteLine(res);

该程序展示了如何等待返回计算结果的任务。

C# Task.Delay

Task.Delay 创建一个在一段时间延迟后完成的任务。

Program.cs
Console.WriteLine("step 1");

await doTask();

Console.WriteLine("step 2");

async Task doTask()
{
    await Task.Delay(3000);
    Console.WriteLine("task finished");
}

创建任务的函数必须使用 async 关键字。

await Task.Delay(3000);

Task.Delay 创建一个新任务,该任务休眠三秒钟。await 运算符等待任务完成。它阻止主程序的执行,直到任务完成。

$ dotnet run
step 1
task finished
step 2

C# async Main 方法

当我们在 Main 方法中使用 await 运算符时,我们必须使用 async 修饰符标记它。

words.txt
sky
main
club
cotton
rocket

这是一个示例文本文件。

Program.cs
namespace AsyncMain;

class Program
{
    static async Task Main(string[] args)
    {
        using StreamReader reader = File.OpenText("words.txt");
        string? res = await reader.ReadLineAsync();

        Console.WriteLine($"First line is: {res}");
    }
}

该示例异步读取文件的第一行。这项工作是在 Main 方法内部完成的。

string? res = await reader.ReadLineAsync();

ReadLineAsync 方法返回一个 Task<String>,它表示一个异步读取操作。任务中的结果包含来自流的下一行,如果已读取所有字符,则为 null。

$ dotnet run
First line is: sky

C# Task.WaitAll

Task.WaitAll 方法等待所有提供的任务完成执行。

Program.cs
using System.Diagnostics;

var sw = new Stopwatch();
sw.Start();

Task.WaitAll(f1(), f2(), f3());

sw.Stop();

var elapsed = sw.ElapsedMilliseconds;
Console.WriteLine($"elapsed: {elapsed} ms");

async Task f1()
{
    await Task.Delay(4000);
    Console.WriteLine("f1 finished");
}

async Task f2()
{
    await Task.Delay(7000);
    Console.WriteLine("f2 finished");
}

async Task f3()
{
    await Task.Delay(2000);
    Console.WriteLine("f3 finished");
}

我们测量了三个异步方法的执行时间。

Task.WaitAll(f1(), f2(), f3());

Task.WaitAll 等待所有提供的任务完成执行。

$ dotnet run
f3 finished
f1 finished
f2 finished
elapsed: 7000 ms

C# Task.ContinueWith

Task.ContinueWith 创建一个延续,当目标 Task<TResult> 完成时,该延续异步执行。

Program.cs
Task<int> task = Task.Run(() => 
    runTask()).ContinueWith<int>((x) => x.Result * 2);
var res = await task;

Console.WriteLine(res);

int runTask()
{
    int x = 1;
    int y = 2;
    int z = 3;

    Thread.Sleep(1000);
    return x + y + z;
}

在该示例中,我们使用 ContinueWith 链接两个操作。

C# 多个异步请求

HttpClient 类用于发送 HTTP 请求并从指定的资源接收 HTTP 响应。

Program.cs
var urls = new string[] { "http://webcode.me", "http://example.com",
    "http://httpbin.org", "https://ifconfig.me", "http://termbin.com",
    "https://github.com"
};

using var client = new HttpClient();

var tasks = new List<Task<HttpResponseMessage>>();

foreach (var url in urls)
{
    tasks.Add(client.GetAsync(url));
}

Task.WaitAll(tasks.ToArray());

var data = new List<HttpResponseMessage>();

foreach (var task in tasks)
{
    data.Add(await task);
}

foreach (var res in data)
{
    Console.WriteLine(res.StatusCode);
}

我们向各种网页发送异步 GET 请求,并获取它们的响应状态代码。

tasks.Add(client.GetAsync(url));

GetAsync 向指定的 url 发送一个 GET 请求,并在一个异步操作中返回响应体。 它返回一个新的任务。 该任务被添加到任务列表中。

Task.WaitAll(tasks.ToArray());

Task.WaitAll 等待所有提供的任务完成执行。

data.Add(await task);

await 解包操作的结果。

foreach (var res in data)
{
    Console.WriteLine(res.StatusCode);
}

我们打印每个请求的状态。

$ dotnet run
OK
OK
OK
OK
OK
OK

来源

Task 类 - 语言参考

在本文中,我们使用了 Task 在 C# 中进行并发操作。

作者

我叫 Jan Bodnar,我是一位充满激情的程序员,拥有丰富的编程经验。 自 2007 年以来,我一直在撰写编程文章。 迄今为止,我撰写了 1,400 多篇文章和 8 本电子书。 我拥有超过十年的编程教学经验。

列出所有 C# 教程