最近在学习OpenMP编程。记录下学习过程中的学习资料和历程。同时简单实现了《并行程序设计导论》中5.8节的生产者消费者问题(进程线程经典问题)。
OpenMP编程指南
参考资料:
《并行程序设计导论》第五章OpenMP
博客(知识点比书本总结全面些,里面有6篇,前4篇推荐学习):
OpenMP编程指南 - 周伟明的多核、测试专栏 - CSDN博客blog.csdn.net超算习题(提供了虚拟练习环境,不过相对简单。有时间的推荐自己边学边敲代码实现)
在线实训www.easyhpc.net以上就是本人在学习OpenMP过程涉及的资料。
生产者消费者问题
理论以及基本操作学完,需要去找个问题用OpenMP实现(测试一下自己是不是真的会用了嘛!)。
本文根据《并行程序设计导论》第5章中5.8小节讲解的生产者消费者问题,用OpenMP实现了书中的伪代码。接下来开始分析这个问题。
- 问题描述
针对典型的生产者和消费者问题,使用OpenMP编程,在共享内存系统上实现消息传递。
每一个线程有一个共享消息队列。每个线程同时充当生产者和消费者角色
生产者:线程随机产生整数“消息”和消息的目标线程,发送消息给目标线程
消费者:线程从自己的消息队列中取出消息并打印该消息
- 问题分析
数据竞争问题:当有多个生产者向同一个消息队列写数据,存在数据竞争问题
数据竞争是主要解决的问题,同时书本还分析了很多其他问题。详细见书本!
- 代码实现
1、数据结构
设计一个消息队列结构,生产者向队尾发送数据,消费者从队头取数据。数据结构中包含队头和队尾指针(用整数模拟,用数组模拟循环队列)。为了提高并行度,队列使用两个锁(队头锁和队尾锁)实现互斥,以解决书中提到的数据竞争的问题。
//消息队列 结构体
struct MesgQueue{
int *mesg;
int enqueued, dequeued;
omp_lock_t front_mutex, back_mutex;
};
2、操作函数
初始化函数:初始化锁,消息队列的内存分配,和相关的变量
void init(MesgQueue* MQ){
MQ->mesg = new int[send_max];
MQ->dequeued = 0;
MQ->enqueued = 0;
omp_init_lock(&(MQ->front_mutex));
omp_init_lock(&(MQ->back_mutex));
}
3、Send_msg函数
发生消息函数:向目标线程发送消息
void Send_msg(){
int mesg = rand();
int dest = rand() % thread_count;
//#pragma omp critical
omp_set_lock(&Msg[omp_get_thread_num()].back_mutex);
Enqueue(dest, mesg);
omp_unset_lock(&Msg[omp_get_thread_num()].back_mutex);
}
4、Try_receive函数
接收消息函数:从消息队列取消息并打印
void Try_receive(){
int cur_p = omp_get_thread_num();
int queue_size = Msg[cur_p].enqueued - Msg[cur_p].dequeued;
if(queue_size == 0) return;
else if(queue_size == 1){
//#pragma omp critical
Dequeue(cur_p);
}
else
{
Dequeue(cur_p);
}
}
5、Done函数
终止函数:判断线程是否完成任务
int Done(){
int cur_p = omp_get_thread_num();
int queue_size = Msg[cur_p].enqueued - Msg[cur_p].dequeued;
if(queue_size == 0 && done_sending == thread_count) return 1;
else return 0;
}
完整代码在文尾!
- 性能分析
书本中提到用omp critical 命令实际上,线程在发生消息依旧是串行的,因为每次只能有一个线程执行发生消息的代码。而在我们的问题中,线程0发生消息给线程1,和线程2发生消息给线程3是允许同时进行的。因此书中提到用锁机制对每个线程的消息队列进行互斥。
因此作此性能分析,观察用critical和锁时程序的不同,以及相同数据量时间上的差异。
1)critical和锁运行时(串行和并行差异)
为了更好的感受用critical和锁时线程之间的串行和并行操作,在发生消息时添加了输出
使用omp critical命令时:
可以看到,输出井然有序,线程之间先后发送消息。
使用omp_lock锁时:
可以看到,输出简直不忍直视,因为当线程之间发送的目标线程不同时,可以同时执行,因此输出就很乱。而critical限制了每次只能有一个线程执行发送消息的代码。因此在实际运用openmp时要分析清楚互斥关系,准确使用好critical和锁。不然可能程序性能不但没有得到提升,甚至更差。
用书本的话总结就是:critical适用于代码块的互斥(粗粒度),锁适用于需要互斥某个数据结构(比如本文的每个线程的消息队列),(细粒度)
2)critical和锁时间差异。
顾名思义,肯定锁的时间会更少,性能会更好。
在实验过程中,小数据量(线程数和需要发送消息数都比较小时),这两个耗时相差不大。随着数据量增大,对比会比较明显(如下两个图)
omp critical:
锁:
在实验中,还存在两个问题:
1) 有时候程序会无法运行出结果,如上面的critical图中第二次运行就没有结果,程序自动退出了emmm
2)当把输出都加上时(发送消息和接收消息的输出),大部分实验锁会比critical慢,不过这个应该是输出流造成的缓慢。
- 完整代码
#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <omp.h>
#include <time.h>
using namespace std;
int thread_count;
int send_max;
int done_sending;
struct MesgQueue{
int *mesg;
int enqueued, dequeued;
omp_lock_t front_mutex, back_mutex;
};
struct MesgQueue* Msg;
void Enqueue(int dest, int mesg){
int cur_p = omp_get_thread_num();
// printf("Thread %d send message %d to %d success!n",cur_p, mesg, dest);
Msg[dest].mesg[Msg[dest].enqueued] = mesg;
Msg[dest].enqueued++;
}
void Dequeue(int dest){
// printf("Thread %d receive message %d success!n", dest, Msg[dest].mesg[Msg[dest].dequeued]);
Msg[dest].dequeued++;
}
void Send_msg(){
int mesg = rand();
int dest = rand() % thread_count;
//#pragma omp critical
omp_set_lock(&Msg[omp_get_thread_num()].back_mutex);
Enqueue(dest, mesg);
omp_unset_lock(&Msg[omp_get_thread_num()].back_mutex);
}
void Try_receive(){
int cur_p = omp_get_thread_num();
int queue_size = Msg[cur_p].enqueued - Msg[cur_p].dequeued;
if(queue_size == 0) return;
else if(queue_size == 1){
//#pragma omp critical
Dequeue(cur_p);
}
else
{
Dequeue(cur_p);
}
}
int Done(){
int cur_p = omp_get_thread_num();
int queue_size = Msg[cur_p].enqueued - Msg[cur_p].dequeued;
if(queue_size == 0 && done_sending == thread_count) return 1;
else return 0;
}
void init(MesgQueue* MQ){
MQ->mesg = new int[send_max];
MQ->dequeued = 0;
MQ->enqueued = 0;
omp_init_lock(&(MQ->front_mutex));
omp_init_lock(&(MQ->back_mutex));
}
void destroy(){
delete [] Msg;
}
int main(int argc, char* argv[]){
if(argc != 2) printf("Error Command!n"), exit(0);
thread_count = strtol(argv[1], NULL, 10);
printf("thread_count = %d, Input the number of message:n", thread_count);
cin>>send_max;
Msg = new MesgQueue[thread_count];
srand((unsigned)time(NULL));
int sent_msgs, i;
clock_t s = clock();
#pragma omp parallel num_threads(thread_count)
{
#pragma omp for
for(i=0; i< thread_count; ++i)
init(&Msg[omp_get_thread_num()]);
#pragma omp barrier
#pragma omp for private(sent_msgs)
for(i=0; i<thread_count; ++i){
for(sent_msgs = 0; sent_msgs < send_max; ++sent_msgs){
Send_msg();
Try_receive();
}
// printf("thread %d send message done!n", omp_get_thread_num());
#pragma omp atomic
done_sending++;
while (!Done())
{
Try_receive();
}
}
}
destroy();
clock_t e = clock();
printf("Running time is: %dmsn", e-s);
return 0;
}
欢迎交流指正!