C/C++实现高性能并行计算——1.pthreads并行编程(中)

系列文章目录

  1. pthreads并行编程(上)
  2. pthreads并行编程(中)
  3. pthreads并行编程(下)
  4. 使用OpenMP进行共享内存编程

文章目录

  • 系列文章目录
  • 前言
  • 一、临界区
    • 1.1 `pi`值估计的例子
    • 1.2 找到问题
      • 竞争条件
      • 临界区
  • 二、忙等待
  • 三、互斥量
    • 3.1 定义和初始化互斥锁
    • 3.2 销毁。
    • 3.3 获得临界区的访问权(上锁)
    • 3.4 退出临界区(解锁)
      • 3.5 小节
    • 3.6 改进`pi`值估计的例子
  • 四、忙等待 vs 互斥量
  • 总结
  • 参考


前言

在C++实现高性能并行计算——1.pthreads并行编程(上)一文中介绍了pthreads的基本编程框架,但是不是随便什么程序都像上一文中轻松多线程编程,会遇到许多问题,涉及到许多底层逻辑。本篇文章就是在讲其底层逻辑。


一、临界区

1.1 pi值估计的例子

在这里插入图片描述
并行化该例子:

// pth_pi_wrong.c
#include <stdlib.h>
#include <stdio.h>
#include <pthread.h>#define n 100000000int num_thread;
double sum = 0;void* thread_sum(void* rank);int main(int argc, char* argv[]){long thread;pthread_t* thread_handles;double pi;num_thread = strtol(argv[1], NULL, 10);thread_handles = (pthread_t *)malloc(num_thread * sizeof(pthread_t *));for (thread = 0; thread < num_thread; thread++){pthread_create(&thread_handles[thread], NULL, thread_sum, (void *)thread);}for (thread = 0; thread < num_thread; thread++){pthread_join(thread_handles[thread], NULL);}pi = 4 *sum;printf("Result is %lf\n", pi);free(thread_handles);return 0;
}void* thread_sum(void *rank){long my_rank = (long)rank;double factor;long long i;long long my_n = n / num_thread; //每个线程所要计算的个数,这里理想情况,可以被整除long long my_first_i = my_n * my_rank;long long my_last_i = my_first_i + my_n;if (my_first_i % 2 == 0){factor = 1.0;}else{factor = -1.0;}for (i = my_first_i; i < my_last_i; i++, factor = -factor){sum += factor / (2 * i + 1);}return NULL;
}

运行结果:
在这里插入图片描述在这里插入图片描述

1.2 找到问题

在这里插入图片描述

竞争条件

当多个线程都要访问共享变量或共享文件这样的共享资源时,如果至少其中一个访问是更新操作,那么这些访问就可能会导致某种错误,称之为竞争条件。

临界区

临界区就是一个更新共享资源的代码段,一次只允许一个线程执行该代码段。


二、忙等待

如何进行更新操作,又要保证结果的正确性?——忙等待
使用标志变量flag,主线程将其初始化为0

y = compute(my_rank);
while (flag != my_rank); // 忙等待,要一直等待它的flag等于其rank才会执行下面的操作
x += y; //就是临界区
flag++;

在忙等待中,线程不停地测试某个条件,但实际上,直到某个条件满足之前,这些测试都是徒劳的。
缺点:浪费CPU周期,对性能产生极大的影响。


三、互斥量

在这里插入图片描述pthread_mutex_t 是 POSIX 线程(pthreads)库中用于实现互斥锁(Mutex)的数据类型。互斥锁是并行编程中常用的同步机制,用于控制多个线程对共享资源的访问,确保一次只有一个线程可以访问该资源。

