1. 概述

在计算机科学中,存在多种可以在多处理器计算机上运行的并行算法,例如多线程算法。

在本教程中,我们将了解这种并行计算模型的含义,并通过一个具体示例来加深理解。


2. 动态多线程概念

多线程是现代计算中一个关键主题。并行计算模型有多种,彼此之间存在显著差异。例如,内存可以是共享的,也可以是分布式的。由于多核处理器已经普及,我们主要关注基于共享内存的并行计算模型。

2.1 动态与静态多线程

  • 静态多线程:程序员需要在程序执行的每个阶段预先指定使用多少处理器。这种方式在面对动态变化的条件时缺乏灵活性。
  • 动态多线程:程序员只需指出潜在的并行机会,而具体的线程调度由并发平台自动管理。

2.2 并发构造

并发平台是一个软件层,负责调度、管理和协调并行计算资源。我们使用一个简单的串行编程模型扩展来反映当前的并行实践:

  • spawn:父线程可以与子线程并行执行,而不是等待子线程完成
  • sync:线程必须等待所有 spawned 子线程完成后再继续执行
  • parallel:用于指示每个迭代都可以并行执行(如 parallel for

⚠️ 注意parallelspawn 关键字并不代表强制并行,而是表示“逻辑并行性”。当使用并行性时,必须尊重 sync。每个过程在末尾都有一个隐式的 sync,以确保安全性。


3. 示例:并行斐波那契数列计算

我们来看一个原本效率较低的斐波那契数列计算算法,并尝试将其并行化。

斐波那契数列定义如下:

$$ \begin{cases} F_{0} = 0 \ F_{1} = 1 \ F_{i} = F_{i-1} + F_{i-2}, \text{ for } i \geq 2 \end{cases} $$

以下是基于该定义的非并行算法:

algorithm FIB(n):
    if n <= 1:
        return n
    else:
        x <- FIB(n - 1)
        y <- FIB(n - 2)
        return x + y

这是该算法的递归调用树:

Fibonacci recursion tree

如果我们使用并发关键字来并行执行两个递归调用,可以得到如下改进版本:

algorithm P-FIB(n):
    if n <= 1:
        return n
    else:
        x <- spawn P-FIB(n - 1)  // 并行执行
        y <- spawn P-FIB(n - 2)  // 并行执行
        sync  // 等待结果
        return x + y

4. 动态多线程建模

为了更好地描述并行计算过程,我们需要一个形式化的模型。

4.1 计算 DAG(有向无环图)

我们可以使用计算 DAG(Directed Acyclic Graph)来建模多线程计算 $ G = (V, E) $:

  • 顶点(V):代表指令。为简化模型,我们把每个顶点视为一个“strand”(不包含并行控制语句的指令序列)
  • 边(E):代表指令之间的依赖关系。边 $ (u, v) \in E $ 表示指令 $ u $ 必须在 $ v $ 之前执行

如果两个线程之间有边,则它们是串行关系;否则是并行关系

4.2 边的分类

  • 延续边(Continuation edge):表示同一个过程中的指令顺序执行
  • spawn 边(Spawn edge):表示一个线程 spawn 了另一个线程
  • 返回边(Return edge):表示子线程执行完毕后返回到调用线程

4.3 模型可视化

我们来看一下使用线程的并行斐波那契算法:

algorithm P-FIB(n):
    if n <= 1:
        return n  // Thread A
    else:
        x <- spawn P-FIB(n - 1) // Thread A continues
        y <- spawn P-FIB(n - 2) // Thread B
        sync // wait for results
        return x + y  // Thread C combines results

这是 P-FIB(4) 的 DAG 模型图:

P-FIB(4) DAG

如图所示,线程 4 spawn 线程 3,线程 3 spawn 线程 2,线程 2 spawn 线程 1,之后通过返回边依次返回结果,最终合并到线程 4。


5. 多线程性能度量

接下来我们介绍几个衡量动态多线程性能的重要指标。

5.1 Work(工作量)

Work 表示整个计算在单个处理器上执行所需的总时间。它等于所有线程执行时间的总和。

在一个拥有 $ P $ 个处理器的理想并行计算机上,单位时间内最多可完成 $ P $ 单位的工作。因此:

$$ T_P \geq \frac{T_1}{P} $$

这就是所谓的工作定律

5.2 Span(跨度)

Span 是计算 DAG 中最长路径的执行时间,即从起点到终点所需的最大时间。

理想并行计算机无法比无限处理器机器运行得更快。因此:

$$ T_P \geq T_\infty $$

这就是所谓的跨度定律

5.3 加速比与并行度

  • 加速比:$ \frac{T_1}{T_P} $,表示在 $ P $ 个处理器上比单个处理器快多少
  • 并行度:$ \frac{T_1}{T_\infty} $,表示平均每个并行执行步骤可以完成多少工作

根据工作定律,理想并行计算机的加速比不能超过处理器数量。

5.4 松弛度(Slackness)

松弛度定义为:

$$ \text{Slackness} = \frac{T_1 / T_\infty}{P} $$

它表示在给定处理器数量下,并行性是否被充分利用。

5.5 性能度量示例

以 P-FIB(4) 为例,DAG 中有 17 个线程(顶点),最长路径上有 8 个线程(红色路径)。

Fibonacci DAG

假设每个线程执行时间为 1 单位:

  • Work:17 单位
  • Span:8 单位

进一步分析:

  • **Work $ T_1 $**:$ \theta\left(\left(\frac{1+\sqrt{5}}{2}\right)^n\right) $
  • **Span $ T_\infty $**:$ \theta(n) $
  • 并行度:$ \theta\left(\frac{(\frac{1+\sqrt{5}}{2})^n}{n}\right) $,随着 $ n $ 增大迅速增长

6. 其他并行算法示例

6.1 并行矩阵乘法算法

使用嵌套的 parallel for 来并行化矩阵乘法:

algorithm P-Matrix-Multiply(A, B):
    n <- number of rows in A
    C <- initialize an empty n x n matrix
    parallel for i <- 1 to n:
        parallel for j <- 1 to n:
            c[i, j] <- 0
            for k <- 1 to n:
                c[i, j] <- c[i, j] + a[i, k] * b[k, j]
    return C
  • Work:$ \theta(n^3) $
  • Span:$ \theta(n) $
  • 并行度:$ \theta(n^2) $

6.2 并行归并排序算法

归并排序非常适合并行化,因为其分治结构天然支持子问题并行:

algorithm Merge-Sort(a, p, r):
    if p < r:
        q <- (p + r) / 2
        spawn Merge-Sort(a, p, q)
        Merge-Sort(a, q+1, r)
        sync
        Merge(a, p, q, r)
  • Work:$ \theta(n \log n) $
  • Span:$ \theta(n) $
  • 并行度:$ \theta(\log n) $

⚠️ 踩坑提醒:归并操作是串行的,这限制了整体并行度。若想提升性能,可以尝试并行化归并步骤。


7. 多线程应用场景

多线程技术广泛应用于以下场景:

  • ✅ 数据集的并发访问
  • ✅ Web 服务器在监听端口的同时处理新连接
  • ✅ 在后台线程加密文件,同时主线程保持应用运行

8. 小结

在本教程中,我们通过斐波那契数列和矩阵乘法等示例,介绍了动态多线程算法的基本概念和建模方法(DAG)。我们还分析了 Work、Span、加速比、并行度等性能指标,并讨论了它们在实际应用中的意义。

如果你正在开发高性能并行系统,理解这些概念将帮助你更好地设计算法、评估性能瓶颈,并做出更优的架构决策。


原始标题:Multithreaded Algorithms