オペレーティングシステム 演習 03¶

スレッド¶

名前と学生証番号を書け. Enter your name and student ID.

  • 名前 Name:
  • 学生証番号 Student ID:

1. スレッド関連コマンド¶

1-1. ps auxmww¶

  • ps は現存するプロセスを表示するコマンド
  • mをつけると各プロセス内のスレッドも表示される
  • 以下によりシステムのすべてのプロセスのすべてのスレッドが表示される
  • 出力が一杯になりすぎたり, そのせいでエラーになったら一旦ファイルへ出力し, そのファイルを開けば良い
  • 例えば
ps auxmww > ps.txt
In [ ]:
ps auxmww

2. スレッド¶

2-1. C (POSIX Threads または Pthreads)¶

  • PthreadはUnix共通のスレッドAPI

  • 基本

    • pthread_create でスレッドを作り, 実行
    • pthread_join でスレッドの終了を待つ
    • pthread_exit で呼び出したスレッドを終了させる
    • pthread_self は呼び出したスレッドのthread IDを返す
  • 以下はともかくスレッドを作ってjoinするだけの例

In [ ]:
%%writefile thread_create.c
#include <err.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>

/* スレッドの開始関数 */
void * f(void * arg) {
  pthread_t thread_id = pthread_self();
  int slp_n = 5;
  for (int i = 0; i < slp_n; i++) {
    printf("child[%lu]: (%d/%d)\n",
           thread_id, i, slp_n);
    fflush(stdout);
    usleep(100 * 1000);
  }
  return arg + 1;
}

int main() {
  pthread_t my_thread_id = pthread_self();
  pthread_t child_thread_id;
  /* スレッドを作る */
  if (pthread_create(&child_thread_id, 0, f, 0)) {
    err(1, "pthread_create");
  }
  int slp_n = 5;
  for (int i = 0; i < slp_n; i++) {
    printf("parent[%lu]: (%d/%d)\n", my_thread_id, i, slp_n);
    fflush(stdout);
    usleep(100 * 1000);
  }
  /* 終了待ち */
  void * ret = 0;
  if (pthread_join(child_thread_id, &ret)) {
    err(1, "pthread_join");
  }
  printf("child thread returned %p\n", ret);
  return 0;
}
In [ ]:
gcc -Wall thread_create.c -o thread_create -lpthread
In [ ]:
./thread_create

2-2. Python (threadingモジュール)¶

  • 基本
    • th = threading.Thread(...) でスレッドオブジェクトを作り, th.start() で実行
    • th.join() でスレッドの終了を待つ
      • ただし pthread のAPIと違って子スレッドの終了ステータスは得られない(あまり必然性のない制限)
    • threading.current_thread() は呼び出したスレッドのスレッドオブジェクトを返す
      • th.native_id で スレッド th の OSレベルのthread IDが得られるようである
In [ ]:
%%writefile thread_create.py
import threading
import time

def f(arg):
    th = threading.current_thread()
    slp_n = 5
    for i in range(slp_n):
        print(f"child[{th.native_id}]: ({i}/{slp_n})", flush=True)
    time.sleep(0.1)

def main():
    my_th = threading.current_thread()
    child_th = threading.Thread(target=f, args=(0,))
    child_th.start()
    slp_n = 5
    for i in range(slp_n):
        print(f"parent[{my_th.native_id}]: ({i}/{slp_n})", flush=True)
        time.sleep(0.1)
    child_th.join()

main()
In [ ]:
python3 thread_create.py

3. スレッドに引数を渡す¶

  • 普通は, スレッドを複数作ったらそれぞれに違う仕事をやらせたい
  • そのためにスレッドが実行する関数(開始関数)に異なる引数を渡すのが普通だが, pthread のAPIでは開始関数がvoid* (ポインタ)型の引数1つしか取れないという制限がある (Pythonでは任意個の引数をタプルとして渡せる)
void * f(void *) { ... }
  • そのため通常, 構造体を作りそれへのポインタを引数として渡す
typedef struct { int xxx; double yyy; ... } thread_arg_t;
  • 開始関数の方では受け取った void* 型を構造体へのポインタ変数に代入し, そこから値を取り出すのが常套手段
void * f(void * arg_) {
  thread_arg_t * arg = arg_;
  arg->xxx, arg->yyy, ...
}
  • 開始関数はvoid* を受け取るが, それに構造体のポインタを渡しても問題はない

3-1. C¶

In [ ]:
%%writefile thread_create_arg.c
#include <assert.h>
#include <err.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>

/* 開始関数に渡したい情報(構造体) */
typedef struct {
  long slp_usec;
  long slp_n;
  long id;                       /* 0,1,2,.. */
  pthread_t th_id;
} thread_arg_t;

/* 開始関数 構造体へのポインタを (void *型で)受け取る */
void * f(void * arg_) {
  thread_arg_t * arg = arg_;
  /* 本当に受け取りたい引数を構造体から受け取る */
  long slp_usec = arg->slp_usec;
  long slp_n = arg->slp_n;
  long id = arg->id;
  pthread_t thread_id = pthread_self();
  for (int i = 0; i < slp_n; i++) {
    printf("child[%ld/%lu] (%d/%ld): sleep %ld usec\n",
           id, thread_id, i, slp_n, slp_usec);
    fflush(stdout);
    usleep(slp_usec);
  }
  return 0;
}

