37、OpenMP 并行编程深入解析

OpenMP 并行编程深入解析

1. OpenMP 执行环境例程

OpenMP 库提供了多个执行环境例程,可用于查询和控制并行执行环境。以下是几个重要的例程:
- 动态线程调整
- void omp_set_dynamic (int dynamic_threads) :用于设置运行时系统对线程数量的动态调整,此函数需在并行区域外调用。当 dynamic_threads 不为 0 时,允许对后续并行区域的线程数量进行动态调整,但同一并行区域内的线程数量保持不变;当 dynamic_threads 为 0 时,禁用线程数量的动态调整。默认情况取决于具体的 OpenMP 实现。
- int omp_get_dynamic (void) :返回动态调整的当前状态信息。返回值为 0 表示未设置动态调整,非 0 表示已设置。
- 线程数量设置
- void omp_set_num_threads (int num_threads) :用于设置后续并行区域(无 num_threads 子句)的线程数量,需在并行区域外调用。其效果取决于动态调整的状态。若设置了动态调整, num_threads 的值为可使用的最大线程数;若未设置, num_threads 的值即为后续并行区域使用的线程数。
- 嵌套并行区域线程设置
- void omp_set_nested (int nested) :影响嵌套并行区域的线程数量。 nested 为 0 时,内部并行区域由一个线程顺序执行,这是默认情况; nested 不为 0 时,允许嵌套并行执行,运行时系统可为内部并行区域使用多个线程,实际行为取决于具体实现。
- int omp_get_nested (void) :返回嵌套并行区域的嵌套策略的当前状态。

以下是这些例程的使用情况总结表格:
| 例程名称 | 功能 | 参数说明 |
| — | — | — |
| omp_set_dynamic | 设置线程数量动态调整 | dynamic_threads 不为 0 允许调整,为 0 禁用 |
| omp_get_dynamic | 获取动态调整状态 | 返回 0 未设置,非 0 已设置 |
| omp_set_num_threads | 设置线程数量 | 受动态调整状态影响 |
| omp_set_nested | 设置嵌套并行区域线程情况 | nested 为 0 顺序执行,非 0 允许并行 |
| omp_get_nested | 获取嵌套策略状态 | - |

2. 线程的协调与同步

并行区域由多个线程访问相同的共享数据,因此需要同步来保护关键区域或避免竞态条件。OpenMP 提供了多种构造用于并行区域内线程的同步和协调。
- 临界区构造
- 语法:

#pragma omp critical [(name)]
structured block
- 可选的 `name` 用于标识特定的临界区域。当线程遇到临界区构造时,它会等待,直到没有其他线程执行相同名称的临界区域,然后执行临界区域的代码。未命名的临界区域被视为具有相同未指定名称的一个临界区域。
  • 屏障构造
    • 语法:
#pragma omp barrier
- 用于在执行的某个点同步线程。在显式的屏障构造处,所有线程会等待,直到团队中的所有其他线程都到达屏障,然后才继续执行后续的程序代码。
  • 原子构造
    • 语法:
#pragma omp atomic
statement
- 可包含以下形式的语句:
x binop= E,
x++, ++x, x--, --x,
其中 `x` 为任意变量,`E` 为不包含 `x` 的标量表达式,`binop` 属于 `{+, -, *, /, &, ^, |, <<, >>}`。原子构造确保语句中寻址的存储位置 `x` 以原子方式更新,即 `x` 的加载和存储操作是原子的,但表达式 `E` 的求值不是。变量 `x` 的加载和存储操作之间不允许中断。不过,原子构造不会对临界区构造指定的临界区域强制对 `x` 进行独占访问。与临界区构造相比,原子构造的一个优点是可以指定数组变量的部分进行原子更新,而使用临界区构造会保护整个数组。
- 示例:
extern float a[], *p=a, b; int index[];
#pragma omp atomic
a[index[i]] += b;
#pragma omp atomic
p[i] -= 1.0;
  • 归约子句
    • 语法:
reduction (op: list)
其中 `op` 属于 `{+, -, *, &, ^, |, &&, ||}` 是要应用的归约运算符,`list` 是要声明为共享的归约变量列表。对于 `list` 中的每个变量,团队中的每个线程都会创建一个私有副本,这些私有副本用操作 `op` 的中性元素初始化,并可由所属线程更新。在指定归约子句的区域结束时,归约变量的局部值根据运算符 `op` 进行组合,归约结果写入原始共享变量。OpenMP 编译器会生成高效的代码来执行全局归约操作,无需使用额外的同步(如临界区构造)来保证归约结果的正确性。
  • 刷新构造
    • 语法:
