一般的なスレッド: POSIX スレッドの説明: 第3回

条件変数を使ってもっと効率的に

POSIX スレッドについての 3 回シリーズの最後となるこの記事では、条件変数の使用法について Daniel がわかりやすく説明します。条件変数は POSIX スレッドの構造体です。条件変数を使用すると、特定の条件が満たされたときにスレッドを「目覚めさせる」ことができます。これは、スレッド・セーフな形でのシグナル送信と考えることができるでしょう。Daniel は記事を締めくくるにあたって、すでに学んだ事柄を使ってマルチスレッドの作業班アプリケーションを実装する方法を示します。

Daniel Robbins (drobbins@gentoo.org), President/CEO, Gentoo Technologies, Inc.

Daniel Robbins氏は、ニューメキシコ州アルバカーキーに住んでいます。彼は、Gentooプロジェクトのチーフ・アーキテクト、Gentoo Technologies Inc. の社長/CEOです。著書に、Macmillanから出版されているCaldera OpenLinux Unleashed、SuSE Linux Unleashed、Samba Unleashed があります。Daniel氏は、小学2年のとき初めてLogoプログラム言語や、中毒になる恐れのあったPac Manに出会って以来、何らかの形でコンピューターに関係してきています。これで、彼がなぜSONY Electronic Publishing/Psygnosisでリード・グラフィック・アーチストを務めているかが分かるでしょう。愛妻Maryさんや、生まれたばかりの愛娘Hadassahちゃんとの時間をとても大切にしています。彼の連絡先はdrobbins@gentoo.org です。



2000年 9月 01日

条件変数についての説明

前回の記事では、ジレンマで記事を締めくくりました - ある特定の条件が真になるのを待つような状況では、スレッドはどのように対処するのでしょうか。たとえば mutex のロックとロック解除を繰り返して、そのたびに共用データ構造体の中の特定の値をチェックするという方法もあるでしょう。しかしこれは時間とリソースの浪費であり、こうした頻繁なポーリングはまったく非効率的です。最も良い方法は、pthread_cond_wait() 呼び出しを使って、特定の条件が真になるのを待機することです。

pthread_cond_wait() が何をするかを理解することは重要です。これは POSIX スレッドのシグナル・システムの本質にかかわる事柄であり、最も難解な部分でもあるからです。

まず、こんなシナリオを考えてください。あるスレッドがリンク・リストを調べるために mutex をロックしましたが、リストはたまたま空です。このスレッドは何もすることができません。スレッドはリストからノードを除去するよう設計されているのですが、ノードが 1 つもありません。そこで、このスレッドは以下のような動作をします。

このスレッドは、mutex ロックを保持している間に pthread_cond_wait(&mycond,&mymutex) を呼び出します。pthread_cond_wait() 呼び出しはやや複雑なので、段階を追って 1 つずつ操作を見ていくことにしましょう。

まず最初に pthread_cond_wait() が行うのは、同時に mymutex という mutex をロック解除し (こうして、他のスレッドがリンク・リストを変更できるようになります)、さらに条件 mycond を待機することです (別のスレッドが これを「シグナル送信」したとき、pthread_cond_wait() は目覚めます)。ここで、mutex がすでにロック解除され、他のスレッドがリンク・リストにアクセスして変更 (たとえば項目の追加) を行えるようになっていることに注意してください。

この時点では、pthread_cond_wait() 呼び出しに対してまだ何も応答されていません。mutex はただちにロック解除されますが、通常、条件 mycond の待機によって操作が止まります。つまりこのスレッドはスリープ状態に入り、次に目覚めるまで CPU サイクルをいっさい消費しません。これはまさに理想的な動作です。スレッドはスリープ状態にあり、特定の条件が真になるのを待っています。その間、CPU 時間を浪費する頻繁なポーリングのようなことはいっさい行いません。スレッド側にとってみれば、単に pthread_cond_wait() 呼び出しが応答されるのを待っているだけです。

さて、説明を続けましょう。ある別のスレッド (「スレッド 2」とします) が mymutex をロックして、リンク・リストに項目を追加するとします。スレッド 2 はこの mutex をロック解除した直後に、関数 pthread_cond_broadcast(&mycond) を呼び出します。そうすることによって、スレッド 2 は、条件変数 mycond を待っているすべてのスレッドをただちに目覚めさせます。つまり、pthread_cond_wait() を呼び出したままの状態である最初のスレッドが、この時点で目覚めます。

