一、编程目的
- 熟悉多线程编程的基本模式;
- 对比多线程和单线程编程的优缺点。
二、代码实现
代码主要涉及到三个文件:生成数组代码:gen_arr.c
、单线程归并merge_sort.c
、多线程归并merge_sort_thread.c
。
1.随机生成数组
gen_arr.c
:
/*
生成200000大小数组,并写入文件arr.txt中
编译命令:
gcc gen_arr.c -o gen_arr
./gen_arr
*/
#include <stdio.h>
#include <time.h>
#include <stdlib.h>
int arr_num = 200000;
char fileName[] = "arr.txt";
int main(void)
{
FILE *fp = fopen(fileName, "w");
if (fp == NULL)
{
printf("Error in opening file %s.\n", fileName);
exit(-1);
}
fprintf(fp, "%d%c", arr_num, '\n');
int num;
srand((unsigned int)time(0)); //修改种子
for (int i = 0; i < arr_num; i++)
{
num = rand();
fprintf(fp, "%d%c", num, ' ');
}
fclose(fp);
fp = fopen(fileName, "r");
if (fp == NULL)
{
printf("Error in opening file %s.\n", fileName);
exit(-1);
}
fscanf(fp, "%d", &arr_num);
printf("arr_num = %d\n", arr_num);
int *a = (int *)malloc(arr_num * sizeof(int));
for (int i = 0; i < arr_num; i++)
{
fscanf(fp, "%d", &a[i]);
}
for (int i = 0; i < arr_num; i++)
{
printf("%d ", a[i]);
}
printf("\n");
return 0;
}
2.单线程归并排序
merge_sort.c
:
/*
单线程归并排序
编译命令:(注意要先使用gen_arr程序生成数组)
gcc merge_sort.c -o merge_sort
time ./merge_sort
*/
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define ARR_LEN 200000
int arr_num;
char fileName[] = "arr.txt";
int arr[ARR_LEN] = {0};
void Display(int *arr, int n)
{
for (register int i = 0; i < n; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
}
void merge(int *arr, int L, int M, int R)
{
int LEFT_SIZE = M - L;
int RIGHT_SIZE = R - M + 1;
int left[LEFT_SIZE];
int right[RIGHT_SIZE];
int i, j, k;
//将原数组左边分到一个新数组left中
for (i = L; i < M; i++)
{
left[i - L] = arr[i];
}
//将原数组右边分到一个新数组right中
for (i = M; i <= R; i++)
{
right[i - M] = arr[i];
}
i = 0;
j = 0;
k = L;
while (i < LEFT_SIZE && j < RIGHT_SIZE)
{
//如果left数组的第一个数字比right数组第一个数字小就把它放到新数组里
if (left[i] < right[j])
{
arr[k] = left[i];
i++;
k++;
}
else
{
arr[k] = right[j];
j++;
k++;
}
}
while (i < LEFT_SIZE)
{
arr[k] = left[i];
i++;
k++;
}
while (j < RIGHT_SIZE)
{
arr[k] = right[j];
j++;
k++;
}
}
void mergesort(int *arr, int L, int R)
{
int M = (L + R) / 2;
if (L == R)
{
return;
}
else
{
// printf("归并排序中:");
// Display(arr, 20);
mergesort(arr, L, M);
mergesort(arr, M + 1, R);
merge(arr, L, M + 1, R);
}
}
void read_data()//从文件中读取数组数据
{
FILE *fp = fopen(fileName, "r");
if (fp == NULL)
{
printf("Error in opening file %s.\n", fileName);
exit(-1);
}
fscanf(fp, "%d", &arr_num);
for (int i = 0; i < arr_num; i++)
{
fscanf(fp, "%d", &arr[i]);
}
}
int main()
{
read_data();
// printf("原数组为:\n");
// Display(arr, arr_num);
// printf("\n");
int L = 0;
int R = arr_num - 1;
mergesort(arr, L, R);
// printf("\n归并排序后:\n");
// Display(arr, R + 1);
return 0;
}
3.多线程归并排序
这里规定了在排序过程中使用的最大线程数量,因为在不断调试程序的过程中,发现数组大小、线程数量都对多线程排序的效率有影响。
- 数组规模太小的话,使用多线程的代价大,内核处理时间占很多;数组规模太大的话,递归层数多,可能开启的线程也多,同样地系统内核处理时间就会变得很长。
- 线程开启数量太少,并发程度不高,和普通的单线程没有太大区别;线程开启数量太多,花销又很大,内核处理时间也太长,还有线程数量过多导致程序段错误的风险(经过试验得出)。‘
所以我只能在确定数据规模和内容(200000个数据)的情况下,不断地试验最多开启多少个线程合适,最后发现选取线程数量在50~100之间效率比较高。
/*
多线程归并排序
编译命令:(注意要先使用gen_arr程序生成数组)
gcc merge_sort_thread.c -o merge_sort_thread -lpthread
time ./merge_sort_thread
*/
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <pthread.h>
#include <unistd.h>
#define ARR_LEN 200000
#define MAX_THREAD_SIZE 75 //规定最多开启线程的数量
int arr_num;
char fileName[] = "arr.txt";
int thread_num = 0; //开启线程的数量,初始化为0
pthread_mutex_t Mutex; //修改thread_num时用到的锁(互斥信号量)
int arr[ARR_LEN] = {0}; //数组
void Display(int *arr, int n) //打印数组用
{
for (register int i = 0; i < n; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
}
void merge(int L, int M, int R) //合并部分,没有用到多线程
{
int LEFT_SIZE = M - L;
int RIGHT_SIZE = R - M + 1;
int left[LEFT_SIZE];
int right[RIGHT_SIZE];
int i, j, k;
//将原数组左边分到一个新数组left中
for (i = L; i < M; i++)
{
left[i - L] = arr[i];
}
//将原数组右边分到一个新数组right中
for (i = M; i <= R; i++)
{
right[i - M] = arr[i];
}
i = 0;
j = 0;
k = L;
while (i < LEFT_SIZE && j < RIGHT_SIZE)
{
//如果left数组的第一个数字比right数组第一个数字小就把它放到新数组里
if (left[i] < right[j])
{
arr[k] = left[i];
i++;
k++;
}
else
{
arr[k] = right[j];
j++;
k++;
}
}
while (i < LEFT_SIZE)
{
arr[k] = left[i];
i++;
k++;
}
while (j < RIGHT_SIZE)
{
arr[k] = right[j];
j++;
k++;
}
}
void *mergesort(void *arg) //排序主体部分,用到了多线程
{
int *m_arg = (int *)arg;
int L = m_arg[0];
int R = m_arg[1];
// printf("L=%d,R=%d\n", L, R);
int M = (L + R) >> 1;
if (L >= R)
{
return NULL;
}
int arg1[2], arg2[2];
arg1[0] = L;
arg1[1] = M;
arg2[0] = M + 1;
arg2[1] = R;
int start_thread = 0; //决定是否开启线程
pthread_t p1, p2;
pthread_mutex_lock(&Mutex);
if (thread_num + 2 <= MAX_THREAD_SIZE) //其实与最大线程数量存在一定偏差(不超过2)
{ //如果没有超过最大线程数量,那么就开启两个子线程
pthread_create(&p1, NULL, mergesort, arg1);
pthread_create(&p2, NULL, mergesort, arg2);
thread_num += 2;
start_thread = 1;
}
pthread_mutex_unlock(&Mutex);
// mergesort(arg1);
if (start_thread == 1)
{
pthread_join(p1, NULL);
pthread_join(p2, NULL);
pthread_exit(NULL);
}
else //超过最大线程数量,用普通的归并排序
{
mergesort(arg1);
mergesort(arg2);
}
merge(L, M + 1, R);
}
void read_data()
{
FILE *fp = fopen(fileName, "r");
if (fp == NULL)
{
printf("Error in opening file %s.\n", fileName);
exit(-1);
}
fscanf(fp, "%d", &arr_num);
for (int i = 0; i < arr_num; i++)
{
fscanf(fp, "%d", &arr[i]);
}
}
int main()
{
read_data();
int arg[2];
arg[0] = 0;
arg[1] = ARR_LEN;
pthread_t tid;
pthread_create(&tid, NULL, mergesort, arg);
pthread_join(tid, NULL);
printf("使用到的线程数量为:%d。\n", thread_num);
return 0;
}
三、对比测试
先使用命令:
gcc gen_arr.c -o gen_arr
./gen_arr
生成大小为200000的数组。
然后用命令:
gcc merge_sort.c -o merge_sort
time ./merge_sort
和
gcc merge_sort_thread.c -o merge_sort_thread -lpthread
time ./merge_sort_thread
分别测试单、多线程归并排序的时间,统计10次运行的平均值:
单线程 | 最大线程数50 | 最大线程数75(实际是74) | 最大线程数100 | |
---|---|---|---|---|
10次平均运行时间 | 0.0537 s 0.0537s 0.0537s | 0.0451 0.0451 0.0451 | 0.0522 s 0.0522s 0.0522s | 0.0454 0.0454 0.0454 |
可以看到在一般情况下多线程归并排序确实比单线程要快一点,数据量越大,多线程的优点应该愈发明显。但一味地开启子线程反而会降低程序的运行效率,甚至可能会导致程序崩溃!因此限制最大线程数是有必要的。