#pragma omp flush [(list)]
用于产生一致的内存视图,`list` 是要使其值一致的变量列表。对于列表中的指针,仅更新指针值。如果未给出列表,则更新所有变量。由于现代计算机提供内存层次结构,更新通常在较快的内存部分(如寄存器或缓存)中进行,这些更新并非立即对所有线程可见。OpenMP 具有特定的宽松一致性共享内存,更新值会稍后写回。为确保在特定程序点一个线程写入的值能被另一个线程实际读取,必须使用刷新构造。需要注意的是,如果多个线程执行刷新构造,不会提供同步。
- 隐式刷新点:
    - 屏障构造
    - 进入和退出临界区域
    - 并行区域结束
    - 无 `nowait` 子句的 `for`、`sections` 或 `single` 构造结束
    - 锁例程的进入和退出

以下是线程同步构造的总结流程图:

graph TD;
    A[并行区域] --> B{同步需求};
    B -->|是| C[选择同步构造];
    C --> D[临界区构造];
    C --> E[屏障构造];
    C --> F[原子构造];
    C --> G[归约子句];
    C --> H[刷新构造];
    B -->|否| I[继续执行];
3. 锁定机制

OpenMP 运行时系统还提供了用于线程同步的锁定机制。OpenMP 库的特定锁定机制提供了两种类型的锁变量,锁定运行时例程对其进行操作。
- 锁变量类型
- omp_lock_t :简单锁,只能锁定一次。
- omp_nest_lock_t :可嵌套锁,同一线程可以多次锁定。
- 锁变量操作例程
- 初始化

void omp_init_lock (omp_lock_t *lock);
void omp_init_nest_lock (omp_nest_lock_t *lock);
- **销毁**:
void omp_destroy_lock (omp_lock_t *lock);
void omp_destroy_nest_lock (omp_nest_lock_t *lock);
- **锁定**:
void omp_set_lock (omp_lock_t *lock)
void omp_set_nest_lock (omp_nest_lock_t *lock)
- **解锁**:
void omp_unset_lock (omp_lock_t *lock)
void omp_unset_nest_lock (omp_nest_lock_t *lock)
- **测试锁定**:
void omp_test_lock (omp_lock_t *lock)
void omp_test_nest_lock (omp_nest_lock_t *lock)
  • 示例
// 假设数据结构 pair 的定义
typedef struct {
    int a;
    int b;
    omp_nest_lock_t l;
} pair;

void incr_a(pair *p) {
    p->a++;
}

void incr_b(pair *p) {
    omp_set_nest_lock(&p->l);
    p->b++;
    omp_unset_nest_lock(&p->l);
}

void incr_pair(pair *p) {
    omp_set_nest_lock(&p->l);
    incr_a(p);
    p->b++;
    omp_unset_nest_lock(&p->l);
}
4. 练习题

以下是一些相关的练习题,帮助你巩固所学知识:
1. 修改矩阵乘法程序,使用固定数量的线程对任意大小的矩阵进行乘法运算。让每个线程计算结果矩阵的行数,而不是单个元素。计算每个线程必须计算的行数,使每个线程计算的行数大致相同。程序中是否需要同步?
2. 使用任务池实现并行矩阵乘法。将 thread_mult() 函数定义为计算结果矩阵的一个元素的任务,并根据需要修改该函数以适应任务池的要求。修改主程序,在启动执行计算的线程之前,生成所有任务并插入任务池。测量不同线程数和不同矩阵大小的执行时间,并与上一个练习的实现进行比较。
3. 扩展读写锁机制的实现,添加 rw_lock_rtrylock() rw_lock_wtrylock() 函数,如果无法授予请求的读或写权限,则返回 EBUSY
4. 修改读写锁机制的实现,使写权限具有优先级。编写一个程序测试新实现,启动三个线程(两个读线程和一个写线程)。
5. 将 Pthreads 中的读写锁实现转换为 Java 线程,编写一个新的 RWlock 类。
6. 修改流水线编程模式的示例,使流水线阶段 i 将值 i 加到从前一个阶段接收的值上。
7. 使用任务池实现定义并行循环模式。
- 修改任务池实现,提供定义并行循环和从并行循环中检索迭代的函数。
- 修改操作,使线程检索一组迭代而不是单个操作,以减少细粒度迭代的负载平衡开销。
- 在并行循环模式中包含引导式自调度(GSS)。
- 使用并行循环模式表达矩阵乘法的计算,测量不同矩阵大小的执行时间,并比较两种负载平衡方案(标准和 GSS)的执行时间。
8. 扩展客户端 - 服务器模式的 Pthreads 实现,允许具有延迟特性的取消操作。
9. 实现一个 Java 类 TaskPool ,具有与 Pthreads 任务池相同的功能。
10. 将流水线模式的 Pthreads 实现转换为 Java。
11. 将客户端 - 服务器模式的 Pthreads 实现转换为 Java 线程。
12. 分析以下 OpenMP 程序片段是否会发生死锁情况:

