并行程序设计
实验一
实验问题描述
使用其他方式(如忙等待、互斥量、信号量等),自行实现不少于2种路障Barrier的功能,分析与Pthread_barrier相关接口功能的异同。提示:可采用课件上路障部分的案例,用其他2种方式实现相同功能。
实验结果
1. 使用忙等待和互斥量实现路障Barrier的功能
实验代码
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <sys/semaphore.h>
#define NUM_THREADS 8
typedef struct {
int threadId;
} threadParm_t;
int counter=0; //表示正在运行的线程数;
pthread_mutex_t barrier_mutex;
void *threadFunc(void *parm) {
threadParm_t *p = (threadParm_t *) parm;
fprintf(stdout, "Thread %d has entered step 1.\n", p->threadId);
pthread_mutex_lock(&barrier_mutex);
counter++;
pthread_mutex_unlock(&barrier_mutex);
while (counter < NUM_THREADS);
fprintf(stdout, "Thread %d has entered step 2.\n", p->threadId);
pthread_exit(NULL);
}
int main(int argc, char *argv[]) {
pthread_mutex_init(&barrier_mutex, NULL);
pthread_t thread[NUM_THREADS];
threadParm_t threadParm[NUM_THREADS];
int i;
for (i = 0; i < NUM_THREADS; i++) {
threadParm[i].threadId = i;
pthread_create(&thread[i], NULL, threadFunc, (void
*) &threadParm[i]);
}
for (i = 0; i < NUM_THREADS; i++) {
pthread_join(thread[i], NULL);
}
pthread_mutex_destroy(&barrier_mutex);
return 0;
}
实验结果
与Pthread_barrier相关接口功能的异同
同:
- 都是线程在执行完某一个任务后等待其他线程完成
- 使得所有线程在某一个地方同步
异:
- barrier中 wait()函数由每个线程主动调用,所有已到wait()的线程停在该函数不动,剩下没执行到wait()的线程继续执行;
- 忙等待使用
while
循环,是线程处于忙等待阶段,CPU利用率低
2. 使用信号量实现路障Barrier的功能
实验代码
typedef struct {
int threadId;
} threadParm_t;
int counter = 0; //判断有多少线程抵达了路障
sem_t count_sem; //用于保护计数器
sem_t barrier_sem; //用于阻塞已经进入路障的线程。
void *threadFunc(void *parm) {
threadParm_t *p = (threadParm_t *) parm;
fprintf(stdout, "Thread %d has entered step 1.\n", p->threadId);
sem_wait(&count_sem); // 等待允许访问计数器 counter,注意执行完该语句时 count_sem 值减 1,自动上锁
if (counter == NUM_THREADS - 1) // 最后一个到达进入的线程
{
counter = 0; // 计数器清零,以后可以重复使用
sem_post(&count_sem); // 计数器解锁, count_sem 值加 1
for (int i = 0; i < NUM_THREADS - 1; sem_post(&barrier_sem), i++);// 解锁整个栅栏,
} // 每有一个线程通过后面的语句 sem_wait(&barrier_sem);,
else // 前面到达的线程 // barrier_sem 的值就减 1,所以这里要为该变量加上 NUM_THREADS - 1
{
counter++; // 计数器加一
sem_post(&count_sem); // 解锁计数器
sem_wait(&barrier_sem); // 等待栅栏解锁
}
fprintf(stdout, "Thread %d has entered step 2.\n", p->threadId);
pthread_exit(NULL);
}
int main(int argc, char *argv[]) {
sem_init(&count_sem, 0, 1);
sem_init(&barrier_sem, 0, 0);
pthread_t thread[NUM_THREADS];
threadParm_t threadParm[NUM_THREADS];
int i;
for (i = 0; i < NUM_THREADS; i++) {
threadParm[i].threadId = i;
pthread_create(&thread[i], NULL, threadFunc, (void
*) &threadParm[i]);
}
for (i = 0; i < NUM_THREADS; i++) {
pthread_join(thread[i], NULL);
}
sem_destroy(&count_sem);
sem_destroy(&barrier_sem);
return 0;
}
实验结果
与Pthread_barrier相关接口功能的异同
同:
- 都是线程在执行完某一个任务后等待其他线程完成
- 使得所有线程在某一个时间段同步
异:
- barrier中 wait()函数由每个线程主动调用,所有已到wait()的线程停在该函数不动,剩下没执行到wait()的线程继续执行;
- 信号量使用
wait
和post
函数,使用post
表示公共资源线程调用完毕,公共资源加一;使用wait
表示线程需要调用公共资源,使用或者等待其他线程调用公共资源。线程被阻塞在sem_wait
不会消耗CPU周期,所以用信号量实现路障的方法比用忙等待实现的路障性能更佳。
3. 使用条件变量实现路障Barrier的功能
实验代码
#include <stdlib.h>
#include <pthread.h>
#include <stdio.h>
#include <semaphore.h>
#include <unistd.h>
#include <sys/semaphore.h>
#define NUM_THREADS 8
typedef struct {
int threadId;
} threadParm_t;
int counter = 0; //判断有多少线程抵达了路障
pthread_mutex_t mutex;
pthread_cond_t cond_var;
void *threadFunc(void *parm) {
threadParm_t *p = (threadParm_t *) parm;
fprintf(stdout, "Thread %d has entered step 1.\n", p->threadId);
pthread_mutex_lock(&mutex);
counter++;
if (counter == NUM_THREADS) {//最后一个进程
counter = 0;
pthread_cond_broadcast(&cond_var);//解锁所有被阻塞的线程
} else {
while (pthread_cond_wait(&cond_var, &mutex) != 0);
}
pthread_mutex_unlock(&mutex);
fprintf(stdout, "Thread %d has entered step 2.\n", p->threadId);
pthread_exit(NULL);
}
int main(int argc, char *argv[]) {
pthread_mutex_init(&mutex, NULL);
pthread_cond_init(&cond_var, NULL);
pthread_t thread[NUM_THREADS];
threadParm_t threadParm[NUM_THREADS];
int i;
for (i = 0; i < NUM_THREADS; i++) {
threadParm[i].threadId = i;
pthread_create(&thread[i], NULL, threadFunc, (void
*) &threadParm[i]);
}
for (i = 0; i < NUM_THREADS; i++) {
pthread_join(thread[i], NULL);
}
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond_var);
return 0;
}
实验结果
与Pthread_barrier相关接口功能的异同
同:
- 都是线程在执行完某一个任务后等待其他线程完成
- 使得所有线程在某一个时间段同步
异:
- 条件变量允许线程在某个特定条件或事件发生前都处于挂起状态。当条件或事件发生时,另一个线程可以通过信号(
pthread_cond_broadcast
)。使用pthread_cond_wait
可以告诉线程需要等待特定的条件才能解出该线程的阻塞状态
实验二
实验问题描述
对于课件中“多个数组排序”的任务不均衡案例进行复现(规模可自己调整),并探索较优的方案。
案例复现
实验代码
1. 均匀分配
代码如下
void *arr_sort(void *parm) {
threadParm_t *p = (threadParm_t *) parm;
int r = p->threadId;
gettimeofday(&startTime, NULL);
for (int i = r * seg; i < (r + 1) * seg; i++)
sort(arr[i].begin(), arr[i].end());
pthread_mutex_lock(&barrier_mutex);
gettimeofday(&stopTime, NULL);
double trans_mul_time =
(stopTime.tv_sec - startTime.tv_sec) * 1000 + (stopTime.tv_usec - startTime.tv_usec) * 0.001;
printf("Thread % d: %lf ms.\n", r, trans_mul_time);
pthread_mutex_unlock(&barrier_mutex);
pthread_exit(nullptr);
}
2. 动态分配
代码如下
void *arr_sort_fine(void *parm) {
threadParm_t *p = (threadParm_t *) parm;
int r = p->threadId;
int task = -1;
gettimeofday(&startTime, NULL);
while (true) {
pthread_mutex_lock(&mutex_task);
task = next_arr++;
pthread_mutex_unlock(&mutex_task);
if (task >= ARR_NUM) break;
sort(arr[task].begin(), arr[task].end());
}
pthread_mutex_lock(&barrier_mutex);
gettimeofday(&stopTime, NULL);
double trans_mul_time =
(stopTime.tv_sec - startTime.tv_sec) * 1000 + (stopTime.tv_usec - startTime.tv_usec) * 0.001;
printf("Thread % d: %lf ms.\n", r, trans_mul_time);
pthread_mutex_unlock(&barrier_mutex);
pthread_exit(nullptr);
}
3. 动态粗颗粒分配
代码如下
void *arr_sort_unfair(void *parm) {
threadParm_t *p = (threadParm_t *) parm;
int r = p->threadId;
int task = 0;
gettimeofday(&startTime, NULL);
while (true) {
pthread_mutex_lock(&mutex_task);
task = next_arr += 50;
pthread_mutex_unlock(&mutex_task);
if (task >= ARR_NUM) break;
for (int i = task - 50; i <= task; i++)
sort(arr[i].begin(), arr[i].end());
}
pthread_mutex_lock(&barrier_mutex);
gettimeofday(&stopTime, NULL);
double trans_mul_time =
(stopTime.tv_sec - startTime.tv_sec) * 1000 + (stopTime.tv_usec - startTime.tv_usec) * 0.001;
printf("Thread % d: %lf ms.\n", r, trans_mul_time);
pthread_mutex_unlock(&barrier_mutex);
pthread_exit(nullptr);
}
4. 主函数
每次改变数组的行数,固定数组的列数
代码如下:
int ARR_NUM = 1000;
const int MAX_NUM = 10000;
const int ARR_LEN = 10000;
const int THREAD_NUM = 4;
int seg = ARR_NUM / THREAD_NUM;
struct timeval startTime, stopTime;// timers
vector<int> arr[MAX_NUM];
int main(int argc, char *argv[]) {
int gap = 1000;//数组规模每次增加的大小
for (; ARR_NUM <= MAX_NUM; ARR_NUM += gap) {
init();
pthread_t thread[THREAD_NUM];
threadParm_t threadParm[THREAD_NUM];
printf("size : %d\n", ARR_NUM);
seg = ARR_NUM / THREAD_NUM;
next_arr = 0;
for (int i = 0; i < THREAD_NUM; i++) {
threadParm[i].threadId = i;
pthread_create(&thread[i], nullptr, arr_sort, (void *) &threadParm[i]);
}
for (int i = 0; i < THREAD_NUM; i++) {
pthread_join(thread[i], nullptr);
}
printf("time: %lf\n", times[times.size() - 1]);
}
pthread_mutex_destroy(&barrier_mutex);
}
结果如下:
如此重复多次,将所得数据汇成echarts
图标
实验结果分析
- 如上图可以看出,平均分配的时间比动态分配的时间要长。而且,随着数组规模的增大,各种算法分配的时间也会越来长,基本上成正增长的形式。
- 如上图比较动态分配和动态粗颗粒分配,可以看出,两者的运行时间基本相似,且随着矩阵规模增大而正增长。可以看出动态粗颗粒的划分动态地划分平均时间要短。
探究线程数目与运行时间的关系
这里为了探究最优方案,我采用上述的动态粗颗粒分配、固定数组大小规模为10000 × \times × 10000的规模(由于规模太小导致所有时间都会很短,所以我会尽可能设置更大的规模),探究最优方案。
主函数代码如下:
int main(int argc, char *argv[]) {
int Max_thread_num = 9;//设置最大的线程数
init();
pthread_t thread[THREAD_NUM];
threadParm_t threadParm[THREAD_NUM];
for (int i = 0; i < THREAD_NUM; i++) {
threadParm[i].threadId = i;
pthread_create(&thread[i], nullptr, arr_sort_unfair, (void *) &threadParm[i]);
}
for (int i = 0; i < THREAD_NUM; i++) {
pthread_join(thread[i], nullptr);
}
pthread_mutex_destroy(&barrier_mutex);
}
由于for
循环对线程也有一定的影响作用,所以我每次均为手动调节线程的数目
实验结果如下:
如此重复多次,得到echarts表格
实验表格
- 上述为线程数2~8之间的运行时间比较,可以看出线程数目在一定范围内,随着线程数量的增加,对函数的运行效率也有一定的提升。但是也不是意味着线程数目越多越好,当线程数达到一定的数量时,整体的运行时间也会有一定的回升。
实验三
问题描述
实现高斯消去法解线性方程组的Pthread多线程编程,与SSE/AVX编程结合,并探索优化任务分配方法。
实验代码
#include <pthread.h>
#include <iostream>
#include <sys/time.h>
#include <vector>
#include <algorithm>
#include <unistd.h>
#include <iostream>
#include <pmmintrin.h>
#include <sys/time.h>
using namespace std;
const int n = 1024;//固定矩阵规模,控制变量
const int maxN = n + 1; // 矩阵的最大值
float a[maxN][maxN];
float temp[maxN][maxN];//用于暂时存储a数组中的变量,控制变量唯一
typedef struct {
int threadId;
} threadParm_t;
const int THREAD_NUM = 4; //表示线程的个数
int seg = 10;//表示每次线程运行分配的颗粒数,将来它会改变来探究最优任务分配方法
int next_task = 0;
int line = 0;//记录当前所依赖的行数
struct timeval startTime, stopTime;// timers
pthread_mutex_t barrier_mutex = PTHREAD_MUTEX_INITIALIZER;
/**
* 根据第i行的元素,消除j行的元素
* @param i 根据的行数
* @param j 要消元的行数
*/
void SSE_elimination(int i, int j) {
float temp;
__m128 div, t1, t2, sub;
// 用temp暂存相差的倍数
temp = a[j][i] / a[i][i];
// div全部用于存储temp,方便后面计算
div = _mm_set1_ps(temp);
//每四个一组进行计算,思想和串行类似
int k = n - 3;
for (; k >= i + 1; k -= 4) {
t1 = _mm_loadu_ps(a[i] + k);
t2 = _mm_loadu_ps(a[j] + k);
sub = _mm_sub_ps(t2, _mm_mul_ps(t1, div));
_mm_store_ss(a[j] + k, sub);
}
//处理剩余部分
for (k += 3; k >= i + 1; --k) {
a[j][k] -= a[i][k] * temp;
}
a[j][i] = 0.00;
}
/**
* 多线程消元函数,动态粗颗粒分配,每次分配seg个
* @param parm
*/
void *SSE_pthread(void *parm) {
int task = 0;
while (true) {
pthread_mutex_lock(&barrier_mutex);
task = next_task;
next_task += seg;//每次分配seg个
pthread_mutex_unlock(&barrier_mutex);
if (task >= n) break;
for (int i = task; i < min(task + seg, n); ++i) {
SSE_elimination(line, i);
}
}
pthread_exit(nullptr);
}
//用于矩阵改变数值,为防止数据溢出,随机数的区间为100以内的浮点数
void change() {
srand((unsigned) time(NULL));
for (int i = 0; i < n; i++) {
for (int j = 0; j <= n; j++) {
a[i][j] = (float) (rand() % 10000) / 100.00;
}
}
}
/**
* 将a数组的数据存储到b数组中
* @param a
* @param b
*/
void store(float a[][maxN], float b[][maxN]) {
for (int i = 0; i < n; i++) {
for (int j = 0; j <= n; j++) {
b[i][j] = a[i][j];
}
}
}
int main(int arg, char *argv[]) {
change();
store(a, temp);
pthread_t thread[THREAD_NUM];
threadParm_t threadParm[THREAD_NUM];
for (int i = 0; i < THREAD_NUM; i++) {
threadParm[i].threadId = i;
}
// SSE算法消元设计
for (; seg <=
100; seg += 10) {
store(temp, a);//从temp中取数
gettimeofday(&startTime, NULL);
//高斯消元,每次依据line行来消除line行一下的行
for (line = 0; line < n - 1; ++line) {
next_task = line + 1;
for (int i = 0; i < THREAD_NUM; i++) {
pthread_create(&thread[i], nullptr, SSE_pthread, (void *) &threadParm[i]);
}
for (int i = 0; i < THREAD_NUM; i++) {
pthread_join(thread[i], nullptr);
}
}
gettimeofday(&stopTime, NULL);
double trans_mul_time =
(stopTime.tv_sec - startTime.tv_sec) * 1000 + (stopTime.tv_usec - startTime.tv_usec) * 0.001;
printf("seg: %d time: %lf ms\n", seg, trans_mul_time);
pthread_mutex_unlock(&barrier_mutex);
pthread_mutex_destroy(&barrier_mutex);
}
}
运行结果
如此多次重复实验,取平均值,得到echarts
表格
表格分析
表格结论分析
- 由上图表格可知,当粗颗粒分配、每次分配30行左右的时间是最短的,80行以上的程序运行时间和30行的程序运行时间相差不太
- 由上图可知,尽管消元规模达到了惊人的1024 × \times × 1024,但是利用多线程粗颗粒分配加上SSE编程,可以使得程序运行消元总时间达到毫秒级,这比并行程序的运行时间快了很多。可以看出,多线程编程和SSE编程可以很好地提升函数运行的效率。