int main(int argc, char ** argv) {
  int nthreads = (argc > 1 ? atoi(argv[1]) : 3);
  thread_arg_t args[nthreads];
  /* 指定された数のスレッドを作る */
  for (int i = 0; i < nthreads; i++) {
    args[i].slp_n = i + 2;
    args[i].slp_usec = 1000 * 1000 / args[i].slp_n;
    args[i].id = i;
    if (pthread_create(&args[i].th_id, 0, f, &args[i])) {
      err(1, "pthread_create");
    }
  }
  /* 終了待ち */
  for (int i = 0; i < nthreads; i++) {
    void * ret;
    if (pthread_join(args[i].th_id, &ret)) {
      err(1, "pthread_join");
    }
    assert(ret == 0);
    printf("child thread %d returned (%p)\n", i, ret);
  }  
  return 0;
}
In [ ]:
gcc -Wall -o thread_create_arg thread_create_arg.c -lpthread
In [ ]:
./thread_create_arg

3-2. Python¶

In [ ]:
%%writefile thread_create_arg.py
import sys
import threading
import time

def f(slp_n, slp_usec, idx):
    th = threading.current_thread()
    for i in range(slp_n):
        print(f"child[{idx}/{th.native_id}] ({i}/{slp_n}): sleep {slp_usec} usec",
               flush=True)
        time.sleep(slp_usec * 1e-6)

def main():
    nthreads = int(sys.argv[1]) if 1 < len(sys.argv) else 3
    # 指定された数のスレッドを作る
    threads = []
    for i in range(nthreads):
        slp_n = i + 2
        th = threading.Thread(target=f, args=(slp_n, 1e6 / slp_n, i))
        th.start()
        threads.append(th)
    # 終了待ち
    for th in threads:
        th.join()

main()
In [ ]:
python3 thread_create_arg.py

4. スレッド vs プロセスの違いを理解する¶

  • 違いは色々あるものの, どちらもCPU (正確には仮想コア)を複数使うための道具であることも確か

  • 端的にその挙動の違いは

    • 「1プロセス間の複数スレッドはメモリ(変数)を共有している」
    • 「複数プロセス間ではメモリは共有されない」 という違いがある
  • 特にfork()は「コピー」を作っているのであって親プロセスと子プロセスでデータ(変数)が共有されているわけではないことに注意

  • 以下が違いを示す例

  • 表示される結果を予想してから実行し, 何が起きているのかを理解せよ

  • プロセス(fork)を使う例

  • C

In [ ]:
%%writefile thread_vs_fork_fork.c
#include <err.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int x = 0;

/* スレッドの開始関数 */
void * f(void * arg) {
  x += 321;
  return 0;
}

int main() {
  x = 123;
  
  pid_t pid = fork();
  if (pid == -1) {
    err(1, "fork");
  } else if (pid == 0) {          /* child */
    f(0);
    return 0;
  } else {
    int ws;
    pid_t cid = waitpid(pid, &ws, 0);
    if (cid == -1) err(1, "waitpid");
    if (WIFEXITED(ws)) {
      printf("exited, status=%d\n", WEXITSTATUS(ws));
      fflush(stdout);
    } else if (WIFSIGNALED(ws)) {
      printf("killed by signal %d\n", WTERMSIG(ws));
      fflush(stdout);
    }
    printf("after the child finished, x = %d\n", x);
  }
  return 0;
}
In [ ]:
gcc -Wall -o thread_vs_fork_fork thread_vs_fork_fork.c
In [ ]:
./thread_vs_fork_fork
  • Python
In [ ]:
%%writefile thread_vs_fork_fork.py
import os

x = 0

def f():
    global x
    x += 321

def main():
    global x
    x = 123
  
    pid = os.fork()
    if pid == 0:
        f()
    else:
        cid, ws = os.waitpid(pid, 0)
        if os.WIFEXITED(ws):
            print(f"exited, status={os.WEXITSTATUS(ws)}", flush=True)
        elif os.WIFSIGNALED(ws):
            print(f"killed by signal {os.WTERMSIG(ws)}", flush=True)
        print(f"after the child finished, x = {x}")

main()
In [ ]:
python3 thread_vs_fork_fork.py
  • スレッド(pthread_create)を使う例

  • C

In [ ]:
%%writefile thread_vs_fork_thread.c
#include <err.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>

int x = 0;

/* スレッドの開始関数 */
void * f(void * arg) {
  x += 321;
  return 0;
}

int main() {
  pthread_t child_thread_id;
  x = 123;
  
  /* スレッドを作る */
  if (pthread_create(&child_thread_id, 0, f, 0)) {
    err(1, "pthread_create");
  }
  /* 終了待ち */
  void * ret = 0;
  if (pthread_join(child_thread_id, &ret)) {
    err(1, "pthread_join");
  }
  printf("after the child finished, x = %d\n", x);
  return 0;
}
In [ ]:
gcc -Wall -o thread_vs_fork_thread thread_vs_fork_thread.c -lpthread
In [ ]:
./thread_vs_fork_thread
  • Python
