问题

C# 中如何在不使用 async和await 关键字的情况下构建一个按照顺序执行的 Task 集合?

回答
在 C 中,构建一个按照顺序执行的任务集合,而无需 `async` 和 `await` 关键字,这其实是通过巧妙地利用 `Task` 对象的链式调用来实现的。虽然 `async/await` 是目前处理这类问题的最直观和推荐的方式,但在某些特定场景下,或者为了理解底层的任务调度机制,我们也可以回归到更基础的 `Task` API。

核心思想在于,让前一个任务的完成状态和结果,作为下一个任务的启动条件。这种“传递”的感觉,就像你在流水线上工作,只有上一道工序完成后,你才能开始处理当前的产品。

让我们详细拆解一下如何做到这一点:

1. 理解 Task 的生命周期和 Continuation

首先,我们要明白 `Task` 对象代表的是一个异步操作。一个 `Task` 会经历几个状态:`Created`(创建)、`Running`(运行中)、`RanToCompletion`(成功完成)、`Faulted`(出错)、`Canceled`(已取消)。

当我们说“按照顺序执行”时,我们希望的是:

任务 B 必须在任务 A 完成后 才开始执行。
如果任务 A 失败了,我们可能需要处理错误,并且 不启动 任务 B。
如果任务 A 成功完成了,并且可能返回一个结果,任务 B 也许会 依赖 这个结果。

C 的 `Task` 类提供了 `ContinueWith` 方法。这个方法正是我们实现顺序执行的“秘密武器”。`ContinueWith` 允许你注册一个回调函数,该函数会在调用 `ContinueWith` 的那个 `Task` 完成时执行。

`ContinueWith` 的签名通常是这样的:

```csharp
public Task ContinueWith(Action continuationAction);
public Task ContinueWith(Func continuationFunction);
public Task ContinueWith(Action continuationAction, TaskContinuationOptions options);
// ... 还有其他重载,用于指定 TaskScheduler, CancellationToken 等
```

这里的关键点在于 `continuationAction`(或 `continuationFunction`)。它接收一个 `Task` 参数,这个参数就是触发这个回调的那个任务。通过检查这个传入的任务的状态(例如 `task.IsCompletedSuccessfully`, `task.IsFaulted`, `task.IsCanceled`),我们就可以控制后续任务的执行逻辑。

2. 构建顺序执行的 Task 链

让我们假设我们要按顺序执行三个任务:`Task1`、`Task2`、`Task3`。

第一步:定义第一个任务

我们先创建一个 `Task`。通常,这是一个启动我们整个链的起点。

```csharp
// 模拟一个耗时操作,返回一个字符串
Task task1 = Task.Run(() => {
Console.WriteLine("Task 1 started.");
Thread.Sleep(1000); // 模拟工作
Console.WriteLine("Task 1 finished.");
return "Result from Task 1";
});
```

第二步:为 Task1 添加 Task2 的 Continuation

现在,我们要在 `task1` 完成后执行 `task2`。使用 `ContinueWith`:

```csharp
Task task2 = task1.ContinueWith(previousTask => {
// previousTask 就是 task1
if (previousTask.IsCompletedSuccessfully)
{
string resultFromTask1 = previousTask.Result; // 获取 task1 的结果
Console.WriteLine($"Task 2 received: '{resultFromTask1}'. Starting Task 2.");
Thread.Sleep(1500); // 模拟工作
Console.WriteLine("Task 2 finished.");
return "Result from Task 2";
}
else
{
// 如果 task1 失败或被取消,则不执行 task2 的主要逻辑
Console.WriteLine($"Task 1 did not complete successfully. Status: {previousTask.Status}");
// 我们可以选择返回一个默认值,或者抛出异常,取决于我们的错误处理策略。
// 为了让链继续,但标志着一个非成功路径,返回null或抛出异常是常见的。
// 如果我们想让链中断,不创建并返回一个新的 Task 也是一种方式。
// 为了示例清晰,我们在这里返回一个标记性的结果,并停止执行后续逻辑。
// 实际上,如果task1失败,且我们不希望task2执行,那task2的continuation本身就不应返回一个新的task。
// 如果Task Continuation抛出异常,该异常会被捕获并包装在返回的Task中。
throw new AggregateException("Task 1 failed, so Task 2 is skipped.");
}
});
```

重要说明:`ContinueWith` 的返回值

`task1.ContinueWith(...)` 返回的是一个新的 `Task` 对象,这个新的 `Task` 代表了 `task1` 完成后 执行 Continuation 逻辑 的整个过程。

如果 `continuationAction` 返回 `void`(即 `Action`),那么 `ContinueWith` 返回的 `Task` 类型就是 `Task`。
如果 `continuationAction` 返回一个 `TResult`(即 `Func`),那么 `ContinueWith` 返回的 `Task` 类型就是 `Task`。

在上面的例子中,`task2` 的 `continuationAction` 返回的是 `string`,所以 `task2` 的类型是 `Task`。

第三步:为 Task2 添加 Task3 的 Continuation

同理,为 `task2` 添加 `task3` 的 Continuation:

