所有分类
  • 所有分类
  • 游戏源码
  • 网站源码
  • 单机游戏
  • 游戏素材
  • 搭建教程
  • 精品工具

用.NET标准库实现多任务并行处理:超详细过程全程拆解与实战

用.NET标准库实现多任务并行处理:超详细过程全程拆解与实战 一

文章目录CloseOpen

我们不用第三方依赖,全程基于.NET原生API:从Task的创建与等待、Parallel类的批量并行,到任务调度器的配置、多任务结果的合并,每一步都附具体代码示例;更聚焦实战痛点——比如如何用lock或Concurrent集合解决资源竞争,怎样根据CPU核心数调整任务分区,甚至是用CancellationToken优雅终止任务。不管你是刚接触并行的新手,还是想优化现有逻辑的老司机,跟着这篇走,就能快速掌握用.NET标准库写稳定、高效并行代码的方法,把程序的“跑速”提上去。

你有没有过这种情况?写.NET程序时,明明CPU还有大半空闲,一堆任务却排着队单线程执行,比如处理1000条库存数据要5分钟,导出10万条订单Excel要等半小时?我去年帮做电商系统的朋友调优时,就碰到过这糟心事儿——原来的单线程逻辑把CPU闲得慌,改完并行处理后,库存同步直接降到1分钟,Excel导出快了4倍。今天我把当时摸透的.NET标准库并行玩法拆开来讲,不用第三方库,纯原生API,你跟着做就能把并行逻辑搭起来,踩过的坑我帮你绕开。

第一步:先搞懂.NET里的“任务”——别再把Task当Thread用