3.1 定义和初始化互斥锁

  • 可以静态初始化:pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

  • 或动态初始化:使用 pthread_mutex_init() 函数。这个函数提供了一种灵活的方式来设置互斥锁的属性,不同于使用 PTHREAD_MUTEX_INITIALIZER 进行静态初始化。动态初始化允许程序在运行时根据需要创建和配置互斥锁。
    该函数原型:

    int pthread_mutex_init(pthread_mutex_t *mutex,            /*out*/const pthread_mutexattr_t *attr		/*in */);
    
      参数:- mutex:指向 pthread_mutex_t 结构的指针,该结构代表互斥锁。这个互斥锁在调用 pthread_mutex_init() 之前不需要被特别初始化。- attr:指向 pthread_mutexattr_t 结构的指针,该结构用于定义互斥锁的属性。如果传入 NULL,则使用默认属性。返回值:- 成功:函数返回 0。- 失败:返回一个错误码,表示初始化失败的原因。常见的错误码包括:- EINVAL:提供了无效的属性。- ENOMEM:没有足够的内存来初始化互斥锁。
    

3.2 销毁。

使用 pthread_mutex_destroy() 函数销毁互斥锁,释放任何相关资源。这通常在互斥锁不再需要时进行。
该函数原型是

int pthread_mutex_destroy(pthread_mutex_t *mutex);

3.3 获得临界区的访问权(上锁)

使用 pthread_mutex_lock() 函数来锁定互斥锁。如果互斥锁已被其他线程锁定,调用线程将阻塞,直到互斥锁被解锁。
该函数原型:

int pthread_mutex_lock(pthread_mutex_t *mutex);

参数

  • mutex:指向已初始化的 pthread_mutex_t 结构的指针,表示要锁定的互斥锁。

返回值

  • 成功:如果函数成功锁定互斥锁,它返回 0。
  • 失败:返回一个错误码,表明为什么锁定失败。常见的错误码包括:
    • EINVAL:如果互斥锁未正确初始化,会返回此错误。
    • EDEADLK:如果是错误检查互斥锁,并且当前线程已经锁定了这个互斥锁,会返回此错误,指示死锁风险。

3.4 退出临界区(解锁)

使用 pthread_mutex_unlock() 函数来解锁互斥锁,允许其他正在等待的线程获得资源访问权限。
该函数原型:

int phtread_mutex_unloc(pthread_mutex_t* mutex_p);

参数

  • mutex:指向需要解锁的 pthread_mutex_t 结构的指针。该互斥锁应该是先前由调用线程使用 pthread_mutex_lock() 锁定的。

返回值

  • 0:函数成功解锁了互斥锁。
  • 失败:返回一个错误码,表明为什么锁定失败。常见的错误码包括:
    • EINVAL:如果互斥锁没有被正确初始化,或者互斥锁指针无效,将返回此错误。
    • EPERM:如果当前线程不持有该互斥锁的锁定权,即尝试解锁一个它并没有锁定或者根本未被锁定的互斥锁,将返回此错误。

3.5 小节

pthread_mutex_t 是 POSIX 线程(pthreads)库中用于实现互斥锁(Mutex)的数据类型。互斥锁是并行编程中常用的同步机制,用于控制多个线程对共享资源的访问,确保一次只有一个线程可以访问该资源。

互斥锁的基本概念

  • 互斥:互斥锁保证当一个线程访问共享资源时,其他线程必须等待,直到该资源被释放(解锁),从而防止数据冲突和不一致性。
  • 死锁:如果不正确使用互斥锁,可能导致死锁,即两个或多个线程相互等待对方释放资源,结果都无法继续执行。

使用 pthread_mutex_t 类型的互斥锁通常包括以下几个步骤:

  1. 定义和初始化互斥锁
  2. 锁定互斥锁
  3. 访问共享资源
  4. 解锁互斥锁
  5. 销毁互斥锁

下面是使用 pthread_mutex_t 的简单示例:

#include <pthread.h>
#include <stdio.h>pthread_mutex_t lock;  //拿到pthread_mutex_t类型的对象lock,它这里还是个全局变量
int counter = 0;void* increment_counter(void* arg) {pthread_mutex_lock(&lock);   // 锁定互斥锁int i = *((int*) arg);counter += i;                // 修改共享资源printf("Counter value: %d\n", counter);pthread_mutex_unlock(&lock); // 解锁互斥锁return NULL;
}int main() {pthread_t t1, t2;pthread_mutex_init(&lock, NULL); // 初始化互斥锁int increment1 = 1;int increment2 = 2;pthread_create(&t1, NULL, increment_counter, &increment1);pthread_create(&t2, NULL, increment_counter, &increment2);pthread_join(t1, NULL);pthread_join(t2, NULL);pthread_mutex_destroy(&lock); // 销毁互斥锁printf("Final Counter value: %d\n", counter);return 0;
}

