当前位置:首页 > 技术 > 正文内容

C# 并行编程实战:掌握 Parallel.Invoke 的核心机制

访客 技术 2026年8月7日 1

在多核处理器普及的今天,并行编程已成为提升软件执行效率的关键手段。传统的多线程开发往往需要手动管理线程生命周期、处理复杂的同步与锁机制。为此,.NET 引入了任务并行库(Task Parallel Library, 简称 TPL)。TPL 将底层的线程池管理抽象为基于"任务"的模型,极大地简化了并发代码的编写。

在 TPL 中,Parallel 静态类提供了三个核心方法来应对不同的并行场景:Parallel.ForParallel.ForEach 主要用于数据并行(循环迭代),而 Parallel.Invoke 则专注于任务并行,即同时执行多个独立的操作。需要强调的是,并行化并非简单地将串行逻辑替换为并行方法,它要求开发者深入理解任务的独立性、共享状态以及线程调度开销。

Parallel.Invoke 的基础调用

Parallel.Invoke 接受一组 Action 委托,并尽可能地利用线程池并发执行它们。它主要提供两个重载版本:

public static void Invoke(params Action[] actions);
public static void Invoke(ParallelOptions parallelOptions, params Action[] actions);

在实际开发中,我们可以通过 Lambda 表达式、方法组或匿名委托来传递任务:

// 方式一:使用 Lambda 表达式
Parallel.Invoke(
    () => ExecuteTaskA(), 
    () => ExecuteTaskB(), 
    () => ExecuteTaskC()
);

// 方式二:直接传递方法组
Parallel.Invoke(ExecuteTaskA, ExecuteTaskB, ExecuteTaskC);

// 方式三:混合使用匿名委托与方法组
Parallel.Invoke(
    () => { ExecuteTaskA(); Console.WriteLine("A 完成"); },
    ExecuteTaskB,
    delegate { ExecuteTaskC(); }
);

执行特性与主线程阻塞

为了深入理解 Parallel.Invoke 的运行机制,我们来看一个基础示例。该示例展示了任务的无序性以及主线程的阻塞行为。

public class InvokeBasics
{
    public void Execute()
    {
        var stopwatch = Stopwatch.StartNew();
        Console.WriteLine($"[主线程 {Environment.CurrentManagedThreadId}] 启动并行任务...");

        Parallel.Invoke(
            () => SimulateWorkload("Alpha", 3000),
            () => SimulateWorkload("Beta", 1000),
            () => SimulateWorkload("Gamma", 2000)
        );

        stopwatch.Stop();
        Console.WriteLine($"[主线程 {Environment.CurrentManagedThreadId}] 所有任务完成,总耗时: {stopwatch.ElapsedMilliseconds} ms");
    }

    private void SimulateWorkload(string taskName, int delayMs)
    {
        Thread.Sleep(delayMs);
        Console.WriteLine($"任务 {taskName} 由线程 {Environment.CurrentManagedThreadId} 执行完毕。");
    }
}

通过运行上述代码,可以观察到以下核心特性:

  • 无序执行:子任务的执行顺序是不确定的,且它们可能被分配到不同的线程池线程中,也可能复用同一个线程。
  • 屏障语义(Barrier)Invoke 会阻塞调用它的主线程,直到所有传入的委托都执行完毕。这意味着总耗时取决于执行时间最长的那个子任务(木桶效应)。因此,在设计并行任务时,应尽量保证各个子任务的耗时相对均衡,避免单个长耗时任务拖垮整体性能。
  • 线程池托管:开发者无需手动创建和销毁线程,TPL 会根据系统负载和可用核心数自动优化线程池的调度。

并行环境下的异常处理

在并行环境中,单个子任务的异常不会直接导致整个进程崩溃,而是会被 TPL 捕获并封装。如果多个任务同时抛出异常,它们会被聚合到一个 AggregateException 中。

public class InvokeExceptionHandling
{
    public void Execute()
    {
        try
        {
            Parallel.Invoke(
                () => SimulateWorkload("Alpha", 1000),
                () => { throw new InvalidOperationException("Beta 任务发生严重错误!"); },
                () => SimulateWorkload("Gamma", 1000)
            );
        }
        catch (AggregateException aggEx)
        {
            Console.WriteLine("捕获到聚合异常:");
            foreach (var innerEx in aggEx.InnerExceptions)
            {
                Console.WriteLine($" -> {innerEx.Message}");
            }
        }
    }

    private void SimulateWorkload(string taskName, int delayMs)
    {
        Thread.Sleep(delayMs);
        Console.WriteLine($"任务 {taskName} 正常完成。");
    }
}

即使某个委托抛出了异常,TPL 仍会等待其他正在运行的任务执行完毕(或触发取消),然后将所有收集到的异常统一抛出。这种机制确保了并行任务的状态完整性,但也要求开发者必须通过遍历 InnerExceptions 来进行精细化的错误处理。