ここで、最初のスレッドに何が起きるか見てみましょう。スレッド 2 が pthread_cond_broadcast(&mymutex) を呼び出した後、ただちにスレッド 1 の pthread_cond_wait() が応答すると読者はお考えかもしれませんが、実はそうではありません。pthread_cond_wait() は最後にもう 1 つの操作を実行します。それは mymutex の再ロックです。pthread_cond_wait() はロックを保持した後にはじめて応答し、スレッド 1 が処理を続行することを可能にします。この時点で、スレッド 1 はただちにリストを検査して変更があるかどうか調べられるようになります。


もう一度、復習を

かなりややこしいので、復習しておきしましょう。最初のスレッドは、以下を呼び出しました。

pthread_mutex_lock(&mymutex);

続いて、このスレッドはリストを検査しましたが、何も見付からなかったので、以下を呼び出します。

pthread_cond_wait(&mycond, &mymutex);

この pthread_cond_wait() 呼び出しは、多数の操作を実行した後、以下を戻します。

pthread_mutex_unlock(&mymutex);

こうしてスレッドは mymutex をロック解除した後、スリープ状態に入り、mycond が POSIX スレッドの「シグナル」を受け取るのを待ちます。そして「シグナル」を受け取ったときに目覚めます (なお、ここでは UNIX シグナルではなく、pthread_cond_signal() または pthread_cond_broadcast() 呼び出しからのシグナルについて議論していますから、シグナルにかぎ括弧を付けて表現します)。しかし pthread_cond_wait() はすぐには応答しません。応答の前に、もう 1 つの操作、つまり mutex の再ロックを実行します。

pthread_mutex_lock(&mymutex);

pthread_cond_wait() は、mymutex の「背後にある」変更が探査されることを知っているので、わざわざロックを取得してくれるわけです。その後、応答を行います。


pthread_cond_wait() クイズ

これで pthread_cond_wait() 呼び出しについての復習を終わりますが、しくみを理解していただけたことと思います。pthread_cond_wait() が実行するすべての操作を、きっと順番に暗唱することができるでしょう。ちょっと、やってみてください。もし pthread_cond_wait() が理解できたら、あとは簡単です。ですから、きちんと暗記できるまで、上のセクションを繰り返し読んでください。さて、完全に覚えることができたら、ここで問題です。mutex は、pthread_cond_wait() 呼び出しの前に、どんな状態でなければなりませんか ? そして pthread_cond_wait() から応答された後、mutex はどんな状態にありますか。答えはどちらも「ロックされている」です。さて、pthread_cond_wait() 呼び出しについて完全に理解していただけたなら、次はもっと簡単な事柄に移ります。初期化について、および実際のシグナル送信プロセスとブロードキャスト・プロセスについて説明します。それに続き、マルチスレッドの作業キューを形成する、おなじみの C コードをたっぷりと紹介します。


初期化と終結処置

条件変数は実際のデータ構造体ですから、初期化する必要があります。初期化する方法は、まず条件変数を以下のように定義、つまり割り振ります。

pthread_cond_t mycond;

続いて、以下を呼び出して初期化します。

pthread_cond_init(&mycond,NULL);

そうすると、ほら、初期化のできあがりです。さて、条件変数を割り振り解除つまり除去するには、以下のようにして、これを破棄する必要があります。

pthread_cond_destroy(&mycond);

簡単でしょう。次に、pthread_cond_wait() 呼び出しを見てみましょう。


待機

mutex と条件変数の初期化が終わったら、次のようにして、条件を待機することができます。

pthread_cond_wait(&mycond, &mymutex);

ここで、コードの中で mycond と mymutex を論理的にグループ分けしなければならないことに注意してください。1 つの特定の条件ごとに、ただ 1 つの mutex のみを対応させる必要があります。そして条件変数は、mutex データ「内部」の特定の条件の変化を表現するものでなければなりません。ある特定の mutex に対して、たくさんの条件変数 (たとえば cond_empty、cond_full、cond_cleanup など) を指定することができますが、1 つの条件変数に関連付けることのできる mutex はただ 1 つだけです。


シグナル送信とブロードキャスト

