多執行緒排序-真香!
- 2019 年 12 月 27 日
- 筆記
來源:公眾號【編程珠璣】
作者:守望先生
ID:shouwangxiansheng 在《系統編程-多執行緒》中已經了解了多執行緒的一些特點,其中包括快!那麼今天就來看看如何利用多執行緒來排序。
思路
我們的思路是這樣的:
- 假設有N個執行緒,則將數組數M據分為N組
- 每個執行緒對其中的一組數據使用庫函數提供的快速排序演算法
- 所有執行緒排序完成後,將每組排序好的數組合併
舉個例子,使用4個執行緒對11個數據進行排序:
12,10,4,7,9,6,8,1,5,16,11
由於4不能被10整除,因此,前面三個執行緒,每個負責排序10%(4-1)= 3三個數,最後一個執行緒負責排序最後兩個數。

假設這4個執行緒都完成了自己的工作後,內容如下:

比較每組的第一個,選出最小的一個,這裡是執行緒2的1,放到新數組的下標0處最後由主執行緒將已經排好的每組進行合併:
- 將1放到新的數組最開始的位置,執行緒的下次計較的內容後移,即下次比較時,比較執行緒2的第二個數。
- 循環比較
最終可以得到合併的數據:
1 4 5 6 7 8 9 10 11 12 16
屏障
通過上面的分析,我們需要多個執行緒進行排序後,一起交給主執行緒合併,因此需要有方法等待所有執行緒完成事情之後,再退出。 在《系統編程-多執行緒》中介紹了pthread_join,今天我們使用pthread_barrier_wait。
#include <pthread.h> int pthread_barrier_destroy(pthread_barrier_t *barrier); int pthread_barrier_init(pthread_barrier_t *restrict barrier, const pthread_barrierattr_t *restrict attr, unsigned count); int pthread_barrier_wait(pthread_barrier_t *barrier);
在解釋之前說明一下基本原理,pthread_barrier_wait等待某一個條件達到(計數到達),一旦達到後就會繼續往後執行。當然了,如果你希望各個執行緒完成它自己的工作,主執行緒再進行合併動作,則你等待的數量可以再加一個。:
//來源:公眾號【編程珠璣】 //barrier.c #include <stdio.h> #include <pthread.h> #include <unistd.h> pthread_barrier_t b; void *workThread(void * arg) { printf("thread %dn",*(int*)arg); pthread_barrier_wait(&b); return (void*)0; } int main(void) { int threadNum = 4; int err; /*計數為創建執行緒數+1*/ pthread_barrier_init(&b,NULL,threadNum + 1); int i = 0; pthread_t tid; /*創建多個執行緒*/ for(i = 0;i < threadNum; i++) { err = pthread_create(&tid,NULL,workThread,(void*)&i); if(0 != err) { printf("create thread failedn"); return -1; } printf("tid:%ldn",tid); usleep(10000); } pthread_barrier_wait(&b); printf("all thread finishedn"); /*銷毀*/ pthread_barrier_destroy(&b); return 0; } 其中,pthread_barrier_init用來初始化相關資源,而pthread_barrier_destroy用來銷毀相關資源。 編譯運行:
$ gcc -o barrier barrier.c -lpthread $ ./barrier tid:140323085256448 thread 0 tid:140323076863744 thread 1 tid:140323068471040 thread 2 tid:140323060078336 thread 3 all thread finished
比較函數
為了使用qsort函數,我們需要實現自己的比較函數,參考《高級指針話題-函數指針》和《快速排序你真的會了嗎?》:
//來源:公眾號【編程珠璣】 //https:www.yanbinghu.com /*比較函數*/ int compare(const void* num1, const void* num2) { long l1 = *(long*)num1; long l2 = *(long*)num2; if(l1 == l2) return 0; else if(l1 < l2) return -1; else return 1; } 合併
對於每個執行緒完成它自己的任務之後,需要合併所有內容,關於合併的邏輯前面已經舉例了,這裡不再多介紹。
//來源:公眾號【編程珠璣】 //https://www.yanbinghu.com /*要排序的數組資訊*/ typedef struct SortInfo_t { long startIdx; //數組啟始下標 long num;//要排序的數量 }SortInfo; /*合併執行緒已經排序好的內容*/ void merge(SortInfo *sortInfos,size_t threadNum) { long idx[threadNum]; memset(idx,0,threadNum); long i,minidx,sidx,num; for(i = 0;i < threadNum;i++) { idx[i] = sortInfos[i].startIdx; } for(sidx = 0;sidx < NUM;sidx++) { num = LONG_MAX; for(i = 0;i < threadNum;i++) { if(idx[i] < (sortInfos[i].startIdx + sortInfos[i].num) && (nums[idx[i]] < num)) { num = nums[idx[i]]; minidx = i; } } snums[sidx] = nums[idx[minidx]]; idx[minidx]++; } } 隨機數生成
關於生成方法,參考《隨機數生成的N種姿勢》。
完整程式碼
由於完整程式碼比較長,這裡就不貼出了,有興趣的可以訪問: https://paste.ubuntu.com/p/yT2cm8G55H/ 或閱讀原文查看。
運行結果
對800W數據進行排序,排序時間:
$ threadSort 1 thread num:1 time 2.369488
使用4個執行緒時:
$ threadSort 4 thread num:4 time 1.029097
可以看到速度提升是比較明顯的。
總結
可以看到使用4執行緒排序要比單個執行緒排序快很多,不過以上實現僅供參考,本文例子可能也存在不妥之處,請根據實際數據情況選擇合適的排序演算法。但是,多執行緒就一定快嗎?敬請關注下一篇。
參考:《unix環境高級編程》

