在C#的进阶开发中,Task并行库(TPL)是处理并行任务的核心组件,它基于任务调度器实现了高效的多核资源利用,相比传统的Thread多线程方式,TPL提供了更简洁的API和更完善的任务管理机制,能够帮助开发者更轻松地实现复杂的并行逻辑。
Task并行库的基础进阶用法
基础的Task创建方式大家都很熟悉,进阶场景下我们更常用Task.Run来启动后台任务,同时结合任务延续来处理后续逻辑。比如我们需要先执行一个数据拉取任务,拉取完成后再处理数据,就可以用下面的方式实现。
using System;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
// 启动第一个任务,模拟数据拉取
Task<string> fetchTask = Task.Run(() =>
{
// 模拟耗时操作
System.Threading.Thread.Sleep(2000);
return "用户数据列表";
});
// 任务延续,处理拉取到的数据
Task processTask = fetchTask.ContinueWith(prevTask =>
{
string data = prevTask.Result;
Console.WriteLine($"处理数据:{data}");
}, TaskScheduler.Default);
await processTask;
}
}
这里需要注意,ContinueWith方法默认使用的任务调度器可能不是当前上下文的调度器,所以如果是在UI线程中使用,需要指定合适的TaskScheduler,避免跨线程操作问题。
并行循环的使用场景
当我们需要对大量数据执行相同的独立操作时,使用Parallel类提供的并行循环比普通循环效率更高。TPL会自动将循环任务拆分到多个核心上执行,不需要我们手动管理线程。
比如我们需要处理1000条数据,每条数据的处理互不干扰,就可以用Parallel.For来实现:
using System;
using System.Threading.Tasks;
class Program
{
static void Main()
{
int totalCount = 1000;
// 并行循环处理数据
Parallel.For(0, totalCount, i =>
{
// 模拟单条数据的处理逻辑
System.Threading.Thread.Sleep(10);
Console.WriteLine($"处理第{i}条数据");
});
}
}
如果循环过程中需要共享一些局部状态,比如统计处理成功的数量,可以使用ParallelLoopState和线程本地存储来避免资源竞争:
using System;
using System.Threading.Tasks;
class Program
{
static void Main()
{
int successCount = 0;
object lockObj = new object();
Parallel.For(0, 1000, () => 0, (i, state, localCount) =>
{
// 模拟处理成功
localCount++;
return localCount;
}, (localCount) =>
{
// 合并本地计数到全局计数
lock (lockObj)
{
successCount += localCount;
}
});
Console.WriteLine($"总处理成功数量:{successCount}");
}
}
任务组合与取消机制
实际开发中经常需要等待多个任务全部完成或者任意一个完成,TPL提供了Task.WhenAll和Task.WhenAny方法来实现这类需求。同时,我们可以通过CancellationToken来实现任务的取消逻辑,避免任务无法终止导致资源浪费。
下面的示例展示了等待多个任务完成,并且支持取消的场景:
using System;
using System.Threading;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
var cts = new CancellationTokenSource();
// 2秒后触发取消
cts.CancelAfter(2000);
Task task1 = Task.Run(() =>
{
for (int i = 0; i < 5; i++)
{
cts.Token.ThrowIfCancellationRequested();
System.Threading.Thread.Sleep(1000);
Console.WriteLine("任务1执行步骤" + i);
}
}, cts.Token);
Task task2 = Task.Run(() =>
{
for (int i = 0; i < 5; i++)
{
cts.Token.ThrowIfCancellationRequested();
System.Threading.Thread.Sleep(1000);
Console.WriteLine("任务2执行步骤" + i);
}
}, cts.Token);
try
{
// 等待所有任务完成
await Task.WhenAll(task1, task2);
Console.WriteLine("所有任务执行完成");
}
catch (OperationCanceledException)
{
Console.WriteLine("任务被取消");
}
}
}
并行编程中的异常处理
TPL中任务的异常不会直接抛出到调用线程,而是会被封装到AggregateException中,当等待任务完成或者访问任务结果时才会抛出。我们需要在代码中合理捕获和处理这些异常,避免程序崩溃。
处理多个任务的异常时,可以遍历AggregateException的InnerExceptions属性来获取所有异常信息:
using System;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
Task task1 = Task.Run(() =>
{
throw new InvalidOperationException("任务1发生异常");
});
Task task2 = Task.Run(() =>
{
throw new NullReferenceException("任务2发生异常");
});
try
{
await Task.WhenAll(task1, task2);
}
catch (Exception ex)
{
if (ex is AggregateException aggEx)
{
foreach (var innerEx in aggEx.InnerExceptions)
{
Console.WriteLine($"捕获到异常:{innerEx.Message}");
}
}
else
{
Console.WriteLine($"捕获到异常:{ex.Message}");
}
}
}
}
进阶场景下的任务调度优化
TPL默认使用的是线程池任务调度器,但是在一些特殊场景下,我们可以自定义任务调度器来满足特定需求,比如限制并行任务的最大数量,避免过多任务同时执行导致系统资源耗尽。
自定义任务调度器需要继承TaskScheduler类,重写相关方法,不过大多数场景下我们不需要自己实现,可以使用ConcurrentExclusiveSchedulerPair来创建限制并发数的调度器:
using System;
using System.Threading.Tasks;
class Program
{
static void Main()
{
// 限制最大并行数为2
var schedulerPair = new ConcurrentExclusiveSchedulerPair(TaskScheduler.Default, maxConcurrency: 2);
var limitedScheduler = schedulerPair.ConcurrentScheduler;
for (int i = 0; i < 10; i++)
{
int index = i;
Task.Factory.StartNew(() =>
{
Console.WriteLine($"任务{index}开始执行");
System.Threading.Thread.Sleep(2000);
Console.WriteLine($"任务{index}执行完成");
}, CancellationToken.None, TaskCreationOptions.None, limitedScheduler);
}
Console.ReadLine();
}
}
通过上面的方式,即使启动了10个任务,同时执行的任务最多也只有2个,能够有效控制资源占用。
并行编程的注意事项
虽然TPL能够提升程序效率,但并不是所有场景都适合使用并行编程。如果任务本身的执行时间很短,或者任务之间存在大量的资源竞争,那么并行反而会降低效率,甚至引发线程安全问题。在使用TPL时,我们需要先评估任务的独立性,尽量减少共享资源的访问,必须共享时也要做好同步措施,比如使用lock、SemaphoreSlim等同步工具。
另外,异步方法和并行任务不要混淆,async/await主要解决的是IO阻塞问题,而TPL主要解决的是CPU密集型任务的并行执行问题,两者可以结合使用,但需要根据场景合理选择。