长沙 外贸网站建设公司价格,网站站建设,wordpress all in one seo pack,怎样开发自己的app系列文章目录
pthreads并行编程(上)pthreads并行编程(中)pthreads并行编程(下)使用OpenMP进行共享内存编程 文章目录 系列文章目录前言一、临界区1.1 pi值估计的例子1.2 找到问题竞争条件临界区 二、忙等待三、互斥量3.1 定义和初始化互斥锁3.2 销毁。3.3 获得临界区的访问权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 类型的互斥锁通常包括以下几个步骤
定义和初始化互斥锁锁定互斥锁访问共享资源解锁互斥锁销毁互斥锁
下面是使用 pthread_mutex_t 的简单示例
#include pthread.h
#include stdio.hpthread_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 互斥量 总结 发现问题线程之间会产生竞争条件 解决思路临界区在更新共享资源的代码段处一次只允许一个线程执行该代码段。但是如何使得该区域每次只能有一个线程访问如何使得该区域成为临界区 解决方法 忙等待使用标志变量flag在线程函数中每次要更新共享资源的代码处时设置一个判断flag的条件语句只有当flag满足特定条件才能让相应的线程进行更新共享资源。互斥量/锁 初始化锁因为互斥锁是pthread库中的一个数据类型得要初始化当然也涉及到销毁上锁访问共享内存解锁销毁锁 忙等待 vs 互斥锁忙等待因为要频繁地执行判断语句所以效率低。最好使用互斥锁 在使用互斥锁的时候也尽量避免频繁上锁解锁操作这样会印象性能。尽量每个线程只执行一次这不是绝对看具体执行什么操作 这里也只是讨论了每个线程执行结果没有逻辑上的先后顺序就像有理数的乘法交换律一样不管什么顺序乘结果都一样。有先后顺序的情况将在下一篇文章讨论就如同矩阵乘法顺序很重要
参考
【团日活动】C实现高性能并行计算——⑨pthreads并行编程