```csharp
Task task3 = task2.ContinueWith(previousTask => {
// previousTask 就是 task2
if (previousTask.IsCompletedSuccessfully)
{
string resultFromTask2 = previousTask.Result; // 获取 task2 的结果
Console.WriteLine($"Task 3 received: '{resultFromTask2}'. Starting Task 3.");
Thread.Sleep(500); // 模拟工作
Console.WriteLine("Task 3 finished.");
return "Result from Task 3";
}
else
{
Console.WriteLine($"Task 2 did not complete successfully. Status: {previousTask.Status}. Skipping Task 3.");
// 同样,处理上一个任务可能发生的失败
throw new AggregateException("Task 2 failed, so Task 3 is skipped.");
}
});
```

3. 处理错误和取消

`ContinueWith` 的一个重要特性是可以指定 `TaskContinuationOptions`。这允许我们更精细地控制何时执行 Continuation,特别是关于前一个任务的状态。

例如,只在前一个任务成功完成时才执行:

```csharp
Task task2_onlyOnSuccess = task1.ContinueWith(previousTask => {
// ... 之前的 Task 2 逻辑 ...
}, TaskContinuationOptions.OnlyOnRanToCompletion);
```

或者,只在前一个任务失败时执行:

```csharp
Task task2_onlyOnFaulted = task1.ContinueWith(previousTask => {
Console.WriteLine($"Task 1 failed. Exception: {previousTask.Exception.InnerException.Message}");
// 可以记录日志,或者尝试恢复
}, TaskContinuationOptions.OnlyOnFaulted);
```

如果我们不指定选项,默认的 `TaskContinuationOptions.None` 会让 Continuation 在任务完成(无论是成功、失败还是取消)时都可能执行。所以在 `continuationAction` 内部检查 `previousTask.IsCompletedSuccessfully` 是非常重要的,否则你可能会尝试访问一个已经失败的任务的 `Result`,从而抛出 `AggregateException`。

4. 启动和等待整个链

我们已经构建了一个链,但需要一个起点。`Task.Run` 是一个常用的启动方式。然后,我们通常需要等待整个链完成。

```csharp
// 假设我们已经有了 task3

// 等待整个链完成。task3 是链的最后一个任务。
// Task.WaitAll() 接收一个 Task 数组。
// 实际上,如果我们只关心最后一个任务,等待最后一个任务即可。
// 如果需要确保所有中间任务的 Continuation 也已执行完毕(即使它们返回了新的 Task),
// 并且我们想以链的末端来表示整个操作的完成,那么等待最后一个 Task 是合理的。
try
{
task3.Wait(); // 等待 task3 完成,这将间接触发它之前的 Continuation
Console.WriteLine("All tasks completed sequentially.");
Console.WriteLine($"Final result: {task3.Result}");
}
catch (AggregateException ae)
{
Console.WriteLine("An error occurred during task execution:");
foreach (var ex in ae.Flatten().InnerExceptions)
{
Console.WriteLine($" {ex.Message}");
}
}
```

5. 链式调用 `ContinueWith` 的好处与缺点

好处:

明确的顺序控制: `ContinueWith` 的链式调用提供了非常直观的顺序执行模型,每个任务的启动都明确依赖于前一个任务的完成。
结果传递: 可以在 Continuation 中访问前一个任务的结果,实现数据在任务间的流动。
错误处理: 通过检查前一个任务的状态(`IsCompletedSuccessfully`, `IsFaulted` 等)可以在链中实现条件化的错误处理和跳过。

缺点:

嵌套和复杂性: 如果链很长,或者有复杂的条件逻辑,`ContinueWith` 的嵌套会变得难以阅读和维护,容易形成“回调地狱”(callback hell)的某种变体,尽管它比传统的嵌套回调更结构化。
异常处理的复杂性: `ContinueWith` 返回的 `Task` 会包装其 Continuation 中抛出的异常。捕获这些异常通常需要处理 `AggregateException`,这不像 `async/await` 中直接的 `trycatch` 那样直观。
没有 `return` 语句的“自由”: 在 `async` 方法中,`return` 语句自然地结束了方法的执行并返回了任务结果。在 `ContinueWith` 中,你需要显式地返回值,或者返回 `Task.FromResult()` 或 `Task.FromException()`。
同步阻塞的可能性: 如果你在主线程上直接调用 `.Wait()` 或 `.Result`,可能会导致 UI 线程或其他关键线程的阻塞,这是 `async/await` 极力避免的。在后台线程上使用 `.Wait()` 或 `.Result` 则相对安全,但通常我们期望异步操作不阻塞。

总结

通过 `Task.Run` 启动第一个任务,然后使用 `ContinueWith` 将后续的任务连接起来,并在 Continuation 中根据前一个任务的状态(特别是 `IsCompletedSuccessfully`)来决定是否执行,以及如何处理其结果,我们就可以构建一个不依赖 `async/await` 关键字的、按照顺序执行的任务集合。这种方式本质上是手动构建了 `async/await` 在底层可能进行的一些操作。虽然不如 `async/await` 简洁,但在理解任务的生命周期和 Continuation 机制方面,它是一个非常好的学习途径。

网友意见

user avatar

其实把你的那个tasks.Add

改成:

       task = task.ContinueWith( () => UploadChunk(i * chunkSize, (i + 1) * chunkSize, fileData) )     

不就行了,,,,

当然,这个函数直接执行就好了,不要返回个Task啥的。

类似的话题

本站所有内容均为互联网搜索引擎提供的公开搜索信息,本站不存储任何数据与内容,任何内容与数据均与本站无关,如有需要请联系相关搜索引擎包括但不限于百度google,bing,sogou

© 2025 tinynews.org All Rights Reserved. 百科问答小站 版权所有