int x=0;
int y=0;
void foo1() {
    #pragma omp critical (x)
    { foo2(); x+=1; }
}
void foo2() {
    #pragma omp critical(y)
    { y+=1; }
}
void foo3() {
    #pragma omp critical(y)
    { y-=1; foo4(); }
}
void foo4() {
    #pragma omp critical(x)
    { x-=1; }
}
int main(int argx, char **argv) {
    int x;
    #pragma omp parallel private(i) {
        for (i=0; i<10; i++)
        { foo1(), foo3(); }
    }
    printf("%d %d \n", x,y );
}

假设两个线程在多核处理器的两个核心上执行此代码。如果会发生死锁,请描述导致死锁的执行顺序;如果不会,请说明原因。

通过学习 OpenMP 的执行环境例程、线程同步构造、锁定机制以及完成相关练习题,你可以更好地掌握并行编程的技巧,提高程序的性能和效率。在实际应用中,根据具体需求选择合适的同步和协调方法是非常重要的。

OpenMP 并行编程深入解析

5. 练习题解析
5.1 矩阵乘法程序修改
  • 操作步骤
    1. 首先确定线程数量 num_threads
    2. 计算结果矩阵的总行数 rows
    3. 计算每个线程大致应计算的行数 rows_per_thread = rows / num_threads
    4. 对于剩余的行数 remainder = rows % num_threads ,依次分配给前 remainder 个线程,每个线程多计算一行。
    5. 每个线程根据分配的行数计算结果矩阵的相应部分。
      - 关于同步 :由于每个线程计算不同的行,不存在数据竞争,通常不需要同步。但如果有共享资源(如输出流),则需要进行同步。
5.2 使用任务池实现并行矩阵乘法
  • 操作步骤
    1. 修改 thread_mult() 函数,使其接受任务信息(如结果矩阵元素的索引)。
    2. 在主程序中,生成所有任务(结果矩阵的每个元素计算任务)并插入任务池。
    3. 启动线程,线程从任务池中取出任务并执行。
    4. 测量不同线程数和不同矩阵大小下的执行时间,并与上一个练习的实现进行比较。
5.3 扩展读写锁机制
  • 操作步骤
    1. 在读写锁实现中添加 rw_lock_rtrylock() rw_lock_wtrylock() 函数。
    2. rw_lock_rtrylock() 中,检查是否可以授予读权限,如果可以则授予并返回成功,否则返回 EBUSY
    3. rw_lock_wtrylock() 中,检查是否可以授予写权限,如果可以则授予并返回成功,否则返回 EBUSY
5.4 修改读写锁机制使写权限具有优先级
  • 操作步骤
    1. 添加一个标志位 write_requested 来表示是否有写请求。
    2. 在有写请求时,不再授予读权限,直到写操作完成。
    3. 编写测试程序,启动两个读线程和一个写线程,验证写权限的优先级。
5.5 将读写锁实现转换为 Java 线程
  • 操作步骤
    1. 创建一个 RWlock 类,包含 num_r num_w 来记录当前的读和写权限数量。
    2. 实现请求和释放读、写权限的方法,类似于 Pthreads 中的函数。
5.6 修改流水线编程模式示例
  • 操作步骤
    1. 修改 pipe_stage() 函数,使其接受一个参数 i ,表示当前流水线阶段。
    2. pipe_stage() 函数中,将 i 加到从前一个阶段接收的值上。