シグナル送信とブロードキャストを理解するのはいたって簡単です。スレッドがいずれかの共用データを変更するとき、待機中のすべてのスレッドを目覚めさせたい場合には、pthread_cond_broadcast 呼び出しを以下のように使用することができます。

pthread_cond_broadcast(&mycond);

場合によっては、アクティブなスレッドが、単に最初にスリープ状態に入ったスレッドだけを目覚めさせればよいという状況もあります。たとえば、作業ジョブを 1 つだけキューに追加した場合を考えてください。その場合、次のようにして、ただ 1 つの作業スレッドを目覚めさせるだけで よいのです (他のスレッドまで起こしてしまうなんて、無礼です !):

pthread_cond_signal(&mycond);

これで、ただ 1 つのスレッドのみが目覚めます。もし、POSIX スレッド標準で、スリープから目覚めさせるスレッドの数を整数で指定することができるなら、もっと便利になるのですが。残念ながら、私は会議に招かれませんでした。


作業班

マルチスレッドの作業班を作成する方法を示しましょう。この場合、たくさんの作業スレッドが作成されることになります。それぞれの作業スレッドは、wq ("work queue" つまり作業キュー) を調べて、完了する必要のある作業があるかどうかを確認します。もし作業が存在すれば、キューからノードが除去され、この特定の作業バンドルが実行された後、スレッドは次の新しい作業が到着するのを待ちます。

その間、メイン・スレッドは、これらの作業スレッドの作成、キューへの作業の追加、および (終了時間になった場合) すべての作業スレッドの収集を担当します。ここからは、C コードがいやというほど出てきますから、覚悟してください。


キュー

2 つの理由から、キューが必要でした。まず、作業ジョブを保持するのにキューが必要です。さらに、終了したスレッドの追跡に使うことができるデータ構造体も必要でした。これまでのシリーズ記事で (この記事の終わりの「参考文献」を参照)、特定の TID (スレッド ID) と pthread_join する必要があると 述べたことを覚えていらっしゃいますか。「終結処置キュー」("cq") を使用すれば、終了した任意の スレッドを待機できないという問題を解決できます (これについては、後で詳しく述べます)。私の標準的なキュー・コードは次のとおりです。これらのコードを、ファイル queue.h および queue.c として保存してください。

queue.h
/* queue.h
** Copyright 2000 Daniel Robbins, Gentoo Technologies, Inc.
** Author: Daniel Robbins
** Date: 16 Jun 2000
*/
typedef struct node {
  struct node *next;
} node;
typedef struct queue {
  node *head, *tail; 
} queue;
void queue_init(queue *myroot);
void queue_put(queue *myroot, node *mynode);
node *queue_get(queue *myroot);
queue.c
/* queue.c
** Copyright 2000 Daniel Robbins, Gentoo Technologies, Inc.
** Author: Daniel Robbins
** Date: 16 Jun 2000
**
** This set of queue functions was originally thread-aware.  I
** redesigned the code to make this set of queue routines
** thread-ignorant (just a generic, boring yet very fast set of queue
** routines).  Why the change?  Because it makes more sense to have
** the thread support as an optional add-on.  Consider a situation
** where you want to add 5 nodes to the queue.  With the
** thread-enabled version, each call to queue_put() would
** automatically lock and unlock the queue mutex 5 times -- that's a
** lot of unnecessary overhead.  However, by moving the thread stuff
** out of the queue routines, the caller can lock the mutex once at
** the beginning, then insert 5 items, and then unlock at the end.
** Moving the lock/unlock code out of the queue functions allows for
** optimizations that aren't possible otherwise.  It also makes this
** code useful for non-threaded applications.
**
** We can easily thread-enable this data structure by using the
** data_control type defined in control.c and control.h.  */
#include <stdio.h>
#include "queue.h"
void queue_init(queue *myroot) {
  myroot->head=NULL;
  myroot->tail=NULL;
}
void queue_put(queue *myroot,node *mynode) {
  mynode->next=NULL;
  if (myroot->tail!=NULL)
    myroot->tail->next=mynode;
  myroot->tail=mynode;
  if (myroot-&gt:head==NULL)
    myroot->head=mynode;
}
node *queue_get(queue *myroot) {
  //get from root
  node *mynode;
  mynode=myroot->head;
  if (myroot->head!=NULL)
    myroot->head=myroot->head->next;
  return mynode;
}

