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

news2024/11/18 15:50:16

系列文章目录

  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 100000000

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 *));

	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 100000000

pthread_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.coloradmin.cn/o/1637202.html

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

相关文章

《读懂财务报表》手绘版读书笔记:通过报表找好公司

通过财报的三张表判断好公司&#xff1a; 然后是在三表中&#xff0c;计算各个项目占总体的比例&#xff0c;以及做比率分析&#xff0c; 比率分析&#xff0c;从偿还能力&#xff0c;运营能力&#xff0c;盈利能力三方面分析&#xff1a; 1&#xff09; 偿还能力 2&#xff09…

新建stm32工程模板步骤

1.先使用keil新建一个project的基本代码 2.stm32启动文件添加 将stm32的启动文件&#xff0c;在原工程当中新建一个Start文件夹把相关的启动文件放到文件夹当中 然后还需要找到下面三个文件 stm32f10x.h是stm32的外设寄存器的声明和定义&#xff0c;后面那两个文件用于配置系…

Python数据预处理1:导入与基本操作

2024/4/30 After installing the xlrd package, you should be able to read Excel files using pandas without any issues. #需要在pyCharm命令行中下载两个包 pip install pandas pip install xlrd .xls数据导入 #数据的导入 import pandas as pd #导入EXCEL表格数据 df…

Java基于微信小程序+uniapp的校园失物招领小程序(V3.0)

博主介绍&#xff1a;✌程序员徐师兄、7年大厂程序员经历。全网粉丝12w、csdn博客专家、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专栏推荐订阅&#x1f447;…

【大数据】利用 Apache Ranger 管理 Amazon EMR 中的数据权限

利用 Apache Ranger 管理 Amazon EMR 中的数据权限 1.需求背景简介2.系统方案架构图3.主要服务和组件简介3.1 Amazon EMR3.2 Simple Active Directory3.3 Apache Ranger 4.部署步骤4.1 部署 Simple AD 服务4.2 部署 Apache Ranger4.3 部署 Amazon EMR4.4 在 Amazon EMR 的主节点…

FPGA高端项目:FPGA帧差算法多目标图像识别+目标跟踪,提供11套工程源码和技术支持

目录 1、前言免责声明 2、相关方案推荐FPGA帧差算法单个目标图像识别目标跟踪 3、详细设计方案设计原理框图运动目标检测原理OV5640摄像头配置与采集OV7725摄像头配置与采集RGB视频流转AXI4-StreamVDMA图像缓存多目标帧差算法图像识别目标跟踪模块视频输出Xilinx系列FPGA工程源…

spring-boot示例

spring-boot版本&#xff1a;2.0.3.RELEASE 数据库: H2数据库 &#xff08;嵌入式内存性数据库&#xff0c;安装简单&#xff0c;方便用于开发、测试&#xff0c;不适合用于生产&#xff09; mybatis-plus框架&#xff0c;非常迅速开发CRUD

Liunx磁盘管理(上)

Liunx磁盘管理&#xff08;中&#xff09;-CSDN博客 目录 一.硬盘类型 机械硬盘&#xff08;HDD&#xff09; 固态硬盘&#xff08;SSD&#xff09; 二.插拔方式 1. 热插拔&#xff08;Hot Swapping&#xff09; 2. 冷插拔&#xff08;Cold Swapping&#xff09; 3. 模块…

新唐的nuc980/nuc972的开发3-官方源码编译

上一节中bsp已经安装&#xff0c;交叉环境已经搭建&#xff0c;理应就可以正常的编写上层的应用程序啦。 但是系统启动次序是- uboot-> kernel内核 ->挂载文件系统 ->上层应用程序 下面是bsp安装后的文件&#xff1a; 因此本章节&#xff0c;将讲解 uboot-> kerne…

MySql-日期分组

一、分别统计各时间各类型数据条数 数据库的 request_time字段 数据类型&#xff1a;timestamp 默认值&#xff1a;CURRENT_TIMESTAMP 例子&#xff1a; 2024-01-26 08:25:48 原数据&#xff1a; 1、将数据按照日期&#xff08;年月日&#xff09;形式输出 按照request_…

C语言:文件操作(下)