5.7 使用任务池实现定义并行循环模式
  • a. 修改任务池实现
    • 操作步骤
      1. 提供定义并行循环的函数,该函数接受循环变量的范围和循环体函数。
      2. 提供从并行循环中检索迭代的函数,线程可以调用该函数获取下一个迭代。
      3. 提供线程函数,线程在该函数中不断检索迭代并执行循环体。
  • b. 线程检索一组迭代
    • 操作步骤
      1. 修改检索迭代的函数,使其返回一组迭代而不是单个迭代。
      2. 线程在执行完一组迭代后,再去检索下一组。
  • c. 包含引导式自调度(GSS)
    • 操作步骤
      1. 记录剩余迭代总数 Ri
      2. 当线程检索迭代时,计算 xi = ⌈Ri / n ⌉ ,其中 n 是线程数量。
      3. 线程获取 xi 个迭代,更新 Ri = Ri - xi
  • d. 使用并行循环模式表达矩阵乘法
    • 操作步骤
      1. 将矩阵乘法的每个元素计算定义为一个迭代。
      2. 使用并行循环模式执行这些迭代。
      3. 测量不同矩阵大小下的执行时间,并比较标准和 GSS 两种负载平衡方案。
5.8 扩展客户端 - 服务器模式的 Pthreads 实现
  • 操作步骤
    1. 添加取消处理功能,允许具有延迟特性的取消操作。
    2. 编写清理处理函数,在取消时释放锁定的互斥变量和分配的内存空间。
    3. tty_server_routine() 中,当取消发生时,重置 running 变量。
    4. client_routine() 中,确保 client_threads 计数器的一致性。
5.9 实现 Java 类 TaskPool
  • 操作步骤
    1. 创建 TaskPool 类,包含一个 Runnable 数组 tasks 来存储任务。
    2. 实现构造函数 TaskPool(int p, int n) ,分配任务数组并创建 p 个线程。
    3. 实现 run() 方法,线程在该方法中从任务池中取出任务并执行。
    4. 实现 insert(Runnable w) 方法,将任务插入任务池。
    5. 实现 terminate() 方法,终止所有线程。
5.10 将流水线模式的 Pthreads 实现转换为 Java
  • 操作步骤
    1. 定义一个 PipelineStage 类,表示流水线的一个阶段。
    2. 定义一个 Pipeline 类,表示整个流水线,包含多个 PipelineStage 对象。
    3. 实现发送数据到流水线、执行流水线阶段计算和从最后阶段获取结果的方法。
5.11 将客户端 - 服务器模式的 Pthreads 实现转换为 Java 线程
  • 操作步骤
    1. 定义一个 Request 类,用于存储客户端请求。
    2. 定义一个 Server 类,实现服务器功能。
    3. 分析同步机制,确保不会发生死锁。例如,使用锁的顺序要一致,避免循环等待。
5.12 分析 OpenMP 程序片段是否会发生死锁
  • 分析
    • 可能会发生死锁。假设线程 T1 进入 foo1() 并锁定临界区 x ,然后调用 foo2() 尝试锁定临界区 y ;同时线程 T2 进入 foo3() 并锁定临界区 y ,然后调用 foo4() 尝试锁定临界区 x 。此时,两个线程都在等待对方释放锁,形成死锁。
6. 总结

通过对 OpenMP 的学习,我们了解了其执行环境例程、线程协调与同步构造、锁定机制等重要内容。这些知识对于并行编程至关重要,可以帮助我们提高程序的性能和效率。以下是一个总结表格:
| 主题 | 关键内容 |
| — | — |
| 执行环境例程 | 动态线程调整、线程数量设置、嵌套并行区域设置 |
| 线程协调与同步 | 临界区构造、屏障构造、原子构造、归约子句、刷新构造 |
| 锁定机制 | 简单锁、可嵌套锁,以及相关的初始化、销毁、锁定、解锁和测试锁定操作 |
| 练习题 | 涵盖矩阵乘法、任务池、读写锁、流水线、并行循环等多种应用场景 |

同时,通过完成练习题,我们可以更好地掌握这些知识,并将其应用到实际的编程中。在实际应用中,要根据具体需求选择合适的同步和协调方法,避免死锁等问题的发生。

