サーバ環境
製品名 | OpenBlockS 600/R |
CPU | 600MHz(AMCC PowerPC 405EX) |
メモリ | 1GB(DDR2 SDRAM) |
ストレージ | 8GB(Compact Flash) |
マルチスレッドによる多重化の特徴
前回紹介したマルチプロセスではクライアントからの接続があるたびに子プロセスを生成していました。今回紹介するマルチスレッドではクライアントからの接続があるたびにスレッドを生成します。
マルチプロセスが送受信処理を子プロセスに任せていたのと同様に、送受信処理をスレッドに任せればよいため、シンプルで分かりやすいコードになります。
具体的には、マルチプロセスでfork()により並列処理を開始したように、マルチスレッドではpthread_create()関数で並列処理を開始します。pthread_create()は呼び出しスレッドと並行して実行する新しい制御スレッドを生成します。pthread_create()にスレッドとして生成したい関数を指定し、その指定された関数から別スレッドになります。
pthread_create()について
#include <pthread.h> int pthread_create(pthread_t * thread, pthread_attr_t * attr, void * (*start_routine)(void *), void * arg);
引数start_routine()関数を新しいスレッドとして生成し、start_routine()の第一引数として引数argを指定できます。
pthread_exit(3) を呼び出すことによって明示的に終了するか、関数 start_routineから返ることで終了します。
引数attrにはその新しいスレッドに適用するスレッド属性を指定します。
スレッド属性について詳細は割愛しますが、NULLを指定すると合流可能・デフォルトのスケジューリング方式になります。
※詳しくはPTHREAD_ATTR_INITについて調べてみてください。
スレッドの生成に成功すると新しく作成したスレッドの識別子が引数threadの指す領域へ格納され、0が返ります。エラーの場合、非 0 のエラーコードが返ります。
前回取り上げた各方式のメリットとデメリットのうち、マルチスレッドについて再度以下に掲載します。
マルチスレッドのメリットとデメリット
メリット |
マルチプロセスに比較してリソースの処理が少ない
時間のかかる処理がある場合に並列性を確保しやすい 他の処理系でも使える場合が多い プログラムがシンプルで分かりやすい |
デメリット |
送受信の連携がやや面倒
デッドロックなどの問題を引き起こしやすい 終了処理の考慮が面倒 プログラム異常時に全体が影響を受ける 各処理の終了時のリソース解放に注意が必要 1プロセスのリソース制限を受ける |
マルチスレッドのコード例
以下の例では、accept()で接続受付を行った後、pthread_create()で送受信専用の関数(send_recv_thread())をスレッド関数として生成しています。
スレッド生成後、呼び出し元のスレッドはループの先頭に戻って再度acceptで接続を待ちます。
送受信処理の終了後にアクセプトソケットのクローズを行う必要がありますが、スレッド終了後に呼び出し元のスレッドでクローズするのは大変なので、スレッド側でクローズを行えるように、アクセプトソケット(acc)をsend_recv_thread()に渡しています。
マルチプロセスではwait()しないとリソースが開放されませんでしたが、同様にマルチスレッドではpthread_join()で終了状態を得ないとリソースが開放されません。ただし、以下のソースで実施しているように、send_recv_thread()でpthread_detach()を行うと、スレッドの資源は終了した時に直ちに解放されるようになります(ただし、pthread_joinで終了状態を得られなくなります)。
それではコード例を載せます。実際はincludeも必要ですが割愛しています。
/* socket→bind→listenを実行する関数。詳細は割愛。 */ int server_sock(const char *portnm) { ... } void accept_loop (int soc) { char hbuf[NI_MAXHOST], sbuf[NI_MAXSERV]; struct sockaddr_storage from; int acc; socklen_t len; pthread_t thread_id; void *send_recv_thread(void *arg); for (;;) { len = (socklen_t) sizeof(from); /* 接続受付 */ if ((acc = accept(soc, (struct sockaddr *) &from, &len)) == -1) { if(errno!=EINTR){ perror("accept"); } } else { (void) getnameinfo((struct sockaddr *) &from, len, hbuf, sizeof(hbuf), sbuf, sizeof(sbuf), NI_NUMERICHOST | NI_NUMERICSERV); (void) fprintf(stderr, "accept:%s:%s\n", hbuf, sbuf); /* スレッド生成 */ if (pthread_create(&thread_id, NULL, send_recv_thread, (void *) acc) != 0) { perror("pthread_create"); } else { (void) fprintf(stderr, "pthread_create:create:thread_id=%d\n", (int)thread_id); } } } } /* 送受信処理を実施する関数。詳細は割愛。 */ void *send_recv_thread(void *arg) { /* スレッドのデタッチ */ (void) pthread_detach(pthread_self()); /* 引数の取得 */ acc = (int) arg; /* 送受信ループ */ for(;;) { クライアントとのrecv/send処理などをここで実施 } /* アクセプトソケットのクローズ */ (void) close(acc); /* スレッド終了 */ pthread_exit((void *) 0); } int main(int argc, char *argv[]) { int soc; /* (void) daemonize(0, 0); */ if (argc <= 1) { (void) fprintf(stderr, "server port\n"); return (EX_USAGE); } if ((soc = server_socket(argv[1])) == -1) { (void) fprintf(stderr, "server_socket(%s):error\n", argv[1]); return (EX_UNAVAILABLE); } (void) fprintf(stderr, "ready for accept\n"); accept_loop(soc); (void) close(soc); return (EX_OK); }