片头 嗨&#xff01;小伙伴们&#xff0c;在前2篇中&#xff0c;我们分别讲述了C语言&#xff1a;文件操作&#xff08;上&#xff09;和 C语言&#xff1a;文件操作&#xff08;中&#xff09;&#xff0c;今天我们将会学习文件操作&#xff08;下&#xff09;&#xff0c;准…

电商技术揭秘四十一:电商平台的营销系统浅析

相关系列文章 电商技术揭秘相关系列文章合集&#xff08;1&#xff09; 电商技术揭秘相关系列文章合集&#xff08;2&#xff09; 电商技术揭秘相关系列文章合集&#xff08;3&#xff09; 文章目录 引言一、用户画像与精准营销用户画像与精准营销的概念用户画像在精准营销中…

基于FPGA的数字信号处理(7)--如何确定Verilog表达式的位宽

一般规则 很多时候&#xff0c;Verilog中表达式的位宽都是被隐式确定的&#xff0c;即使你自己设计了位宽&#xff0c;它也是根据规则先确定位宽后&#xff0c;再扩展到你的设计位宽&#xff0c;这常常会导致结果产生意想不到的错误。比如&#xff1a; timescale 1ns/1ns mod…

Docker命令(持续更新)

目录 下载和安装 安装必要的依赖项 添加Docker仓库 安装Docker Engine 启动Docker服务 配置阿里云镜像 卸载Docker 镜像 删除指定id镜像 删除所有镜像 镜像保存本地 本地镜像加载到docker服务器内 容器 创建容器 查看所有容器 停止所有容器 启动已存在容器 删…

从键入网址到网页显示,期间发生了什么?

从键入网址到网页显示&#xff0c;期间发生了什么&#xff1f; 孤单小弟【HTTP】真实地址查询【DNS】指南帮手【协议栈】可靠传输【TCP】远程定位【IP】两点传输【MAC】出口【网卡】送别者【交换机】出境大门【路由器】互相扒皮【服务器与客户端】相关问答 不少小伙伴在面试过程…

【千帆平台】AppBuilder工作流编排新功能体验之创建自定义组件

欢迎来到《小5讲堂》 这是《千帆平台》系列文章&#xff0c;每篇文章将以博主理解的角度展开讲解。 温馨提示&#xff1a;博主能力有限&#xff0c;理解水平有限&#xff0c;若有不对之处望指正&#xff01; 目录 前言工作流编排组件 创建组件组件界面组件信息 组件画布操作节点…

Liunx发布tomcat项目

Liunx在Tomcat发布JavaWeb项目 1.问题2.下载JDK3.下载Tomcat4.Tomcat本地JavaWeb项目打war包、解压、发布5.重启Tomcat,查看项目 1.问题 1.JDK 与 Tomcat 版本需匹配&#xff0c;否则页面不能正确显示 报错相关&#xff1a;Caused by: java.lang.ClassNotFoundException: java…

Tag文件与Tag标记

一、Tag文件 Tag文件与JSP文件很类似&#xff0c;可以被JSP页面动态加载调用&#xff0c;实现代码的复用&#xff0c;但用户不能通过该Tag文件所在Web服务目录直接访问Tag文件 1、文件结构 Tag文件是扩展名为.tag的文本文件&#xff0c;其结构和JSP文件类似。一个Tag文件中可…

十一、大模型-Semantic Kernel与 LangChain 的对比

Semantic Kernel 与 LangChain 的对比 Semantic Kernel 和 LangChain 都是用于开发基于大型语言模型&#xff08;LLM&#xff09;的应用程序的框架&#xff0c;但它们各有特点和优势。 基本概念和目标 Semantic Kernel 是一个由微软开发的轻量级 SDK&#xff0c;旨在帮助开发…

nginx版本号隐藏

隐藏Nginx版本号的主要作用是增强服务器的安全性。当Nginx的版本号被隐藏时&#xff0c;攻击者就难以利用已知的漏洞来攻击特定版本的软件&#xff0c;因为他们无法确切知道服务器上运行的Nginx版本。这样可以降低攻击者对系统的了解&#xff0c;增加攻击的复杂性&#xff0c;从…