graph LR;
    A[OpenMP学习] --> B[执行环境例程];
    A --> C[线程协调与同步];
    A --> D[锁定机制];
    A --> E[练习题实践];
    B --> B1[动态线程调整];
    B --> B2[线程数量设置];
    B --> B3[嵌套并行区域设置];
    C --> C1[临界区构造];
    C --> C2[屏障构造];
    C --> C3[原子构造];
    C --> C4[归约子句];
    C --> C5[刷新构造];
    D --> D1[简单锁];
    D --> D2[可嵌套锁];
    E --> E1[矩阵乘法];
    E --> E2[任务池应用];
    E --> E3[读写锁扩展];
    E --> E4[流水线模式修改];
    E --> E5[并行循环模式实现];

希望通过本文的介绍和解析,你对 OpenMP 并行编程有了更深入的理解,并能够在实际项目中灵活运用这些知识。

内容概要:本文介绍了一个基于冠豪猪优化算法(CPO)的无人机三维路径规划项目,利用Python实现了在复杂三维环境中为无人机规划安全、高效、低能耗飞行路径的完整解决方案。项目涵盖空间环境建模、无人机动力学约束、路径编码、多目标代价函数设计以及CPO算法的核心实现。通过体素网格建模、动态障碍物处理、路径平滑技术和多约束融合机制,系统能够在高维、密集障碍环境下快速搜索出满足飞行可行性、安全性与能效最优的路径,并支持在线重规划以适应动态环境变化。文中还提供了关键模块的代码示例,包括环境建模、路径评估和CPO优化流程。; 适合人群:具备一定Python编程基础和优化算法基础知识,从事无人机、智能机器人、路径规划或智能优化算法研究的相关科研人员与工程技术人员,尤其适合研究生及有一定工作经验的研发工程师。; 使用场景及目标:①应用于复杂三维环境下的无人机自主导航与避障;②研究智能优化算法(如CPO)在路径规划中的实际部署与性能优化;③实现多目标(路径最短、能耗最低、安全性最高)耦合条件下的工程化路径求解;④构建可扩展的智能无人系统决策框架。; 阅读建议:建议结合文中模型架构与代码示例进行实践运行,重点关注目标函数设计、CPO算法改进策略与约束处理机制,宜在仿真环境中测试不同场景以深入理解算法行为与系统鲁棒性。
在科技快速演进的时代背景下,移动终端性能持续提升,用户对移动应用的功能需求日益增长。增强现实、虚拟现实、机器人导航、自动驾驶辅助、手势识别、物体检测与距离测量等前沿技术正成为研究与应用的热点。作为支撑这些技术的核心,双目视觉系统通过模仿人类双眼的成像机制,同步获取两路图像数据,并借助图像处理与立体匹配算法提取场景深度信息,进而生成点云并实现三维重建。这一技术体系对提高移动终端的智能化程度及优化人机交互体验具有关键作用。 双目视觉系统需对同步采集的两路视频流进行严格的时间同步与空间校正,确保图像在时空维度上精确对齐,这是后续深度计算与立体匹配的基础。立体匹配旨在建立两幅图像中对应特征点的关联,通常依赖复杂且高效的计算算法以满足实时处理的要求。点云生成则是将匹配后的特征点转换为三维空间坐标集合,以表征物体的立体结构;其质量直接取决于图像处理效率与匹配算法的精度。三维重建基于点云数据,运用计算机图形学方法构建物体或场景的三维模型,该技术在增强现实与虚拟现实等领域尤为重要,能够为用户创造高度沉浸的交互环境。 双目视觉技术已广泛应用于多个领域:在增强现实与虚拟现实中,它可提升场景的真实感与沉浸感;在机器人导航与自动驾驶辅助系统中,能实时感知环境并完成距离测量,为路径规划与决策提供依据;在手势识别与物体检测方面,可精准捕捉用户动作与物体位置,推动人机交互设计与智能识别系统的发展。此外,结合深度计算与点云技术,双目系统在精确距离测量方面展现出显著潜力,能为多样化的应用场景提供可靠数据支持。 综上所述,双目视觉技术在图像处理、深度计算、立体匹配、点云生成及三维重建等环节均扮演着不可或缺的角色。其应用跨越多个科技前沿领域,不仅推动了移动设备智能化的发展,也为丰富交互体验提供了坚实的技术基础。随着相关算法的持续优化与硬件性能的不断提升,未来双目视觉技术有望在各类智能系统中实现更广泛、更深层次的应用。 资源来源于网络分享,仅用于学习交流使用,请勿用于商业,如有侵权请联系我删除!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值