多线程排序,主要是将整个排序的序列分成若干份,每一个线程排序一份,所以线程排序完成之后,就进行归并,相当于多个有序序列合并成一个有序序列。
这里就需要用到线程屏障,也就是 pthread_barrier 系列函数。
屏障,通俗的说就是一个比赛跑步的过程,所以队员就绪了,才能进行比赛。
多线程排序也是,需要每个线程都是排序完成后,才能进行合并的过程。
代码:
#include <cstdio>
#include <cstring>
#include <iostream>
#include <cstdlib>
#include <sys/time.h>
#include <pthread.h>
#include <algorithm>
using namespace std;
const long MAX = 1e7L; // max num in array
const long long MAX_NUM = 1e8L; // num of element to sort
const int thread = 100;
const int thread_num = MAX_NUM / thread;
int num[MAX_NUM];
int tmp_num[MAX_NUM];
pthread_barrier_t barrier; // barrier
//Initialized Data
void init()
{
srand(1);
for (int i = 0; i < MAX_NUM; ++i) {
num[i] = rand() % MAX;
}
}
//Quick sort function
void qsorts(int* start, int* end) {
int nums = end - start;
if (nums > 0) {
int flag = start[0];
int i = 0;
int j = nums;
while (i < j) {
while (j > i && start[j] > flag) {
--j;
}
start[i] = start[j];
while (i < j && start[i] <= flag) {
++i;
}
start[j] = start[i];
}
start[i] = flag;
qsorts(start, start + i - 1);
qsorts(start + i + 1, end);
}
}
void* work(void* arg) {
long index = (long)arg;
qsorts(num+index, num+index+thread_num-1);
pthread_barrier_wait(&barrier);
pthread_exit(NULL);
}
void meger()
{
long index[thread];
for (int i = 0; i < thread; ++i)
{
index[i] = i * thread_num;
}
for(long i = 0; i < MAX_NUM; ++i)
{
long min_index;
long min_num = MAX;
for(int j = 0; j < thread; ++j)
{
if((index[j] < (j + 1) * thread_num)
&& (num[index[j]] < min_num))
{
min_index = j;
min_num = num[index[j]];
}
}
tmp_num[i] = num[index[min_index]];
index[min_index]++;
}
}
int main(int argc, char *argv[])
{
init();
struct timeval start, end;
pthread_t ptid;
printf("%ld %ld\n", num[1], num[2]);
gettimeofday(&start, NULL);
//init pthread and thread barrier
pthread_barrier_wait(&barrier);
//add 1, total have (thread+1) threads
for (int i = 0; i < thread; ++i) {
pthread_create(&ptid, NULL, work, (void*)(i*thread_num));
}
pthread_barrier_wait(&barrier);
meger();
gettimeofday(&end, NULL);
long long s_usec = start.tv_sec * 1000000 + start.tv_usec;
long long e_usec = end.tv_sec * 1000000 + end.tv_usec;
double useTime = (double)(e_usec - s_usec) / 1000000.0;
printf("sort use %.4f seconds\n", useTime);
return 0;
}
结果:
thread time
1 0.4500
2 0.6070
3 0.9510
4 1.2010
8 8.1470
12 11.2020
16 10.5170
100 27.1150
为什么我的线程数越多计算时间反而慢了呢
首先我们应该知道,操作系统是如何使用线程的。每个进程中可以启动若干个线程,这些线程跟操作系统请求计算资源,操作系统也许没我们想的那么智能,它不一定按照我们想要的方式去讲线程与计算核心对应起来。
比如,有时候,我们的线程内部的计算需要在磁盘读取数据,这样就会使得当前线程等待,操作系统就可能智能的把它挂起了,它的计算资源又被其它线程使用了,等到数据准备完毕之后,操作系统又将挂起的线程以及他的资源(寄存器数据,缓存数据)一同放到一个计算核心(计算资源)上(实际上操作系统可能不会吧所有缓存数据都拉出到内存,但是如果在第一个线程等待时,插入的线程的使用了大量缓存,你的原有缓存数据就极有可能被冲掉了,就需要重新缓存),当然这个时候你被唤醒的线程可能不会再原有的核心上了,总之这种线程切换的过程会使得你的线程计算时间变慢了。
又比如,你的线程数量过多。这个情况下,如果线程的负载是雷同的,那么你的线程很可能被频繁切换,这样也会把时间变慢。不过不是说线程数比核心数少就好,这个要有个度。你的线程负载均衡的话,如果线程切换时间能很好的弥补掉线程挂起等待的时间,各个线程交错执行,完全占用计算资源,你的计算速度才会快。
上下文切换的精确定义可以参考: http://www.linfo.org/context_switch.html。多任务系统往往需要同时执行多道作业。作业数往往大于机器的CPU数,然而一颗CPU同时只能执行一项任务,为了让用户感觉这些任务正在同时进行,操作系统的设计者巧妙地利用了时间片轮转的方式,CPU给每个任务都服务一定的时间,然后把当前任务的状态保存下来,在加载下一任务的状态后,继续服务下一任务。任务的状态保存及再加载,这段过程就叫做上下文切换。时间片轮转的方式使多个任务在同一颗CPU上执行变成了可能,但同时也带来了保存现场和加载现场的直接消耗。(Note. 更精确地说, 上下文切换会带来直接和间接两种因素影响程序性能的消耗. 直接消耗包括: CPU寄存器需要保存和加载, 系统调度器的代码需要执行, TLB实例需要重新加载, CPU 的pipeline需要刷掉; 间接消耗指的是多核的cache之间得共享数据, 间接消耗对于程序的影响要看线程工作区操作数据的大小).
------------------------------------------------------------------------------------------------------
根据上面上下文切换的定义,我们做出下面的假设:
之所以TwoThreadSwitchTester执行速度最慢,因为线程上下文切换的次数最多,时间主要消耗在上下文切换了,两个线程交替计数,每计数一次就要做一次线程切换。
“Multi Threads - 100 Threads”比“Multi Threads - 2 Threads”开的线程数量要多,导致线程切换次数也比后者多,执行时间也比后者长。
由于Windows下没有像Linux下的vmstat这样的工具,这里我们使用Process Explorer看看程序执行的时候线程上线文切换的次数。
Single Thread:
计数期间,线程总共切换了580-548=32次。(548是启动程序后,初始的数值)
Two Thread Switch:
计数期间,线程总共切换了33673295-124=33673171次。(124是启动程序后,初始的数值)
Multi Threads - 100 Threads:
计数期间,线程总共切换了846-329=517次。(329是启动程序后,初始的数值)
Multi Threads - 2 Threads:
计数期间,线程总共切换了295-201=94次。(201是启动程序后,初始的数值)
从上面收集的数据来看,和我们的判断基本相符。
干活的其实是CPU,而不是线程
再想想原来学过的知识,之前一直以为线程多干活就快,简直是把学过的计算机原理都还给老师了。真正干活的不是线程,而是CPU。线程越多,干活不一定越快。
那么高并发的情况下什么时候适合单线程,什么时候适合多线程呢?
适合单线程的场景:单个线程的工作逻辑简单,而且速度非常快,比如从内存中读取某个值,或者从Hash表根据key获得某个value。Redis和Node.js这类程序都是单线程,适合单个线程简单快速的场景。
适合多线程的场景:单个线程的工作逻辑复杂,等待时间较长或者需要消耗大量系统运算资源,比如需要从多个远程服务获得数据并计算,或者图像处理。
------------------------------------------------------------------------------------------------------
控制代码写的不好会导致并行更慢。理论上不提倡自己写多线程库去优化,建议使用更成熟的并行库(比如OpenMP/cilk plus等)。
如果cpu 2核心4线程。假设生成了100个线程,理论上会有4个线程并行,但因为锁的争用以及线程间调度,实际上效率会大大降低。
参考资料:
https://blog.csdn.net/qq_25425023/article/details/72705285
https://blog.csdn.net/bendanban/article/details/42773723