C言語でスレッドを教えて!
こういった悩みにお答えします.
本記事の信頼性
- リアルタイムシステムの研究歴12年.
- 東大教員の時に,英語でOS(Linuxカーネル)の授業.
- 2012年9月~2013年8月にアメリカのノースカロライナ大学チャペルヒル校(UNC)コンピュータサイエンス学部で客員研究員として勤務.C言語でリアルタイムLinuxの研究開発.
- プログラミング歴15年以上,習得している言語: C/C++,Python,Solidity/Vyper,Java,Ruby,Go,Rust,D,HTML/CSS/JS/PHP,MATLAB,Assembler (x64,ARM).
- 東大教員の時に,C++言語で開発した「LLVMコンパイラの拡張」,C言語で開発した独自のリアルタイムOS「Mcube Kernel」をGitHubにオープンソースとして公開.
- 2020年1月~現在はアメリカのノースカロライナ州チャペルヒルにあるGuarantee Happiness LLCのCTOとしてECサイト開発やWeb/SNSマーケティングの業務.2022年6月~現在はアメリカのノースカロライナ州チャペルヒルにあるJapanese Tar Heel, Inc.のCEO兼CTO.
- 最近は自然言語処理AIとイーサリアムに関する有益な情報発信に従事.
- (AI全般を含む)自然言語処理AIの論文の日本語訳や,AIチャットボット(ChatGPT,Auto-GPT,Gemini(旧Bard)など)の記事を50本以上執筆.アメリカのサンフランシスコ(広義のシリコンバレー)の会社でプロンプトエンジニア・マネージャー・Quality Assurance(QA)の業務委託の経験あり.
- (スマートコントラクトのプログラミングを含む)イーサリアムや仮想通貨全般の記事を200本以上執筆.イギリスのロンドンの会社で仮想通貨の英語の記事を日本語に翻訳する業務委託の経験あり.
こういった私から学べます.
C言語を独学で習得することは難しいです.
私にC言語の無料相談をしたいあなたは,公式LINE「ChishiroのC言語」の友だち追加をお願い致します.
私のキャパシティもあり,一定数に達したら終了しますので,今すぐ追加しましょう!
独学が難しいあなたは,元東大教員がおすすめするC言語を学べるオンラインプログラミングスクール5社で自分に合うスクールを見つけましょう.後悔はさせません!
目次
スレッド
スレッドとは,OSにおけるプログラムの実行単位です.
スレッドが管理する情報はプロセスより少ないので,スレッド間のコンテキストスイッチのオーバヘッドも小さいです.
プロセスと比較して,あるスレッドは同じプロセス内の他のスレッド同士でメモリ空間を共有するため,スレッド間のデータのやり取りのオーバヘッドが小さくできます.
同じプロセス内のスレッド同士でメモリ空間を共有するため,あるスレッドは同じプロセス内の他のスレッドのデータを変更すると,正常に動作しなくなることがあります.
なので,マルチスレッドプログラミングでは,スレッド間で共有するデータの読み書きに注意する必要があります.
プロセスの生成と実行を知りたいあなたはこちらからどうぞ.
本記事では,x86-64だけでなくARM64(AARCH64)用のコードがあります.
ARM64用の開発環境の構築は以下の記事を参考にして下さい.
pthreadによるマルチスレッドプログラミング
pthreadによるマルチスレッドプログラミングを紹介します.
1 2 |
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg); |
pthread_create関数は,呼び出し元のスレッドと並行して実行する新しいスレッドを生成する関数です.
新しいスレッドは,argを第1引数とするstart_routineという関数になります.
引数attrには,その新しいスレッドに適用するスレッド属性を指定します.
スレッド属性に関する詳細は,PTHREAD_ATTR_INITに記載されています.
成功すると新しく作成したスレッドの識別子が引数threadの指す領域へ格納され,関数の返り値は0になります.
1 |
int pthread_join(pthread_t th, void **thread_return); |
pthread_join関数は,呼び出し元のスレッドの実行を停止し,thで指定したスレッドが終了するのを待ちます.
thread_returnがNULLでない場合,thの返り値がthread_returnで指し示される領域に格納されます.
pthreadでスレッドを生成して実行するコードは以下になります.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
/* * Author: Hiroyuki Chishiro * License: 2-Clause BSD */ #include <stdio.h> #include <stdlib.h> #include <pthread.h> void *func_thread(void *args) { int *p = (int *) args; printf("func_thread(): *p = %d\n", *p); return NULL; } int main(void) { pthread_t pthread; int ret; int a = 123; if ((ret = pthread_create(&pthread, NULL, &func_thread, &a)) != 0) { fprintf(stderr, "pthread_create(): ret = %d\n", ret); exit(1); } if ((ret = pthread_join(pthread, NULL)) != 0) { fprintf(stderr, "pthread_join(): ret = %d\n", ret); exit(2); } return 0; } |
実行結果は以下になります.
pthreadのライブラリを利用する時は,-lpthreadオプションを付けてコンパイルすることに注意して下さい.
1 2 3 |
$ gcc pthread.c -lpthread $ a.out func_thread(): *p = 123 |
pthreadでスレッドIDの取得
pthreadでスレッドIDを取得する方法を紹介します.
pthread_self関数
1 |
pthread_t pthread_self(void); |
pthread_self関数は,呼び出し元のスレッドのIDを取得する関数です.
ここで,pthread_self関数で取得するスレッドIDはLinuxが管理するスレッドIDではないことに注意して下さい.
ではpthread_self関数の返り値が何かというと,生成したpthreadが利用するスタックのアドレスになります.
このスタックのアドレスの値は,スレッドが利用可能なメモリ領域の最大値もしくは近い値だと思われます.
Linuxが管理するスレッドIDとは,psコマンドを-Lオプション付きで実行した時に表示される軽量プロセス(LWP:Light-Weight Process)のIDのことです.
以下のpsコマンドの実行結果で,PIDがプロセスID,LWPがLinuxが管理するスレッドIDになります.
1 2 3 4 |
$ ps -L PID LWP TTY TIME CMD 4354 4354 pts/1 00:00:01 zsh 8649 8649 pts/1 00:00:00 ps |
pthread_self関数を利用し,x86-64/ARM64の命令でスタックアドレスを取得して表示するコードは以下になります.
17行目でx86-64のmov命令,19行目でARM64のmov命令を利用していることに注意して下さい.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
/* * Author: Hiroyuki Chishiro * License: 2-Clause BSD */ #include <stdio.h> #include <stdlib.h> #include <stdint.h> #include <pthread.h> #define NR_THREADS 3 static inline uintptr_t get_stack_address(void) { uintptr_t stack_address; #if defined(__x86_64__) asm volatile("mov %%rsp, %0" : "=r"(stack_address)); #elif defined(__aarch64__) asm volatile("mov %0, sp" : "=r"(stack_address)); #else #error "Unknown Architecture" #endif return stack_address; } void *func_thread(void *args) { uintptr_t sa = get_stack_address(); pid_t tid = *(pid_t *) args; printf("pthread_self(): tid = %d, 0x%lx\n", tid, pthread_self()); printf("func_thread(): tid = %d, sa = 0x%lx\n", tid, sa); return NULL; } int main(void) { pthread_t pthreads[NR_THREADS]; int ret; pid_t i; pid_t args[NR_THREADS]; for (i = 0; i < NR_THREADS; i++) { args[i] = i; if ((ret = pthread_create(&pthreads[i], NULL, &func_thread, &args[i])) != 0) { fprintf(stderr, "pthread_create(): ret = %d\n", ret); exit(1 + i); } printf("pthread_create(): tid = %d, pthreads = 0x%lx\n", i, pthreads[i]); } for (i = 0; i < NR_THREADS; i++) { if ((ret = pthread_join(pthreads[i], NULL)) != 0) { fprintf(stderr, "pthread_join(): ret = %d\n", ret); exit(1 + NR_THREADS + i); } } return 0; } |
x86-64の実行結果は以下になります.
tid毎のpthread_create関数の第1引数とpthread_self関数の返り値が同じアドレスになっています.
また,func_thread関数でスタックポインタを取得した値saは,上記のアドレスから0x840引いた値になっていることがわかります.
1 2 3 4 5 6 7 8 9 10 11 |
$ gcc pthread_self.c -lpthread $ a.out pthread_create(): tid = 0, pthreads = 0x7f523aee3700 pthread_self(): tid = 0, 0x7f523aee3700 func_thread(): tid = 0, sa = 0x7f523aee2ec0 pthread_create(): tid = 1, pthreads = 0x7f523a6e2700 pthread_self(): tid = 1, 0x7f523a6e2700 func_thread(): tid = 1, sa = 0x7f523a6e1ec0 pthread_create(): tid = 2, pthreads = 0x7f5239ee1700 pthread_self(): tid = 2, 0x7f5239ee1700 func_thread(): tid = 2, sa = 0x7f5239ee0ec0 |
ARM64の実行結果は以下になります.
x86-64と同様に,tid毎のpthread_create関数の第1引数とpthread_self関数の返り値が同じアドレスになっています.
また,x86-64とは異なり,saは上記のアドレスから0x850引いた値になっていることがわかります.
1 2 3 4 5 6 7 8 9 10 11 |
$ aarch64-linux-gnu-gcc pthread_self.c -lpthread $ a.out pthread_create(): tid = 0, pthreads = 0x40012131e0 pthread_self(): tid = 0, 0x40012131e0 func_thread(): tid = 0, sa = 0x4001212990 pthread_create(): tid = 1, pthreads = 0x4001b141e0 pthread_self(): tid = 1, 0x4001b141e0 func_thread(): tid = 1, sa = 0x4001b13990 pthread_create(): tid = 2, pthreads = 0x40023151e0 pthread_self(): tid = 2, 0x40023151e0 func_thread(): tid = 2, sa = 0x4002314990 |
Linux固有のgettid関数
1 |
pid_t gettid(void); |
Linux固有のgettid関数は,Linuxが管理するスレッドIDを取得する関数です.
こちらのスレッドIDはpsコマンドで-Lオプションを付けて実行すると表示されるLWPの値と同じになります.
gettid関数を利用するコードは以下になります.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
/* * Author: Hiroyuki Chishiro * License: 2-Clause BSD */ #define _GNU_SOURCE #include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <unistd.h> #include <sys/types.h> void *func_thread(void *args) { printf("func_thread(): gettid() = %d\n", gettid()); sleep(1); return NULL; } int main(void) { pthread_t pthread; int ret; printf("main(): gettid() = %d\n", gettid()); if ((ret = pthread_create(&pthread, NULL, &func_thread, NULL)) != 0) { fprintf(stderr, "pthread_create(): ret = %d\n", ret); exit(1); } if ((ret = pthread_join(pthread, NULL)) != 0) { fprintf(stderr, "pthread_join(): ret = %d\n", ret); exit(2); } return 0; } |
実行結果は以下になります.
2行目の実行方法「a.out&; ps -L; fg」は少し複雑ですが,セミコロン「;」で区切って以下のようになります.
- a.out&:実行ファイルをバックグラウンドのジョブとして実行
- ps -L:psコマンドでPIDとLWPを表示
- fg:バックグラウンドで実行しているものをフォアグラウンドのジョブとして実行(または再開)
4行目のgettid関数の返り値と8行目のLWP,5行目のgettid関数の返り値と9行目のLWPがそれぞれ同じ値であることがわかります.
1 2 3 4 5 6 7 8 9 10 11 |
$ gcc gettid.c -lpthread $ a.out&; ps -L; fg [1] 10393 main(): gettid() = 10393 func_thread(): gettid() = 10395 PID LWP TTY TIME CMD 4354 4354 pts/1 00:00:02 zsh 10393 10393 pts/1 00:00:00 a.out 10393 10395 pts/1 00:00:00 a.out 10394 10394 pts/1 00:00:00 ps [1] + running a.out |
参考:pthread_create関数の第1引数を利用して生成したスレッドのスレッドIDの取得
pthread_create関数の第1引数pthreadを利用して生成したスレッドのスレッドIDを取得する方法を紹介します.
生成したスレッドのスレッドIDは,x86-64の場合はpthread + 0x2d0,ARM64の場合はpthread + 0xd0が指すアドレスにあります.
この実装は実験的ですので,LinuxカーネルのバージョンやGCCコンパイラのオプションによっては動作しない可能性があることに注意して下さい.
生成したスレッドのスレッドIDを取得するgettid_pthread関数を自作したコードは以下になります.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
/* * Author: Hiroyuki Chishiro * License: 2-Clause BSD */ #define _GNU_SOURCE #include <stdio.h> #include <stdlib.h> #include <stdint.h> #include <pthread.h> #include <unistd.h> #include <sys/types.h> #if defined(__x86_64__) #define OFFSET_PTHREAD_LWP 0x2d0 #elif defined(__aarch64__) #define OFFSET_PTHREAD_LWP 0xd0 #else #error "Unknown Architecture" #endif pid_t gettid_pthread(pthread_t pthread) { return *(uintptr_t *)(pthread + OFFSET_PTHREAD_LWP); } void *func_thread(void *args) { printf("func_thread(): gettid() = %d\n", gettid()); sleep(1); return NULL; } int main(void) { pthread_t pthread; int ret; printf("main(): gettid() = %d\n", gettid()); if ((ret = pthread_create(&pthread, NULL, &func_thread, NULL)) != 0) { fprintf(stderr, "pthread_create(): ret = %d\n", ret); exit(1); } printf("gettid_pthread(0x%lx) = %d\n", pthread, gettid_pthread(pthread)); if ((ret = pthread_join(pthread, NULL)) != 0) { fprintf(stderr, "pthread_join(): ret = %d\n", ret); exit(2); } return 0; } |
x64の実行結果は以下になります.
生成元のスレッドが呼び出しているgettid_pthread関数の返り値と,生成したスレッドが呼び出しているfunc_thread関数内のgettid関数の返り値が,同じスレッドID(11359)になっていることがわかります.
1 2 3 4 5 |
$ gcc gettid_pthread.c -lpthread $ a.out main(): gettid() = 11358 gettid_pthread(0x7f9ead7e6700) = 11359 func_thread(): gettid() = 11359 |
ARM64の実行結果は以下になります.同様です.
1 2 3 4 5 |
$ aarch64-linux-gnu-gcc gettid_pthread.c -lpthread $ a.out main(): gettid() = 11372 gettid_pthread(0x40013131e0) = 11374 func_thread(): gettid() = 11374 |
まとめ
C言語でスレッドの生成と実行方法を紹介しました.
具体的には,pthreadによるマルチスレッドプログラミングとスレッドIDの取得方法を解説しました.
C11規格でマルチスレッドプログラミングを知りたいあなたはこちらからどうぞ.
C言語を独学で習得することは難しいです.
私にC言語の無料相談をしたいあなたは,公式LINE「ChishiroのC言語」の友だち追加をお願い致します.
私のキャパシティもあり,一定数に達したら終了しますので,今すぐ追加しましょう!
独学が難しいあなたは,元東大教員がおすすめするC言語を学べるオンラインプログラミングスクール5社で自分に合うスクールを見つけましょう.後悔はさせません!