我之前也犯过傻:以为Task就是开线程,直到查了微软官方文档(https://learn.microsoft.com/zh-cn/dotnet/standard/parallel-programming/task-parallel-library-tpl?nofollow)才明白,Task是“要做的事”,Thread是“做事的人”——调度器会帮你把任务分配给合适的线程,不用自己手动管理线程池。比如你要下载10个图片,用Task.Run()启动10个任务,调度器会根据CPU核心数(比如8核)决定同时跑8个还是16个,不会因为开太多线程导致上下文切换的开销拖慢程序。

朋友的库存同步功能原来的代码是这样的:

foreach (var item in stockItems)

{

UpdateStock(item); // 单线程循环,每个任务等前一个结束

}

我帮他改成并行后,用Task.WhenAll()把所有任务“打包”执行:

var tasks = new List();

foreach (var item in stockItems)

{

tasks.Add(Task.Run(() => UpdateStock(item))); // 每个任务独立启动

}

await Task.WhenAll(tasks); // 等所有任务完成

就这么几行代码,执行时间从5分钟缩到50秒——不是我多厉害,是.NET的任务调度器帮了大忙,它会自动把任务分配给空闲的线程,不用自己去调线程池参数。

但你别以为Task.Run()是万能的!我之前帮朋友改数据库同步功能时,一开始把所有SQL操作都包进Task.Run(),结果反而变慢了——后来才反应过来:数据库连接池是有限的,并行100个任务会导致连接排队等待,反而不如单线程快。所以得先分清楚任务类型:

  • IO密集型任务(下载文件、调用API、读数据库):适合用Task.Run(),因为任务大部分时间在等IO响应,CPU闲着也是闲着;
  • CPU密集型任务(大量计算、数据加密):得控制并行度,比如用Parallel.ForEach()并设置MaxDegreeOfParallelism(比如设为CPU核心数的2倍),避免CPU被占满导致其他功能超时。
  • 第二步:并行处理的核心技巧——搞定调度、结果和异常

    光会开任务还不够,我帮朋友调优时踩过最大的坑是“任务乱串”:比如导出Excel时,并行任务把CPU占满,导致用户下单接口超时;或者某个任务抛异常,整个并行流程全崩了。这一步我把压箱底的技巧拆给你——

  • 用调度器控制并行度,别让CPU“过载”
  • 朋友的Excel导出功能要处理10万条订单,一开始用Parallel.ForEach()直接跑,结果CPU使用率瞬间拉到100%,其他接口全超时。后来我帮他加了个自定义任务调度器,限制并行度为CPU核心数的2倍(比如8核CPU设16):

    // 自定义调度器(可参考微软官方示例)
    

    public class LimitedConcurrencyLevelTaskScheduler TaskScheduler

    {

    private readonly int _maxConcurrencyLevel;

    public LimitedConcurrencyLevelTaskScheduler(int maxConcurrencyLevel)

    {

    _maxConcurrencyLevel = maxConcurrencyLevel;

    }

    // 省略调度逻辑...

    }

    // 使用调度器启动任务

    var scheduler = new LimitedConcurrencyLevelTaskScheduler(Environment.ProcessorCount * 2);

    var factory = new TaskFactory(scheduler);

    var tasks = orders.Select(order => factory.StartNew(() => ProcessOrder(order)));

    await Task.WhenAll(tasks);

    这么一改,CPU使用率稳定在60%左右,Excel导出时间从30分钟降到8分钟,其他接口也不超时了——调度器就像“任务管家”,帮你把任务分配得恰到好处。

  • 怎么收集并行任务的结果?别再用锁存List了
  • 我之前做图片压缩功能时,一开始用List存压缩后的图片,为了线程安全加了lock

    var compressedImages = new List();
    

    var lockObj = new object();

    var tasks = images.Select(img => Task.Run(() => {

    var compressed = CompressImage(img);

    lock(lockObj) compressedImages.Add(compressed); // 加锁存结果

    }));

    await Task.WhenAll(tasks);

    结果发现lock会导致任务排队,反而慢了——后来才知道,Task.WhenAll()本身就能返回所有任务的结果,根本不用加锁:

    var tasks = images.Select(img => Task.Run(() => CompressImage(img)));
    

    var compressedImages = await Task.WhenAll(tasks); // 直接拿到结果数组

    是不是简单多了?Task.WhenAll()会把所有任务的结果装进一个数组,顺序和原任务一致,不用自己手动管理线程安全。

  • 异常别“一锅端”——单独捕获每个任务的错误
  • 我去年做物流轨迹同步时,有个任务因为网络波动抛出了HttpRequestException,结果整个Task.WhenAll()都失败了,其他99个成功的任务也白跑了。后来我学聪明了,用包装方法单独捕获每个任务的异常

    // 定义一个包装任务,返回“结果或异常”的对象
    

    private async Task> TrySyncTrack(LogisticsTrack track)

    {

    try

    {

    var result = await SyncTrackAsync(track);

    return new ResultOrError(result);

    }

    catch (Exception ex)

    {

    return new ResultOrError(ex);

    }

    }

    // 并行执行所有任务,收集结果和异常

    var tasks = logisticsTracks.Select(track => TrySyncTrack(track));

    var results = await Task.WhenAll(tasks);

    // 分开处理成功和失败的任务

    var successfulTracks = results.Where(r => r.IsSuccess).Select(r => r.Result).ToList();

    var failedTracks = results.Where(r => !r.IsSuccess).Select(r => new { Track = r.Input, Error = r.Error }).ToList();

    这样即使某个任务出错,其他任务的结果也能保留,还能把失败的任务单独拎出来重试——朋友说这个技巧帮他省了好多人工核对的时间。

    附:不同并行方式的适用场景对比

    我把常用的并行方法整理成了表格,你可以直接对照着选:

    并行方式 适用场景 优点 注意事项
    Task.Run() + Task.WhenAll() IO密集型任务(下载、API调用) 代码简单,无需手动管线程 控制并行度,避免资源竞争(如数据库连接池)
    Parallel.ForEach() CPU密集型任务(计算、数据处理) 自动分区,利用多核效率高 设MaxDegreeOfParallelism防CPU过载
    TaskFactory + 自定义调度器 需精确控制并行度的核心功能(如Excel导出) 灵活控制任务执行顺序 需自己实现调度器(参考微软示例)

    其实并行处理没你想的那么复杂——关键是搞懂“任务是要做的事,调度器是分配事的人”,避开资源竞争的坑,用对方法收集结果和异常。我去年帮朋友改完代码,他说省下来的时间能多陪孩子读绘本了;我自己做用户画像分析时,用这些技巧把100万条用户数据的处理时间从2小时缩到20分钟。

    你要是按这些步骤改了代码,欢迎回来告诉我效果——比如你之前处理数据要多久,改了之后快了多少?说不定你也能像我朋友那样,省下时间做更重要的事。并行处理不是“炫技”,是帮你把CPU的潜力挖出来,试试就知道了!


    用.NET做并行处理时,Task和Thread有什么不一样?

    其实Task更像“要做的事”,Thread是“做事的人”——你不用自己手动管线程,.NET的调度器会帮你把Task分配给合适的线程。比如你要下载10个图片,用Task.Run()启动10个任务,调度器会根据CPU核心数(比如8核)决定同时跑多少个,不会因为开太多线程导致上下文切换慢。而Thread是直接开线程,得自己管线程池,容易因为线程太多拖慢程序,我之前就犯过这傻,直到查了微软文档才明白。

    举个例子,朋友的库存同步原来用单线程循环,改成交叉任务后,每个任务独立启动,调度器自动分配线程,结果时间从5分钟缩到50秒——不是我厉害,是Task的调度机制帮了大忙。

    并行处理时多个任务改同一个List,总出现资源竞争怎么办?

    我之前踩过这坑:压缩图片时用List存结果,为了安全加了lock,结果任务排队变慢。后来才发现不用锁——用Task.WhenAll()就能直接收集所有任务的结果!比如每个任务返回压缩后的字节数组,用Task.WhenAll()等所有任务完成,直接拿到结果数组,顺序和原任务一致,根本不用手动加锁。

    要是你确实需要动态存数据,也可以用ConcurrentBag这类线程安全的集合,比自己加锁更高效。比如朋友的物流轨迹同步,用ConcurrentBag存成功的轨迹,没再出现资源竞争的问题。

    并行任务太多导致CPU占满,其他接口超时怎么办?

    这得控制并行度,别让CPU“过载”。比如用Parallel.ForEach()时,可以设MaxDegreeOfParallelism参数,比如CPU是8核,就设成16(核心数的2倍),这样CPU使用率不会拉满。我帮朋友调Excel导出时,一开始没设这个参数,CPU瞬间到100%,其他接口全超时,设成16后,CPU稳定在60%,导出时间从30分钟降到8分钟。

    要是需要更精确的控制,还能自定义任务调度器——比如写个LimitedConcurrencyLevelTaskScheduler,限制最多同时跑16个任务,调度器会帮你分配任务,既利用多核又不影响其他功能。

    并行任务中有一个出错,整个流程都崩了,怎么保留其他任务的结果?

    可以给每个任务加个“包装”,单独捕获异常。比如同步物流轨迹时,我写了个TrySyncTrack方法,里面用try-catch包着同步逻辑,返回“结果或异常”的对象。所有任务并行执行后,用Task.WhenAll()收集所有结果,再分开处理成功和失败的——成功的拿结果,失败的记下来重试。

    这样就算某个任务因为网络波动出错,其他99个成功的任务结果也能保留,不会白跑。我去年做这个功能时,用这方法把失败率从20%降到了1%,省了好多人工核对的时间。

    .NET里Task.Run、Parallel.ForEach、自定义调度器,分别适合什么场景?

    得看任务类型:如果是IO密集型(比如下载文件、调用API、读数据库),用Task.Run()就行,因为任务大部分时间在等IO,CPU闲着也是闲着;如果是CPU密集型(比如大量计算、数据加密),用Parallel.ForEach()并设MaxDegreeOfParallelism,避免CPU占满;要是需要精确控制并行度(比如核心功能如Excel导出),就用TaskFactory加自定义调度器,灵活调整并行数。

    比如我做用户画像分析时,用Parallel.ForEach()处理100万条数据,设并行度为16,比单线程快了5倍;朋友的库存同步用Task.Run(),时间从5分钟缩到1分钟——选对方法比瞎折腾管用。

    原文链接:https://www.mayiym.com/50046.html,转载请注明出处。
    0
    显示验证码
    没有账号?注册  忘记密码?

    社交账号快速登录

    微信扫一扫关注
    如已关注,请回复“登录”二字获取验证码