The data_control コード

私はスレッド・セーフのキュー・ルーチンを書く代わりに、どんな種類のデータ構造体でもスレッド処理可能にするような 「データ・ラッパー」つまり「制御」構造体を作成しました。では control.h について見てみましょう。

control.h
#include<pthread.h>
typedef struct data_control {
  pthread_mutex_t mutex;
  pthread_cond_t cond;
  int active;
} data_control;

data_control struct の定義をご覧いただいた後は、そのビジュアル表記を示しましょう。

data_control 構造体の使用法
data_control 構造体の使用法

図の中の鍵 (ロック) は、データ構造体への排他アクセスを可能にする mutex を表しています。黄色い星印は、条件変数を表します。条件変数を使って、特定のデータ構造体が変更されるまでスリープ状態にすることができます。そして on/off スイッチは、このデータがアクティブ状態かどうかをスレッドに知らせる「アクティブ」整数 (int) を表しています。私のコードでは、いつシャットダウンすべきかを作業キューに対して示すフラグとして active int を使ってます。次に control.c です。

control.c
/* control.c
** Copyright 2000 Daniel Robbins, Gentoo Technologies, Inc.
** Author: Daniel Robbins
** Date: 16 Jun 2000
**
** These routines provide an easy way to make any type of
** data-structure thread-aware.  Simply associate a data_control
** structure with the data structure (by creating a new struct, for
** example).  Then, simply lock and unlock the mutex, or
** wait/signal/broadcast on the condition variable in the data_control
** structure as needed.
**
** data_control structs contain an int called "active".  This int is
** intended to be used for a specific kind of multithreaded design,
** where each thread checks the state of "active" every time it locks
** the mutex.  If active is 0, the thread knows that instead of doing
** its normal routine, it should stop itself.  If active is 1, it
** should continue as normal.  So, by setting active to 0, a
** controlling thread can easily inform a thread work crew to shut
** down instead of processing new jobs.  Use the control_activate()
** and control_deactivate() functions, which will also broadcast on
** the data_control struct's condition variable, so that all threads
** stuck in pthread_cond_wait() will wake up, have an opportunity to
** notice the change, and then terminate.
*/
#include "control.h"
int control_init(data_control *mycontrol) {
  int mystatus;
  if (pthread_mutex_init(&(mycontrol->mutex),NULL))
    return 1;
  if (pthread_cond_init(&(mycontrol->cond),NULL))
    return 1;
  mycontrol->active=0;
  return 0;
}
int control_destroy(data_control *mycontrol) {
  int mystatus;
  if (pthread_cond_destroy(&(mycontrol->cond)))
    return 1;
  if (pthread_cond_destroy(&(mycontrol->cond)))
    return 1;
  mycontrol->active=0;
  return 0;
}
int control_activate(data_control *mycontrol) {
  int mystatus;
  if (pthread_mutex_lock(&(mycontrol->mutex)))
    return 0;
  mycontrol->active=1;
  pthread_mutex_unlock(&(mycontrol->mutex));
  pthread_cond_broadcast(&(mycontrol->cond));
  return 1;
}
int control_deactivate(data_control *mycontrol) {
  int mystatus;
  if (pthread_mutex_lock(&(mycontrol->mutex)))
    return 0;
  mycontrol->active=0;
  pthread_mutex_unlock(&(mycontrol->mutex));
  pthread_cond_broadcast(&(mycontrol->cond));
  return 1;
}

デバッグの時間

大物を捕まえる前に、もう 1 つ小さなファイルを見てください。dbug.h です。

dbug.h
#define dabort() \
 {  printf("Aborting at line %d in source file %s\n",__LINE__,__FILE__); abort(); }

このコードは、作業班コードに発生した回復不能エラーを処理するために使われます。


作業班コード

作業班コードに触れたところで、いよいよそれを見てみましょう。

workcrew.c
#include <stdio.h>
#include <stdlib.h>
#include "control.h"
#include "queue.h"
#include "dbug.h"
/* the work_queue holds tasks for the various threads to complete. */
struct work_queue {
  data_control control;
  queue work;
} wq;

/* I added a job number to the work node.  Normally, the work node
   would contain additional data that needed to be processed. */
