1. 程式人生 > >pthread 多執行緒基礎

pthread 多執行緒基礎

本文主要介紹如何通過 `pthread` 庫進行多執行緒程式設計,並通過以下例子進行說明。 + 基於萊布尼茲級數計算 $\pi$ . + 多執行緒歸併排序 參考文章: + [1] https://computing.llnl.gov/tutorials/pthreads ## API 介紹 ### pthread_create 作用:新建一個執行緒。 函式原型: ```c int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg); ``` 引數解析: + `pthread_t *thread` 用於快取新執行緒的 `pid` . + `const pthread_attr_t *attr` 制定新執行緒的 `attr` ,如果為 `NULL`,那麼將使用預設的 `attr` 。 + `start_routine` 是新執行緒即將進入的執行函式。 + `arg` 向新執行緒傳遞的某些引數,一般封裝為結構體傳入。 執行緒的中止可以通過以下方式: + 呼叫 `pthread_exit(void *retval)` , 其中 `retval` 可以通過 `pthread_join` 獲得。 + 在 `start_routine` 函式直接 `return ` . + 該執行緒被取消 (See [pthread_cancel](https://man7.org/linux/man-pages/man3/pthread_cancel.3.html)) . + 執行緒所屬的程序呼叫了 `exit` , 或者該程序的 `main` 函式中執行了 `return` . ### pthread_join 等待某個執行緒結束。 函式原型: ```c int pthread_join(pthread_t thread, void **retval); ``` 引數解析: + `thread` 是某個執行緒的 `pid` . + `retval` 用於獲取執行緒 `start_routine` 的返回值 . 基本用法請看下面的「雙執行緒計算 $\pi$」,該例子同時能夠回答為什麼 `retval` 是 `void**` 型別而不是 `void *` 型別。 ### pthread_attr_t `pthread_attr_t` 的定義如下: ```c struct __pthread_attr { struct sched_param __schedparam; void *__stackaddr; size_t __stacksize; size_t __guardsize; enum __pthread_detachstate __detachstate; enum __pthread_inheritsched __inheritsched; enum __pthread_contentionscope __contentionscope; int __schedpolicy; }; ``` 與之相關的 API,請看: ```bash man pthread_attr ``` ## Examples ### 建立執行緒 下面是一個簡單的多執行緒例子,用於演示 `pthread_create` 和 `pthread_join` 的基本用法。 該例子建立 4 個執行緒,通過 `order[i]` 分別標號,執行緒的工作內容是輸出本執行緒的標號。 所涵蓋的知識點: + 如何建立執行緒 + 如何向執行緒傳遞引數:通過對 `void *arg` 進行強制型別轉換實現。 + `pthread_join` 的作用:如果去掉 `pthread_join` 呼叫,那麼程式很可能是沒有輸出的。因為在進入各個執行緒的 `worker` 函式時,`main` 函式已經結束,這時候所有執行緒都被強制終止。 ```c #include #include const int N = 4; void* worker(void *arg) { int *pid = (int *)arg; printf("%d ", *pid); return NULL; } int main() { pthread_t pid[N] = {0}; const int order[] = {0, 1, 2, 3}; int i = 0; for (; i < N; i++) pthread_create(&pid[i], NULL, worker, (void *)&order[i]); for (i = 0; i < N; i++) pthread_join(pid[i], NULL); return 0; } ``` ### 雙執行緒計算 π 要求: + 基於萊布尼茲級數:1 - 1/3 + 1/5 - 1/7 + 1/9 - ... = PI/4 + 使用主執行緒 + 輔助執行緒的方式 涵蓋知識點: + 如何向不同的執行緒傳遞不同引數 + 如何獲取執行緒的結果 程式碼實現: ```c #include #include #include const int N = 1e8; typedef struct { int start, end; } param_t; typedef struct { double value; } result_t; void *worker(void *arg) { param_t *param = (param_t *)arg; result_t *res = (result_t *)malloc(sizeof(result_t)); int i = param->start; for (; i <= param->end; i++) { if (i % 2) res->value += 1.0 / (2 * i - 1); else res->value -= 1.0 / (2 * i - 1); } return res; } double master(void *arg) { double res = 0.0; param_t *param = (param_t *)arg; int i = param->start; for (; i <= param->end; i++) { if (i % 2) res += 1.0 / (2 * i - 1); else res -= 1.0 / (2 * i - 1); } return res; } int main() { pthread_t tid = 0; param_t p1 = {1, N / 2}, p2 = {N / 2 + 1, N}; pthread_create(&tid, NULL, worker, &p2); double val = master(&p1); result_t *res = NULL; pthread_join(tid, (void **)&res); printf("PI = %f\n", 4 * (val + res->value)); free(res); } ``` ### 多執行緒計算 π 要求: + 適應 N 核心的 CPU + 不能使用全域性變數,必須通過傳遞引數與 `join` 獲取返回值實現 程式碼實現: ```c #include #include #include const int N = 1e3; const int NR_CPU = 8; typedef struct { int start, end; } param_t; typedef struct { double value; } result_t; void *worker(void *arg) { param_t *p = (param_t *)arg; int i = p->start; result_t *res = malloc(sizeof(result_t)); for (; i < p->end; i++) { if (i % 2) res->value += 1.0 / (2 * i - 1); else res->value -= 1.0 / (2 * i - 1); } return res; } int main() { param_t params[NR_CPU]; pthread_t pids[NR_CPU] = {0}; const int step = N / NR_CPU; int i = 0; for (; i < NR_CPU; i++) { params[i].start = i * step + 1; params[i].end = params[i].start + step; } params[NR_CPU - 1].end = N; for (i = 0; i < NR_CPU; i++) pthread_create(&pids[i], NULL, worker, ¶ms[i]); result_t *res = NULL; double pi = 0.0; for (i = 0; i < NR_CPU; i++) { pthread_join(pids[i], (void **)&res); pi += res->value; if (res) free(res), res = NULL; } pi *= 4; printf("PI = %f\n", pi); return 0; } ``` ### 多執行緒歸併排序 要求: + 把陣列分為若干個區間,每個區間單獨通過一個執行緒排序。 + 最後在主執行緒,所有區間通過歸併完成排序。 ```c #include #include #include #include #include const int N = 1e6; const int NR_CPU = 4; typedef struct { int *nums; int start, end; } param_t; int check(const int *nums, int len) { int i = 1; for (; i < len; i++) if (nums[i] < nums[i - 1]) return 0; return 1; } int cmp(const void *a, const void *b) { return (*(int *)a) - (*(int *)b); } void *worker(void *arg) { param_t *p = (param_t *)arg; int *start = p->nums + p->start; int n = p->end - p->start; qsort(start, n, sizeof(int), cmp); return NULL; } // merge [start, mid) and [mid, end) void merge(const int *nums, int start, int mid, int end) { int *p = malloc(sizeof(int) * (end - start)); const int *p1 = nums + start, *p2 = nums + mid; int len1 = mid - start, len2 = end - mid; int idx = 0, i = 0, j = 0; while (i < len1 && j < len2) { if (p1[i] < p2[j]) p[idx++] = p1[i++]; else p[idx++] = p2[j++]; } while (i < len1) p[idx++] = p1[i++]; while (j < len2) p[idx++] = p2[j++]; memcpy((void *)(nums + start), (void *)p, sizeof(int) * idx); } int main() { srand(time(NULL)); int nums[N] = {0}; int i = 0; for (; i < N; i++) nums[i] = random() % N; param_t params[NR_CPU]; int step = N / NR_CPU; for (i = 0; i < NR_CPU; i++) { params[i].nums = nums; params[i].start = i * step; params[i].end = params[i].start + step; } params[NR_CPU - 1].end = N; pthread_t pids[NR_CPU] = {0}; for (i = 0; i < NR_CPU; i++) pthread_create(&pids[i], NULL, worker, ¶ms[i]); for (i = 0; i < NR_CPU; i++) pthread_join(pids[i], NULL); while (step < N) { int start = 0; while (start < N) { int mid = start + step; int end = mid + step; if (mid > N) mid = N; if (end > N) end = N; merge(nums, start, mid, end); start = end; } step *= 2; } assert(check(nums, N))