多线程实现快速排序

多线程排序,主要是将整个排序的序列分成若干份,每一个线程排序一份,所以线程排序完成之后,就进行归并,相当于多个有序序列合并成一个有序序列。

这里就需要用到线程屏障,也就是 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

https://blog.csdn.net/ljj2312/article/details/44206925

https://www.zhihu.com/question/26189066

  • 6
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Java多线程快速排序的基本思路如下: 1. 将待排序的数组分成两部分,每一部分都是一个子数组; 2. 对于每一个子数组,创建一个新的线程去进行递归排序; 3. 等待所有子线程执行结束后,将两个子数组合并成一个有序的数组。 下面是使用Java多线程实现快速排序的示例代码: ```java import java.util.concurrent.*; public class QuickSort { public static void quickSort(int[] array) { ExecutorService executor = Executors.newCachedThreadPool(); executor.execute(new QuickSortTask(array, 0, array.length - 1)); executor.shutdown(); try { executor.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS); } catch (InterruptedException e) { e.printStackTrace(); } } private static class QuickSortTask implements Runnable { private int[] array; private int left; private int right; public QuickSortTask(int[] array, int left, int right) { this.array = array; this.left = left; this.right = right; } @Override public void run() { if (left >= right) { return; } int i = left; int j = right; int pivot = array[(left + right) / 2]; while (i <= j) { while (array[i] < pivot) { i++; } while (array[j] > pivot) { j--; } if (i <= j) { int temp = array[i]; array[i] = array[j]; array[j] = temp; i++; j--; } } if (left < j) { ExecutorService executor = Executors.newCachedThreadPool(); executor.execute(new QuickSortTask(array, left, j)); executor.shutdown(); } if (i < right) { ExecutorService executor = Executors.newCachedThreadPool(); executor.execute(new QuickSortTask(array, i, right)); executor.shutdown(); } } } } ``` 在这个示例代码中,我们使用了Java的线程池来管理线程,并且在每次递归时都创建了一个新的线程来处理子数组的排序。这种方法可以在一定程度上提高排序的速度,但是也会带来一些额外的开销和复杂度。因此,我们需要根据实际情况进行权衡和选择。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值