注意事项

  • 避免死锁:确保每个锁定的互斥锁最终都会被解锁,特别是在可能引发异常或提前返回的代码段之前。
  • 适当的锁粒度:选择正确的锁粒度很重要。过粗的锁可能导致性能低下,而过细的锁可能增加复杂性和死锁的风险。

互斥锁是保护共享数据和防止并发错误的关键工具,在设计多线程程序时需要仔细管理。

3.6 改进pi值估计的例子

主要是改进线程函数里面访问全局变量的那段代码(也就是临界区)

void* thread_sum(void *rank){long my_rank = (long)rank;double factor;long long i;long long my_n = n / num_thread; //每个线程所要计算的个数,这里理想情况,可以被整除long long my_first_i = my_n * my_rank;long long my_last_i = my_first_i + my_n;//这里定义my_sum是因为不想频繁调用互斥锁的访问临界区的权限(for循环里),所以只在最后将my_sum赋给sum的时候调用访问权限和退出权限double my_sum;  if (my_first_i % 2 == 0){factor = 1.0;}else{factor = -1.0;}for (i = my_first_i; i < my_last_i; i++, factor = -factor){my_sum += factor / (2 * i + 1);}pthread_mutex_lock(&mutex);sum += my_sum;pthread_mutex_unlock(&mutex);//在一个线程函数中只调用一次申请锁和释放锁的条件return NULL;
}

主函数:

#include <stdlib.h>
#include <stdio.h>
#include <pthread.h>#define n 100000000pthread_mutex_t mutex;int num_thread;
double sum = 0;void* thread_sum(void* rank);int main(int argc, char* argv[]){long thread;pthread_t* thread_handles;double pi;num_thread = strtol(argv[1], NULL, 10);thread_handles = (pthread_t *)malloc(num_thread * sizeof(pthread_t *));//初始化互斥锁pthread_mutex_init(&mutex, NULL);for (thread = 0; thread < num_thread; thread++){pthread_create(&thread_handles[thread], NULL, thread_sum, (void *)thread);}for (thread = 0; thread < num_thread; thread++){pthread_join(thread_handles[thread], NULL);}pi = 4 *sum;printf("Result is %lf\n", pi);free(thread_handles);pthread_mutex_destroy(&mutex);return 0;
}

运行结果:
在这里插入图片描述


四、忙等待 vs 互斥量

在这里插入图片描述