typedef struct work_node {
  struct node *next;
  int jobnum;
} wnode;
/* the cleanup queue holds stopped threads.  Before a thread
   terminates, it adds itself to this list.  Since the main thread is
   waiting for changes in this list, it will then wake up and clean up
   the newly terminated thread. */
struct cleanup_queue {
  data_control control;
  queue cleanup;
} cq;
/* I added a thread number (for debugging/instructional purposes) and
   a thread id to the cleanup node.  The cleanup node gets passed to
   the new thread on startup, and just before the thread stops, it
   attaches the cleanup node to the cleanup queue.  The main thread
   monitors the cleanup queue and is the one that performs the
   necessary cleanup. */
typedef struct cleanup_node {
  struct node *next;
  int threadnum;
  pthread_t tid;
} cnode;
void *threadfunc(void *myarg) {
  wnode *mywork;
  cnode *mynode;
  mynode=(cnode *) myarg;
  pthread_mutex_lock(&wq.control.mutex);
  while (wq.control.active) {
    while (wq.work.head==NULL & wq.control.active) {
      pthread_cond_wait(&wq.control.cond, &wq.control.mutex);
    }
    if (!wq.control.active) break;
    //we got something!
    mywork=(wnode *) queue_get(&wq.work);
    pthread_mutex_unlock(&wq.control.mutex);
    //perform processing...
    printf("Thread number %d processing job %d\n",mynode->threadnum,mywork->jobnum);
    free(mywork);
    pthread_mutex_lock(&wq.control.mutex);
  }
  pthread_mutex_unlock(&wq.control.mutex);
  pthread_mutex_lock(&cq.control.mutex);
  queue_put(&cq.cleanup,(node *) mynode);
  pthread_mutex_unlock(&cq.control.mutex);
  pthread_cond_signal(&cq.control.cond);
  printf("thread %d shutting down...\n",mynode->threadnum);
  return NULL;
  
}
#define NUM_WORKERS 4
int numthreads;
void join_threads(void) {
  cnode *curnode;
  printf("joining threads...\n");
  while (numthreads) {
    pthread_mutex_lock(&cq.control.mutex);
    /* below, we sleep until there really is a new cleanup node.  This
       takes care of any false wakeups... even if we break out of
       pthread_cond_wait(), we don't make any assumptions that the
       condition we were waiting for is true.  */
    while (cq.cleanup.head==NULL) {
      pthread_cond_wait(&cq.control.cond,&cq.control.mutex);
    }
    /* at this point, we hold the mutex and there is an item in the
       list that we need to process.  First, we remove the node from
       the queue.  Then, we call pthread_join() on the tid stored in
       the node.  When pthread_join() returns, we have cleaned up
       after a thread.  Only then do we free() the node, decrement the
       number of additional threads we need to wait for and repeat the
       entire process, if necessary */
      curnode = (cnode *) queue_get(&cq.cleanup);
      pthread_mutex_unlock(&cq.control.mutex);
      pthread_join(curnode->tid,NULL);
      printf("joined with thread %d\n",curnode->threadnum);
      free(curnode);
      numthreads--;
  }
}

int create_threads(void) {
  int x;
  cnode *curnode;
  for (x=0; x<NUM_WORKERS; x++) {
    curnode=malloc(sizeof(cnode));
    if (!curnode)
      return 1;
    curnode->threadnum=x;
    if (pthread_create(&curnode->tid, NULL, threadfunc, (void *) curnode))
      return 1;
    printf("created thread %d\n",x);
    numthreads++;
  }
  return 0;
}
void initialize_structs(void) {
  numthreads=0;
  if (control_init(&wq.control))
    dabort();
  queue_init(&wq.work);
  if (control_init(&cq.control)) {
    control_destroy(&wq.control);
    dabort();
  }
  queue_init(&wq.work);
  control_activate(&wq.control);
}
void cleanup_structs(void) {
  control_destroy(&cq.control);
  control_destroy(&wq.control);
}

