やかんです。

今回は、threadのapiについて復習しようと思います。

東大生やかんのブログ
やかん

pthreadなんたらのやつ。

OSの過去問解いてみた感想。

過去問、めっちゃ面白い。解いててめっちゃ楽しい。こんな問題を作れる教授は本当にすごいです。。

僕は工学部じゃないことや研究室配属に関係ない身分なので、試験にストレスフリーで臨むことができるんですが、OSの試験については楽しみですらあります。

東大生やかんのブログ
やかん

場合によっては供養お願いします。

pthreadのAPIについて復習。

スレッド間の同期について復習しようと思ったんですが、そのためにはpthreadの復習が必要だったので、まずはそちらから。

シンプルなpthread_create / pthread_join

例外処理とか全部はしょった場合はこちらのコード。

#include <stdio.h>
#include <pthread.h>

void *f()
{
  printf("this is child thread.\n");
}

int main()
{
  pthread_t child_thread_id;

  pthread_create(&child_thread_id, 0, f, 0);

  pthread_join(child_thread_id, 0);

  return 0;
}

このとき、ポインタ周りについて述べると以下。

  • pthread_tのchild_thread_idを宣言した段階では、ただの「スレッドID用の箱」しか用意されない。
  • pthread_create()に、上記「箱」のポインタを渡してあげると、pthread_create()がその箱の中身を適切なスレッドIDで埋めてくれる。
  • pthread_joinが呼ばれるときには、すでにpthread_createによってスレッドIDがchild_thread_idに入れられている。よって、渡すのはポインタじゃなくて実体。
    • このとき、プロセス間でメモリ空間は独立しているから、当該プロセスにおいてスレッドIDはユニークなものとなっている。そのため、ポインタではなく実体を果たすことで処理が意図通りに進む。

これに多少の例外処理とかをつけると以下。まあ、そりゃそうだよねって感じ。

#include <stdio.h>
#include <err.h>
#include <pthread.h>

void *f()
{
  printf("this is child thread.\n");
}

int main()
{
  pthread_t child_thread_id;

  if (pthread_create(&child_thread_id, 0, f, 0))
  {
    err(1, "pthread_create");
  };

  if (pthread_join(child_thread_id, 0))
  {
    err(1, "pthread_join");
  };

  return 0;
}

どちらのコードも、pthread_joinしないと正しく動作しない。というのは、pthread_joinは引数に渡したthread idを持つスレッドが終わるのを待つ働きをするため、pthread_joinを書かないと、スレッドが完了するのを待たずにmain関数が終わってしまう。

main関数が終わるということは、そのmain関数で作成されたスレッドを含んでいるプロセスが終了するということ。プロセスが終了したらその中のスレッドも終了するため、動作は正しくなくなってしまうというお話。

作成したpthread_createで作成したthreadに引数を渡したい。

↑これを実現したい場合は、pthread_createの第四引数に、スレッドに渡したい引数を指定してあげる。

その際、型のキャストに注意。まあまずはコードを示すと以下。

#include <stdio.h>
#include <err.h>
#include <pthread.h>

void *f(void *arg)
{
  printf("arg = %d\n", *(int *)arg);
  printf("this is child thread.\n");
}

int main()
{
  pthread_t child_thread_id;

  int arg = 42;

  if (pthread_create(&child_thread_id, 0, f, (void *)&arg))
  {
    err(1, "pthread_create");
  };

  if (pthread_join(child_thread_id, 0))
  {
    err(1, "pthread_join");
  };

  return 0;
}

「型に注意」と言ったのは、pthread_createの第四引数が受け付けている型はvoid*であるため、それに合わせて型をキャストしてあげる必要があるということ。まあ、キャストしなくても動くんだけどね。型のキャストはあくまで開発とかプログラムが実行される際の安定のために存在しているという理解なのですが、型を指定していなくても動きます。例えば、以下のコードも、期待通りに動くわけじゃないんですが、エラーにはなりません。

#include <stdio.h>
#include <err.h>
#include <pthread.h>

void *f(void *arg)
{
  printf("arg = %d\n", arg);
  printf("this is child thread.\n");
}

int main()
{
  pthread_t child_thread_id;

  int arg = 42;

  if (pthread_create(&child_thread_id, 0, f, &arg))
  {
    err(1, "pthread_create");
  };

  if (pthread_join(child_thread_id, 0))
  {
    err(1, "pthread_join");
  };

  return 0;
}

まあ、危険なのでやめましょう。型は関数が望んでいるものを渡し、void*を利用する際は想定される型にキャストした上で利用するのが良いと思われます。

子スレッドの終了したステータスを取得したい。

学習目的程度だど使う機会のない実装ですが、まあ、学習目的なので練習しておきます。

#include <stdio.h>
#include <err.h>
#include <pthread.h>

void *f()
{
  printf("this is child thread.\n");
}

int main()
{
  pthread_t child_thread_id;

  if (pthread_create(&child_thread_id, 0, f, 0))
  {
    err(1, "pthread_create");
  };

  void *child_status;

  if (pthread_join(child_thread_id, &child_status))
  {
    err(1, "pthread_join");
  };

  printf("child_status = %d\n", child_status);

  return 0;
}

child_statusを宣言してあげて、そのポインタをpthread_joinの第二引数に指定してあげると、そこに指定したスレッドが終了した時のステータスが書き込まれます。

この辺ややこしいですが、まず

void *child_status;

において、child_statusという変数がポインタとして宣言されます。ちょこちょこ出てきてるvoid*はジェネリックの型なので、typescriptのanyとかに近いんじゃないでしょうか。次に、

pthread_join(child_thread_id, &child_status)

にて、&child_statusは「statusというポインターが格納されているメモリ領域(アドレス?)のポインター」を表現します。

なんでこんなややこしいポインタの操作をしているのかは分かりませんが、まあ、pthread_joinの使用がそうなてます。余裕があったら調べてみよう。

そんで、pthread_joinの第二引数に渡されたポインターが指し示すポインターが指し示すメモリ領域(アドレス)に、当該スレッドの終了時ステータスが書き込まれます。

「『ポインターが指し示すポインター』が指し示すアドレス」です。ややこしいです。

pthread_exit()について。

pthread_exitは、pthread_createの第三匹数に指定される関数の中で使用されます。言い換えると、作成したスレッドで実行する関数の中で呼ばれるということ。

さっきのpthread_joinのサンプルコードにpthread_exitを書き加えると分かりやすくて、以下のコード。

#include <stdio.h>
#include <err.h>
#include <pthread.h>

void *f()
{
  printf("this is child thread.\n");

  pthread_exit(42);
}

int main()
{
  pthread_t child_thread_id;

  if (pthread_create(&child_thread_id, 0, f, 0))
  {
    err(1, "pthread_create");
  };

  void *child_status;

  if (pthread_join(child_thread_id, &child_status))
  {
    err(1, "pthread_join");
  };

  printf("child_status = %d\n", child_status);

  return 0;
}

この時、出力としては以下になる。

this is child thread.
child_status = 42

コンパイルでワーニングが出ますがそれは気にしないとして、pthread_exitは引数でそのスレッドの終了ステータスを指定することができます。そんで、そのスレッドを終了することができます。

スレッドの終了ステータスがpthread_joinで取得可能なことは前述した通り!

ざっとこんな感じ!ということで、こちらの記事は終了です。最後までお読みいただき、ありがとうございます。