总结

  1. 发现问题:线程之间会产生竞争条件

  2. 解决思路:临界区:在更新共享资源的代码段处,一次只允许一个线程执行该代码段。但是如何使得该区域每次只能有一个线程访问(如何使得该区域成为临界区

  3. 解决方法:

    • 忙等待:使用标志变量flag,在线程函数中,每次要更新共享资源的代码处时设置一个判断flag的条件语句,只有当flag满足特定条件,才能让相应的线程进行更新共享资源。
    • 互斥量/锁:
      • 初始化锁(因为互斥锁是pthread库中的一个数据类型,得要初始化,当然也涉及到销毁)
      • 上锁
      • 访问共享内存
      • 解锁
      • 销毁锁
  4. 忙等待 vs 互斥锁:忙等待因为要频繁地执行判断语句,所以效率低。最好使用互斥锁

  5. 在使用互斥锁的时候也尽量避免频繁上锁,解锁操作,这样会印象性能。尽量每个线程只执行一次(这不是绝对,看具体执行什么操作)

  6. 这里也只是讨论了每个线程执行结果没有逻辑上的先后顺序,就像有理数的乘法交换律一样,不管什么顺序乘,结果都一样。有先后顺序的情况将在下一篇文章讨论,就如同矩阵乘法,顺序很重要!

参考

  1. 【团日活动】C++实现高性能并行计算——⑨pthreads并行编程

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/830929.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

windows11安装nginx

1.解压nginx安装包到没有中文的目录 2.双击运行nginx.exe 3.任务管理器查看是否有nginx进程 4.任务管理器->性能->资源监视器 5.网络->侦听端口&#xff0c;查看nginx侦听的端口&#xff0c;这里是90端口

大连宇都环境 | 成都5月水科技大会暨技术装备成果展览会

中华环保联合会水环境治理专业委员会 秘书处 王小雅 13718793867 —— 展位号&#xff1a;A09 —— 一、企业介绍 大连宇都环境成立于2002年&#xff0c;公司20年 MBBR填料产品及工艺技术&#xff0c;&#xff0c;构建了研发、制造、设计、工程、运营链式服务能力&#xff…

数据赋能(73)——数据要素:特征

生产要素中的数据要素具有一系列基本特征&#xff0c;这些特征使得数据在现代经济活动中发挥着越来越重要的作用。数据要素的主要特征如下图所示。 数据已经成为关键的生产要素&#xff0c;数据要素的基本特征可以概括为&#xff1a;虚拟性、非消耗性、非稀缺性、非均质性、排他…

selinux 基础知识

目录 概念 作用 SELinux与传统的权限区别 SELinux工作原理 名词解释 主体&#xff08;Subject&#xff09; 目标&#xff08;Object&#xff09; 策略&#xff08;Policy&#xff09; 安全上下文&#xff08;Security Context&#xff09; 文件安全上下文查看 先启用…

冰蓄冷空调系统介绍

冰蓄冷空调系统介绍 1.空调系统的发展历程 人类早在古代就意识到了控制室内温度的重要性&#xff0c;但直到19世纪末&#xff0c;才出现了第一个商业化的空调系统。从那时起&#xff0c;空调技术一直在不断发展&#xff0c;迅速成为现代生活中不可或缺的一部分。传统的空调系…

如何解决网络应用运行中的审核问题【系列研究预告】

目前互联网是非常发达的&#xff0c;但是随着技术的发展&#xff0c;有些问题逐渐变得严重。对于一般企业而言&#xff0c;一个比较重要的问题就是审核准确性和成本问题。 比如知乎的审判官&#xff0c;我本人是最早的一批审判官&#xff0c;然而多年下来的经历却很让人感到无…

数据结构—C语言实现双向链表

目录 1.双向带头循环链表 2.自定义头文件&#xff1a; 3.List.cpp 文件 3.1 newnode()函数讲解 3.2 init() 函数 初始化 3.3 pushback()函数 尾插 3.4 pushfront()函数 头插 3.5 popback() 尾删 3.6 popfront() 函数 头删 3.7 insert()函数 在pos之后插入 3.8 popbac…

ES与关系数据库的同步练习(hotel_admin)

目录 1 es与数据库同步的方法2 实践 1 es与数据库同步的方法 方式一&#xff1a;同步调用 优点&#xff1a;实现简单&#xff0c;粗暴缺点&#xff1a;业务耦合度高 方式二&#xff1a;异步通知&#xff08;选择这个折中下&#xff09; 优点&#xff1a;低耦合&#xff0c;…

[C++]11版本新特性4:包装器:function、bind

前言 本文将介绍包装器&#xff08;适配器&#xff09; 引入 我们之前接触过函数指针、仿函数、lambda 但他们各有优劣&#xff0c;有没有一种方法可以对众多类型进行打包封装&#xff0c;这样就可以提高效率了 那就是function包装器 包装器 function 包装器本质是一个类…

汇编语言-jcxz指令和loop指令

jcxz指令&#xff1a; jcxz 是有条件转移指令&#xff0c; 所有的有条件转移指令都是短转移&#xff0c; 在对应的机器码中包含转移的位移&#xff0c;而不是目的地址 对 IP 的修改范围都为&#xff1a; -128-127 指令格式&#xff1a;jcxz 标号 &#xff08;如果 (cx)0 &…

uniapp 对接 Apple 登录

由于苹果要求App使用第三方登录必须要求接入Apple登录 不然审核不过 所以&#xff1a; 一、勾选苹果登录 二、 设置AppId Sign In Apple 设置完成重新生成描述文件 &#xff01;&#xff01;&#xff01;&#xff01;证书没关系 示例代码&#xff1a; async appleLogin…

Android APP转成launcher

一、背景 使用场景&#xff0c;需要开机的时候&#xff0c;自动启动app&#xff0c;解决方案为将Android app转成 launcher app&#xff0c;可实现效果。 二、实现 在app入口activity 的配置文件&#xff08;AndroidManifest.xml&#xff09;对应位置&#xff0c;添加 <cat…

深入探索DreamFusion:文本到3D生成的革命性技术

深入探索DreamFusion&#xff1a;文本到3D生成的革命性技术 引言&#xff1a; 在人工智能和计算机视觉领域&#xff0c;DreamFusion无疑是一个引人注目的新星。这项技术&#xff0c;基于Google提出的深度学习模型&#xff0c;将自然语言与三维内容生成紧密结合&#xff0c;开…

Delta lake with Java--将数据保存到Minio

今天看了之前发的文章&#xff0c;居然有1条评论&#xff0c;看到我写的东西还是有点用。 今天要解决的问题是如何将 Delta产生的数据保存到Minio里面。 1、安装Minio&#xff0c;去官网下载最新版本的Minio&#xff0c;进入下载目录&#xff0c;运行如下命令&#xff0c;曾经…

Co-assistant Networks for Label Correction论文速读

文章目录 Co-assistant Networks for Label Correction摘要方法Noise DetectorNoise Cleaner损失函数 实验结果 Co-assistant Networks for Label Correction 摘要 问题描述&#xff1a; 描述医学图像数据集中存在损坏标签的问题。强调损坏标签对深度神经网络性能的影响。 提…

SpringBoot指标监控

一.SpringBoot指标监控_添加Actuator功能 Spring Boot Actuator可以帮助程序员监控和管理SpringBoot应用&#xff0c;比如健康检查、内存使用情况统计、线程使用情况统计等。我 们在SpringBoot项目中添加Actuator功能&#xff0c;即可使用Actuator监控 项目&#xff0c;用法如…

使用YALMIP定义LMI,SEDUMI求解矩阵方程

YALMIP&#xff08;Yet Another MATLAB Package for Modeling and Optimization&#xff09;是一个MATLAB工具箱&#xff0c;用于定义和求解优化问题&#xff0c;包括线性矩阵不等式&#xff08;LMI&#xff09;问题。SEDUMI是一个用于求解LMI问题的求解器&#xff0c;它可以与…

2024年五一数学建模竞赛赛题浅析-助攻快速选题

题目 复杂性 技术需求 数据处理需求 主要难点 综合评估 A题 高 优化算法、图论、启发式搜索 几何数据解析和路径计算 设计有效的路径优化算法来最小化空程&#xff0c;处理几何布局的复杂性 2 B题 非常高 网络流优化、线性/非线性规划 大规模网络数据处理和流量…

GPT每日面试题—csrf攻击的原理和解决方案

充分利用ChatGPT的优势&#xff0c;帮助我们快速准备前端面试。今日问题&#xff1a;csrf原理和解决方案? Q&#xff1a;如果在前端面试中&#xff0c;被问到csrf原理和解决方案&#xff0c;怎么回答比较好&#xff0c;全面具体的描述一下 A&#xff1a;在前端面试中&#xf…

Vitis HLS 学习笔记--S_AXILITE 寄存器及驱动

目录 1. 简介 2. S_AXILITE Registers 寄存器详解 2.1 “隐式”优势 2.2 驱动程序文件 2.3 硬件头文件 2.4 硬件头文件中 SC/COR/TOW/COH 的解释 2.5 驱动控制过程 3. 总结 1. 简介 回顾此博文《Vitis HLS 学习笔记--Syn Report解读&#xff08;1&#xff09;-CSDN博…