int main(void) {
  int x;
  wnode *mywork;
  initialize_structs();
  /* CREATION */
  if (create_threads()) {
    printf("Error starting threads... cleaning up.\n");
    join_threads();
    dabort();
  }
  pthread_mutex_lock(&wq.control.mutex);
  for (x=0; x<16000; x++) {
    mywork=malloc(sizeof(wnode));
    if (!mywork) {
      printf("ouch! can't malloc!\n");
      break;
    }
    mywork->jobnum=x;
    queue_put(&wq.work,(node *) mywork);
  }
  pthread_mutex_unlock(&wq.control.mutex);
  pthread_cond_broadcast(&wq.control.cond);
  printf("sleeping...\n");
  sleep(2);
  printf("deactivating work queue...\n");
  control_deactivate(&wq.control);
  /* CLEANUP  */
  join_threads();
  cleanup_structs();
}

コードの概要説明

では、コードを手短かに説明しましょう。最初に定義される struct は "wq" といい、data_control および queue ヘッダーを含んでいます。data_control 構造体は、キュー内のノードを含む、キュー全体へのアクセスを調停するために使われます。次の作業は、実際の作業ノードを定義することです。この記事に収まるようにコードの長さを縮めるため、ここでは単にジョブ番号だけを示しています。

次は、終結処置キューの作成です。このしくみについては、コメント部分をご覧ください。では、次に the threadfunc()、join_threads()、create_threads()、および initialize_structs() 呼び出しを飛ばして、main() の説明に移ります。最初に行うなうのは、構造体の初期化です。つまり data_control とキューの初期化、および作業キューのアクティブ化を行います。


終結処置スペシャル

次はスレッド初期化の時間です。create_threads() 呼び出しを見てみると、1 つの事柄を除いて特別なものはありません。ここでは終結処置ノードを割り振って、その threadnum (スレッド番号 ) コンポーネントおよび TID (スレッド ID) コンポーネントを初期化していることに注意してください。また、それぞれの新しい作業スレッドに、終結処置ノードを初期引数として渡します。なぜ、このようなことをするのでしょうか ?

それは、各作業スレッドの終了の際、作業スレッドはそれぞれの終結処置ノードを終結処置キューに 付加したうえで終了するからです。その後、メイン・スレッドは、終結処置キューに追加がなされたことを (条件変数によって) 検出し、そのノードをデキューします。TID (スレッド id) が終結処置ノードに保管されているため、メイン・スレッドは、厳密にどのスレッドが終了したかを知ることができます。次に、メイン・スレッドは pthread_join(tid) を呼び出し、適切な作業スレッドと結合 (マージ) します。このように記録を取っておかないと、メイン・スレッドは何らかの不確かな順序で (おそらく、それぞれの作業スレッドが作成された順序で)、作業スレッドと結合しなければなりません。スレッドはこの順序で終了するとは限らないので、メイン・スレッドがあるスレッドとの結合を待っている間、そのスレッドはまた別の 10 個のスレッドとの結合を待つというようなことが起きかねません。今回のような設計上の決定が、とくに何百もの作業スレッドを使用するような場合に、シャットダウン・コードのスピードアップに本当に役立つことがおわかりいただけるでしょうか。


作業の作成

さて、作業スレッドが開始して (このあとすぐ説明する) threadfunc() を実行している間、メイン・スレッドは作業キューの中に項目を挿入し始めます。まずメイン・スレッドは作業キュー (wq) の制御 mutex をロックし、16000 個の作業パケットを割り振って、それらを 1 つずつキューに入れます。その後、pthread_cond_broadcast() が呼び出されて、スリープ状態のすべてのスレッドが目覚めて作業を行えるようになります。次にメイン・スレッドが 2 秒間スリープに入り、その後、作業キューを非アクティブ化して、作業スレッドに対して終了するよう指示します。それから、メイン・スレッドは join_threads() 関数を呼び出して、すべての作業スレッドを終結処理します。


threadfunc()

threadfunc() の時間です。このコードは、それぞれの作業スレッドが実行します。作業スレッドが開始すると、ただちに作業キュー mutex をロックし、(作業ノードがあれば) 作業ノードを 1 つ獲得してそれを処理します。作業が 1 つも存在しない場合は、pthread_cond_wait() が呼び出されます。これが非常に短い while() ループの中で呼び出されることにお気付きと思いますが、これは大変重要です。pthread_cond_wait() 呼び出しから目覚めるとき、いかなる場合も条件が真であることを想定してはなりません。もしかすると 真かもしれませんが、そうでないかもしれません。この while ループは、スレッドが誤って目覚めさせられ、リストが空であるような場合に、もう 1 度 pthread_cond_wait() を呼び出すようにします。