使用 ParallelOptions 进行精细化控制

默认情况下,Parallel.Invoke 会尝试最大化利用系统资源。但在某些场景下(如避免耗尽服务器 CPU 影响其他服务,或需要响应外部取消信号),我们需要通过 ParallelOptions 进行干预。

public class InvokeWithOptions
{
    private readonly CancellationTokenSource _cts = new CancellationTokenSource();

    public void Execute()
    {
        var options = new ParallelOptions
        {
            MaxDegreeOfParallelism = 2, // 限制最大并发线程数为 2
            CancellationToken = _cts.Token
        };

        // 在后台启动一个定时器,3秒后触发取消信号
        Task.Run(async () => {
            await Task.Delay(3000);
            _cts.Cancel();
            Console.WriteLine("已发出取消信号...");
        });

        try
        {
            Parallel.Invoke(options,
                () => LongRunningTask("Task-A", options),
                () => LongRunningTask("Task-B", options),
                () => LongRunningTask("Task-C", options)
            );
        }
        catch (OperationCanceledException)
        {
            Console.WriteLine("并行操作已被成功取消。");
        }
    }

    private void LongRunningTask(string taskId, ParallelOptions options)
    {
        for (int i = 0; i < 10; i++)
        {
            // 每次循环检查取消令牌
            options.CancellationToken.ThrowIfCancellationRequested();
            Thread.Sleep(500);
            Console.WriteLine($"{taskId} 正在执行... (线程: {Environment.CurrentManagedThreadId})");
        }
    }
}

在上述代码中,MaxDegreeOfParallelism 被设置为 2,这意味着即使传入了 3 个任务,同一时刻最多也只有 2 个线程在执行委托,有效控制了资源占用。同时,通过 CancellationToken,长耗时任务可以在每次迭代中检查取消状态,从而实现优雅退出,避免线程泄漏。

在实际工程中,Parallel.Invoke 的每次调用都会产生一定的调度开销。如果子任务的执行时间过短(例如几毫秒内的简单计算),并行化带来的线程切换成本可能会抵消并发带来的收益。因此,在应用此方法前,务必通过基准测试评估任务粒度,并确保子任务之间不存在隐式的共享资源竞争。

相关文章

Linux crontab 详解

1) crontab 是什么cron 是 Linux 的定时任务守护进程;crontab 是用来编辑/查看“按时间周期执行命令”的表(cron table)。常见两类:用户 crontab:每个用户一份(crontab -e 编辑)系统级 crontab / cron.d:可指定执行用户(/etc/crontab、/etc/cron.d/*)2) crontab 时间...

富文本里可以允许的 HTML 属性

一、所有标签默认允许的安全属性(极少)class        (可选)id           (通常建议禁用)title️ 注意:id 容易被滥用做锚点注入,很多系统直接禁用class 允许的话最好只允许固定前缀(如 editor-*)二、a 标签允许属性<a href="" t...

Mac 安装 Node.js 指南

方法一:通过官网安装包(最简单,适合初学者)如果你只是想快速安装并开始使用,这是最直接的方法。访问 Node.js 官网。页面会显示两个版本:LTS (Recommended For Most Users):长期支持版,最稳定。建议选这个。Current:最新特性版,包含最新功能但可能不够稳定。下载 .pkg 安装包并运行。按照安装向导点击“下一步”即可完成。方法二:使用 Homebrew 安装(...

Dom\HTML_NO_DEFAULT_NS 的副作用:自动加闭合标签

在使用Dom\HTMLDocument时,Dom\HTML_NO_DEFAULT_NS 将禁止在解析过程中设置元素的命名空间, 此设置是为了与DOMDocument向后兼容而存在的。当使用它时,已知的一个副作用就是:自动加闭合标签例如 </img> 为什么会这样?当你使用:Dom\HTML_NO_DEFAULT_NS文档会变成 无命名空间模式,此时内部更接近 XML...

Laravel 事件和监听器创建

在 Laravel 中,使用 Artisan 命令创建 Events(事件) 和 Listeners(监听器) 是非常高效的。你可以通过以下几种方式来实现:1. 手动创建单个 Event如果你只想创建一个事件类,可以使用 make:event 命令:Bashphp artisan make:event UserRegistered执行后,文件将生成在 app/Even...

linux screen 用法详情 (nohup 的替代方案)

一、screen 是什么?能干嘛?screen 是一个终端复用器,可以:在一个 SSH 会话中开多个“虚拟终端”SSH 断线后,程序仍然在后台运行随时重新连接到原来的会话特别适合:nohup 的替代方案跑脚本 / 爬虫 / 训练模型运维、远程开发二、安装 screen# CentOS / Rocky / Almayum install -y screen# Debian / Ubuntuapt i...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。