In [ ]:
%%writefile thread_vs_fork_thread.py
import os
import threading

x = 0

def f():
    global x
    x += 321

def main():
    global x
    x = 123
    th = threading.Thread(target=f)
    th.start()
    th.join()
    print(f"after the child finished, x = {x}")

main()
In [ ]:
python3 thread_vs_fork_thread.py

Problem 1 : スレッドの練習¶

以下のようなプログラムを書け.

  1. 時刻をナノ秒単位で取得(Linux: clock_gettime または gettimeofday, Mac: gettimeofday; manを参照. Python time.time())
  2. 以下を多数回($n$回)繰り返す
  • 子スレッドを作る. 子スレッドは, 何もしない関数do_nothingを実行する
  • 親スレッドはただちに子スレッドの終了を待つ
  1. 時刻をナノ秒単位で取得
  2. 1回あたりの時間をナノ秒単位で出力

do_nothingは以下のような関数.

void * do_nothing(void *) {
  return 0;
}  
  • $n$はコマンドラインから取得できるようにする

  • 以下のコードを修正して上記を達成せよ

  • C

In [ ]:
%%writefile time_thread_create.c
/* 必要な #include を補うこと (man ページを参照) */
#include <err.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

long cur_time() {
  struct timespec ts[1];
  clock_gettime(CLOCK_REALTIME, ts);
  return ts->tv_sec * 1000000000L + ts->tv_nsec;
}

void * do_nothing(void * arg) {
  return arg;
}

int main(int argc, char ** argv) {
  int n = (argc > 1 ? atoi(argv[1]) : 5);
  long t0 = cur_time();

  
  ここにプログラムを書く

  
  long t1 = cur_time();
  long dt = t1 - t0;
  printf("%ld nsec to pthrea_create-and-join %d threads (%ld nsec/thread)\n",
         dt, n, dt / n);
  return 0;
}
In [ ]:
gcc -O3 -Wall -Wextra -o time_thread_create time_thread_create.c -lpthread
In [ ]:
./time_thread_create
In [ ]:
%%writefile time_thread_create_ans.c
#include <err.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

long cur_time() {
  struct timespec ts[1];
  clock_gettime(CLOCK_REALTIME, ts);
  return ts->tv_sec * 1000000000L + ts->tv_nsec;
}

void * do_nothing(void * arg) {
  return arg;
}

int main(int argc, char ** argv) {
  int n = (argc > 1 ? atoi(argv[1]) : 5);
  long t0 = cur_time();
  for (int i = 0; i < n; i++) {
    pthread_t tid;
    void * ret;
    int e = pthread_create(&tid, 0, do_nothing, 0);
    if (e) err(e, "pthread_create");
    e = pthread_join(tid, &ret);
    if (e) err(e, "pthread_join");
  }
  long t1 = cur_time();
  long dt = t1 - t0;
  printf("%ld nsec to pthrea_create-and-join %d threads (%ld nsec/thread)\n",
         dt, n, dt / n);
  return 0;
}
In [ ]:
gcc -Wall -o time_thread_create_ans time_thread_create_ans.c -lpthread
In [ ]:
./time_thread_create_ans
  • 以下のコマンドラインを色々変更して, 1回あたりの時間を計測せよ
  • 正しく動いているかを確認するために, 子スレッド(do_nothing関数)で何かを表示するとか, 子スレッドのstatus (do_nothingの返り値)を変えてそれが正しく受け取れていることを確認するなどせよ
  • 時間を計測するときはそれらの表示を消すこと(消さないと, 測っているのは出力時間が大半を占めることになる)
In [ ]:
./time_thread_create 10
In [ ]:
./time_thread_create_ans 1000
  • Python
In [ ]:
%%writefile time_thread_create.py
import sys
import threading
import time

def cur_time():
    return int(time.time() * 1e9)

def do_nothing():
    return

def main():
    n = int(sys.argv[1]) if 1 < len(sys.argv) else 5
    t0 = cur_time()

  
    ここにプログラムを書く

  
    t1 = cur_time()
    dt = t1 - t0
    print(f"{dt} nsec to thread_create-and-join {n} threads ({dt/n} nsec/thread)")

main()
In [ ]:
python3 time_thread_create.py
In [ ]:
%%writefile time_thread_create_ans.py
import sys
import threading
import time

def cur_time():
    return int(time.time() * 1e9)

def do_nothing():
    return

def main():
    n = int(sys.argv[1]) if 1 < len(sys.argv) else 5
    t0 = cur_time()
    for i in range(n):
        th = threading.Thread(target=do_nothing)
        th.start()
        th.join()
    t1 = cur_time()
    dt = t1 - t0
    print(f"{dt} nsec to thread_create-and-join {n} threads ({dt/n} nsec/thread)")

main()
In [ ]:
python3 time_thread_create_ans.py
  • Cと同様に測定せよ
In [ ]:
python3 time_thread_create.py 10
In [ ]:
python3 time_thread_create_ans.py 1000