もし作業ノードが存在すれば、単にそのジョブ番号を出力し、ノードを解放して終了します。実際のコードは、もっと中身のある操作を行うことでしょう。while() ループの最後に mutex をロックして、ループの冒頭で active 変数および新しい作業ノードを検査できるようにします。コードに目を通せば、wq.control.active が 0 の場合、while ループが終了して、threadfunc() の末尾の終結処置が始まることがわかるでしょう。

終結処置において作業スレッドが担当する役割はとても興味深いです。まず、work_queue をロック解除します。これは、mutex がロックされた状態で pthread_cond_wait() が応答するからです。次に、終結処置キューのロックを獲得し、(メイン・スレッドが pthread_join() 呼び出しに使用する TID を含む) 終結処置ノードを追加してから、終結処置キューをロック解除します。その後、cq を待っているすべてのスレッドにシグナルを送り (pthread_cond_signal(&cq.control.cond)) 、処理すべき新しいノードがあることをメイン・スレッドに知らせます。ここで pthread_cond_broadcast() を使わない理由は、それが不要だからです。終結処置キューの中の新しい項目を待っているスレッドは、ただ 1 つだけ (メイン・スレッドだけ) です。作業スレッドはシャットダウン・メッセージを出力した後に終了して、メイン・スレッドが join_threads() を呼び出したときに pthread_join() されるのを待ちます。


join_threads()

条件変数の使用法を示す簡単な例として、join_threads() 関数について見てください。作業スレッドが依然として存在する間、join_threads() は終結処置キューの中に新しい終結処置が現れるのを待ちながらループします。新しいノードが見付かれば、そのノードをデキューし、(作業スレッドが他の終結処置ノードを 追加できるように) 終結処置キューをロック解除し、(終結処置ノードに保管されている TID を使って) 新しいスレッドと結合し、終結処置ノードを解放し、「残っている」スレッドの数を減らして、処理を続行します。


まとめ

これで、「POSIX スレッドの説明」シリーズは終わりました。読者のアプリケーションにも、マルチスレッドのコードを追加する準備が整ったことを期待しています。より詳しい情報については、「参考文献」をご覧ください。ここには、この記事で使われたすべてのソースの tarball も掲載されています。また次のシリーズでお目にかかりましょう。

参考文献

  • この記事で使用されたソースの tarball を 入手できます。
  • Linux pthread のマニュアル・ページは、とてもわかりやすい優れた参考文献です ("man -k pthread")。
  • POSIX スレッドの扱い方を非常に詳しく説明した本として、David R. Butenhof 著「Programming with POSIX Threads」(Addison-Wesley、1997 年) を推奨します。この本は、POSIX スレッドに関する資料の中でおそらく最も優れた本でしょう。
  • POSIX については、W. Richard Stevens 著「UNIX Network Programming - Networking APIs: Sockets and XTI」(Prentice Hall、1997 年) でも説明されています。これは最高級の資料ですが、スレッドに関しては、上記の「Programming with POSIX Threads」 ほどには詳しく扱っていません。
  • Daniel による POSIX スレッド・シリーズの以前の記事を、developerWorks でご覧いただけます。
    • POSIX スレッドの説明 では、コードでスレッドを使用する方法をが正確に説明します。
    • POSIX の説明、第 2 回 では、mutex というちょっとした優れた手段により、スレッド化されたコードの整合性を保つ方法について説明されています。
  • Sean Walton (KB7rfa) による、「Linux のスレッド」に関する資料をご覧ください。
  • University of Arizona の Mark Hays による、POSIX スレッドの「チュートリアル」をご覧ください。
  • 「Pthreads-Tcl 入門 (An Introduction to Pthreads-Tcl)」で、POSIX スレッドと一緒に使用できるよう Tcl に加えられた変更事項をご覧ください。
  • LinuxThread ライブラリーをご覧ください。
  • 「Proolix」 は、永続的に開発の続く、i8086+ 用の簡単な POSIX 準拠のオペレーティング・システムです。

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Linux
ArticleID=229869
ArticleTitle=一般的なスレッド: POSIX スレッドの説明: 第3回
publish-date=09012000