先月(2018年12月)からマルチスレッドプログラミングを学び始めたのですが、調べた内容や実践した内容を少しずつ書いていきたいと思います。
私は、プログラムは C言語で書き、スレッドライブラリは Pthreads を使いました。コンパイラは GCC(GNU Compiler Collection) を使用。詳しい情報はまた後で書きます。
マルチスレッドプログラミングを学ぶ理由
私個人がマルチスレッドプログラミングを学び始めた理由は、膨大な計算量を短時間でさばけるプログラムを作れるようになりたかったからです。これには、機械学習を扱い始めたことが強く影響していると思います。
とはいえ、マルチスレッドプログラミングには実は昔(2016年頃)から興味はあったものの、ずっと手を出せずにいました。
そして最近、業務でも使う可能性が出てきたことで、やっと本格的に学び始めたという形です。
Pthreads とは何か
Pthreads は、POSIX の仕様に基づいたスレッドの実装です。
…といってもこれだけの説明では分かりにくいですね。
POSIX とは、Portable Operating System Interface for UNIX の略のようです。
POSIX は、UNIX系OS の互換性維持のための標準仕様で、OS の各種インタフェースを定めたものです(例: システムコール、標準ライブラリ関数、コマンド体系)。
POSIX の中には、プロセスやスレッドの仕様も含まれており、Pthreads は POSIX の定めるスレッド仕様に基づいた C言語のマルチスレッドプログラミング用ライブラリです。
Windows でも動かせた
上記の通り、POSIX は UNIX系OS のための規格のはず。そのため、私は Windows では Pthreads は使えないと思い込んでいて、「Windows しか持っていないけど、マルチスレッドプログラミングの実践、どうしよう」などと考えていました。
しかし、手元の Windows7 で試してみたところ、実は Pthreads は使えることが分かりました。
環境
OS:Windows 7
GCC: 8.1.0
最近新しい MinGW を入れました。手元の PC ではずっと GCC が 4.6.1 のままだったのですが、これにより一気に新しくなりました。
※ ただ、更新の動機としては C でなく C++ のためだった気がします。。
MinGW というのは、Windows 用のネイティブアプリを開発するための最小限の環境です。”GNU” で使える開発者向けの機能が Windows でも使える開発環境といったイメージです。
(※ 新しいバージョンがほしければ、MinGW でなく MinGW-w64 のサイトに行く必要がある点と、環境変数の設定で実は少しハマってしまいました。MinGW-w64 のインストールの際、”Threads” という設定項目で “posix” を選んだのですが、これによって Pthreads が使えるようになったと考えています。)
簡単な実験プログラム
処理がマルチスレッドになるかどうか、簡単なプログラムで試してみます。
処理の内容は、ただループさせて数字をインクリメントし、画面に表示させるだけです。
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 |
/* test_pthreads.c */ #include <stdio.h> #include <pthread.h> void* thread_func( void *args ) { int n = 0; printf( "thread_func;\n" ); for(; n<20 ; n++) { printf("[thread_func] %d\n", n); } return NULL; } int main(void) { int cnt = 0; pthread_t thread; printf("main;\n"); /* thread_func 関数を新たなスレッドで呼び出す */ pthread_create( &thread, NULL, thread_func, (void *)NULL ); for(; cnt<60 ; cnt++) { if ( cnt >= 10) { printf("[main]%d\n", cnt); } } pthread_join( thread, NULL ); return 0; } |
このソースコードを、次のコマンドでコンパイルします。
$ gcc -o test_pthreads -pthread test_pthreads.c
※ -o オプションの値により、実行ファイルのファイル名は test_pthreads.exe となります。
プログラムの実行結果は、次のようになりました。(途中まで記載)
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 |
main; [main]10 [main]11 [main]12 thread_func; [thread_func] 0 [thread_func] 1 [thread_func] 2 [thread_func] 3 [thread_func] 4 [thread_func] 5 [main]13 [main]14 [main]15 [thread_func] 6 [thread_func] 7 [thread_func] 8 [main]16 [main]17 [thread_func] 9 [thread_func] 10 [main]18 [main]19 [thread_func] 11 [thread_func] 12 [main]20 [main]21 [main]22 [main]23 [main]24 [main]25 [main]26 [main]27 [main]28 [thread_func] 13 [thread_func] 14 [thread_func] 15 [main]29 [main]30 |
main 関数による数字の表示と、thread_func 関数による数字の表示が交互にあらわれているのがわかります。
何度か同じプログラムを実行しましたが、表示の順序はそのたびに変化しました。処理が並列化されることで、順序の制約がはずれ、揺れ動くようになります。
なお、コンパイル時に -pthread オプションを付けない場合は、並列処理ではなくなるかと思っていましたが、試してみるとそうでもないようです。この点については今後、もう少し調べてみようと思っています。
上記プログラムの仕組み
今回の並列化の仕組みは、Pthreads が提供してくれるごく少数の API を使って実現されています。上記プログラムで使っているのは次の2つのみです。
- pthread_create 関数 (新しいスレッドの開始)
- pthread_join 関数 (スレッドの同期)
これらの関数は、pthread.h をインクルードして使います。
そして、これらの関数は、 pthread_t 型の変数を引数として渡して使う仕様になっています。1スレッドごとに、pthread_t 型の変数が1つ必要になります。
今回は、新しいスレッドで行いたい処理を thread_func という関数(関数名は自由です)に書いており、この thread_func 関数を、pthread_create 関数により、新しいスレッドで実行させています。
また pthread_join 関数を呼び出すと、指定したスレッドの処理が終了するまで待機します。今回の場合、 thread_func の処理が終了するまで、main 関数の処理は先に進まず待ってくれます。
このような待機処理を行うことを、しばしば「スレッド間の同期をとる」と表現するようです。
まだマルチスレッドプログラミングのほんの入り口ですが、今回はここまでにしたいと思います。
追記
先にマルチスレッドの記事を書いてしまいましたが、実は今、Numpy と pandas についての書きかけの記事があるので、早めにそちらを仕上げなくてはと思っています。pandas についての記憶が薄れてしまわないうちに書いておきたいところです。
依然として、日々の生活は情報のインプットの割合が高く、なかなかアウトプットのほうに時間を割けていない状況ですが、精進しなくては。