并行程序设计——实现路障Barrier的功能

并行程序设计

实验一

实验问题描述

使用其他方式(如忙等待、互斥量、信号量等),自行实现不少于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相关接口功能的异同

同:

  1. 都是线程在执行完某一个任务后等待其他线程完成
  2. 使得所有线程在某一个地方同步

异:

  1. barrier中 wait()函数由每个线程主动调用,所有已到wait()的线程停在该函数不动,剩下没执行到wait()的线程继续执行;
  2. 忙等待使用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相关接口功能的异同

同:

  1. 都是线程在执行完某一个任务后等待其他线程完成
  2. 使得所有线程在某一个时间段同步

异:

  1. barrier中 wait()函数由每个线程主动调用,所有已到wait()的线程停在该函数不动,剩下没执行到wait()的线程继续执行;
  2. 信号量使用waitpost函数,使用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相关接口功能的异同

同:

  1. 都是线程在执行完某一个任务后等待其他线程完成
  2. 使得所有线程在某一个时间段同步

异:

  1. 条件变量允许线程在某个特定条件或事件发生前都处于挂起状态。当条件或事件发生时,另一个线程可以通过信号(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图标

实验结果分析

请添加图片描述

  1. 如上图可以看出,平均分配的时间比动态分配的时间要长。而且,随着数组规模的增大,各种算法分配的时间也会越来长,基本上成正增长的形式。

请添加图片描述

  1. 如上图比较动态分配和动态粗颗粒分配,可以看出,两者的运行时间基本相似,且随着矩阵规模增大而正增长。可以看出动态粗颗粒的划分动态地划分平均时间要短。

探究线程数目与运行时间的关系

这里为了探究最优方案,我采用上述的动态粗颗粒分配、固定数组大小规模为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表格

实验表格

请添加图片描述

  1. 上述为线程数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 <=![请添加图片描述](https://img-blog.csdnimg.cn/e099ffcce6ae4561ae0fb80dc66e8318.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBATksuTWFpbkpheQ==,size_20,color_FFFFFF,t_70,g_se,x_16)
 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表格

表格分析

请添加图片描述

表格结论分析
  1. 由上图表格可知,当粗颗粒分配、每次分配30行左右的时间是最短的,80行以上的程序运行时间和30行的程序运行时间相差不太
  2. 由上图可知,尽管消元规模达到了惊人的1024 × \times × 1024,但是利用多线程粗颗粒分配加上SSE编程,可以使得程序运行消元总时间达到毫秒级,这比并行程序的运行时间快了很多。可以看出,多线程编程和SSE编程可以很好地提升函数运行的效率。

源代码

源代码

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值