pthread and erlang

pthread

erlangを始めたんだけど、その中でthreadってのが使われていた。threadってどんな物なの? 古くは、Javaに触った時に出てきてたような気がするが、それ以来ですよ。

スレッドってどうやって追ったらいいの? ggしたら、 GDBの上級トピック なんてのに出会ったぞ。それから少しは資料の収集。

オペレーティングシステム(thread)

GNU/Linux でのスレッドプログラミング

スレッド・プログラミング

マルチスレッドの基礎

どうやら、標準が有るようだ。OpenBSDにも pthreads(3) なんてのが用意されてた。

そして、なるべく簡単な例って事で、こんなのを見つけた。

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

void *thread(void *arg) {
  char *ret;
  printf("thread() entered with argument %s \n", arg);
  if ((ret = (char*) malloc(20)) == NULL) {
    perror("malloc() error");
    exit(2);
  }
  strcpy(ret, "This is a test");
  pthread_exit(ret);
}

int main() {
  pthread_t thid;
  void *ret;

  if (pthread_create(&thid, NULL, thread, "MYthread") != 0) {
    perror("pthread_create() error");
    exit(1);
  }

  if (pthread_join(thid, &ret) != 0) {
    perror("pthread_create() error");
    exit(3);
  }

  printf("thread exited with %s \n", ret);
}

子供のスレッドを一つ、createするんだな。その時、そのスレッドで実行する関数と引数を渡せるとな。fork + execve みたいな動きなんだな。起動に成功するとpidならぬthidが帰って来る。

メインスレッドは親プロセス相当。子供スレッドが終了するのをjoinで待つとな。子供は何人でも持てる(そのようにプログラミング出来る)。

実際に3秒待つ関数と5秒待つ関数をスレッドに登録して走らせてみたら、全体では5秒の実行時間で済んだ。ちゃんと並行実行されてるね。

sakae@pen:/tmp$ cc -g z.c -pthread
sakae@pen:/tmp$ ./a.out
thread() entered with argument MYthread
thread exited with This is a test

折角なのでgdbしてみる。スレッドの関数にBPを置いて実行。

(gdb) r
Starting program: /tmp/a.out
[New thread 473637]
[Switching to thread 473637]

Thread 2 hit Breakpoint 1, thread (arg=0x1ab7b49a) at z.c:8
8         printf("thread() entered with argument %s \n", arg);
(gdb) bt
#0  thread (arg=0x1ab7b49a) at z.c:8
#1  0x0bc1525d in _rthread_start (v=0x69abc82c) at /usr/src/lib/librthread/rthr\
ead.c:96
#2  0x01eccdea in __tfork_thread () at /usr/src/lib/libc/arch/i386/sys/tfork_th\
read.S:92
(gdb) info thread
  Id   Target Id         Frame
   1    thread 381791     futex () at /tmp/-:3
 * 2    thread 473637     thread (arg=0x1ab7b49a) at z.c:8

forkしてスレッドが走って、ユーザー定義の関数の頭で止まってる。スレッドidが1の方は、メインスレッドなんだな。

(gdb) thread 1
[Switching to thread 1 (thread 381791)]
#0  futex () at /tmp/-:3
3       /tmp/-: No such file or directory.
(gdb) bt
#0  futex () at /tmp/-:3
#1  0x0bc12676 in _twait (p=0x69abc834, val=<error reading variable: Cannot acc\
ess memory at address 0x0>, clockid=<error reading variable: Cannot access memo\
ry at address 0x0>, abs=<optimized out>) at /usr/src/lib/librthread/synch.h:41
#2  _sem_wait (sem=0x69abc82c, can_eintr=0, abstime=0x0, delayed_cancel=0x21e5c\
40c <_initial_thread+136>) at /usr/src/lib/librthread/rthread_sem.c:76
#3  0x0bc14d99 in pthread_join (thread=0x69abc82c, retval=0xcf7f9578) at /usr/s\
rc/lib/librthread/rthread.c:304
#4  0x1ab7c8c7 in main () at z.c:26

メインスレッドの方は、待ちに入ってる。

(gdb) f 4
#4  0x1ab7c8c7 in main () at z.c:26
26        if (pthread_join(thid, &ret) != 0) {
(gdb) p *thid
$2 = {donesem = {lock = 0, waitcount = 1, value = 0, shared = 0}, flags = 0, fl\
ags_lock = 0, tib = 0x69abc800, retval = 0x0, fn = 0x1ab7c780 <thread>, arg = 0\
x1ab7b49a, name = '\000' <repeats 31 times>, stack = 0x78bffea0, threads = {le_\
next = 0x21e5c384 <_initial_thread>, le_prev = 0x2bc10240 <_thread_list>}, wait\
ing = {tqe_next = 0x0, tqe_prev = 0x0}, blocking_cond = 0x0, attr = {stack_addr\
 = 0x0, stack_size = 262144, guard_size = 4096, detach_state = 0, contention_sc\
ope = 2, sched_policy = 2, sched_param = {sched_priority = 0}, sched_inherit = \
4}, local_storage = 0x0, cleanup_fns = 0x0, delayed_cancel = 0}

スレッドの制御情報に色々なデータが登録されているな。

数値積分をpthreadで

並行実行は、何もptheadだけじゃないだろう。ってな事で、オイラーは図書館で、ぴったりな本を借りてきた。『並行コンピューティング技法』って、オライリーなやつ。

それによると、OpenMP、Windowsスレッド、Haskell、ERLANGもあるよ。そう、erlがちゃんと認知されてたね。

で、その解説本に数値積分でPIを求める例が紹介されてた。数値積分なら、maximaが得意だったな。

(%i2) romberg(4/(1 + x * x), x, 0, 1);
(%o2)                          3.141592638396796

それから、オクターブも有ったな。

octave:1> function y = f(x)
> y = 4 / ( 1 + x * x);
> endfunction
octave:2> quad("f", 0, 1)
ans =  3.1416

いかん、いかん。と言う事で、解説本から例を引いてきた。

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

#define N_RECT (1000 * 1000 * 1000)
#define N_THD  4

double gA = 0.0;
pthread_mutex_t gl;

void *calc(void *pA){
  int myN =*((int *)pA);
  double pH = 0.0, lw = 1.0 / N_RECT, x;

  for (int i = myN; i < N_RECT; i += N_THD){
    x = (i + 0.5f) / N_RECT;
    pH += 4.0f / (1.0f + x * x);
  }
  pthread_mutex_lock(&gl);
  gA += pH * lw;
  pthread_mutex_unlock(&gl);
}

void main(){
  pthread_t th[N_THD];
  int       tn[N_THD];

  pthread_mutex_init(&gl, NULL);
  for(int i = 0; i< N_THD; i++){
    tn[i] = i;
    pthread_create(&th[i], NULL, calc, (void *)&tn[i]);
  }
  for(int i =0; i < N_THD; i++){
    pthread_join(th[i], NULL);
  }

  pthread_mutex_destroy(&gl);
  printf("PI = %.20g\n", gA);
}

スレッドを4つ作って、分担計算。これって、昔流行った MapReduce じゃんと思うぞ。

sakae@pen:/tmp$ time ./a.out
PI = 3.1415926545925798585

real    0m5.400s
user    0m20.739s
sys     0m0.061s

無暗に計算量を多くしてみた。ユーザーが感じる計算時間は5秒だけど、実際には、20秒間CPU が動いてました。そうさ、2コアで4スレッド相当のマシンだからね。

ob$ time ./a.out
PI = 3.1415926545925794144
    0m02.04s real     0m08.07s user     0m00.02s system

OpenBSD(64Bit)での結果。久しぶりにOpenBSDが大差をつけてリナに勝った。その勝因は? clang vs. gcc なのかな。それともpthreadの処理方法に起因してる? こういうの調べてみるのも楽しそう。

sakae@pen:/tmp$ time ./a.out
PI = 3.1415926545925794144

real    0m7.317s
user    0m14.546s
sys     0m0.004s

こちらは、計算機実験と称して、2つのCPUのうち1個を取り外しての実験。誰かさんのように、特殊なドライバーを使って基盤をさらけ出す必要も無い。VMWareの設定をひょいと変更するだけだ。

おまけで、debian(32Bit)で試そうと思ったら

debian:tmp$ cc z.c -lphtread
/usr/bin/ld: cannot find -lphtread
collect2: error: ld returned 1 exit status

そんな生意気な事を32Bitの石でやるなと、馬鹿にされたぞ。

#include <stdio.h>
#define N_RECT (200 * 1000 * 1000)
double gA = 0.0;

void main(){
  double pH = 0.0, lw = 1.0 / N_RECT, x;

  for (int i = 0; i < N_RECT; i++){
    x = (i + 0.5f) / N_RECT;
    pH += 4.0f / (1.0f + x * x);
  }
  gA = pH * lw;
  printf("PI = %.20g\n", gA);
}

シングルスレッド用と言うか、普通のやつ。

debian:tmp$ time ./a.out
PI = 3.1415926535901146366

real    0m4.048s
user    0m4.016s
sys     0m0.004s

上のコードで(pthread版もそうだけど)、面積を求めるのに最後に1回だけ、幅を掛け算してる。これってどゆ事? 数値積分って、短冊型に切り刻んで、それを長方形とみなして、個々の短冊の面積を求める。それを全部加えれば、全体の面積になる。

gA = pH1 * lw + pH2 * lw ... pHn * lw   ==> (pH1 + pH2 ... pHn) * lw

重い掛け算の回数を減らす工夫がされてるんだね。数学もプログラミングでは重要って事です。

これ数学の問題じゃないよね。小学生にも分かる算数問題だぞ。オイラーは、難しい掛け算は苦手です。何とか掛け算を避けたいです。短冊の幅が一緒なんだから、短冊をどんどんと積み重ねちゃえ。そして最後に一回だけ、それに幅を掛け算すれば、短冊全体の面積になるね。きっとオイラーさんも、同じ発想で、1から100まで、全部足したら幾つになる、を一瞬で解いたのだろう。オイラーは神童だな。

startup erlang

erlangがどうやって立ち上がるか? 前回もちょいと調べたけど、起動はshellscriptだ。

sakae@pen:/tmp$ sh -x /usr/local/bin/erl
+ ROOTDIR=/usr/local/lib/erlang
+ BINDIR=/usr/local/lib/erlang/erts-11.2/bin
+ EMU=beam
+ echo /usr/local/bin/erl
+ sed s/.*\///
+ PROGNAME=erl
+ export EMU
+ export ROOTDIR
+ export BINDIR
+ export PROGNAME
+ exec /usr/local/lib/erlang/erts-11.2/bin/erlexec
Erlang/OTP 23 [erts-11.2] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:1] [hipe]

で、最後にexecしてるのは、実際のbinaryだ。だから、これを起動すればいいんだな。

sakae@pen:/tmp$ cd /usr/local/lib/erlang/erts-11.2/bin
sakae@pen:/usr/local/lib/erlang/erts-11.2/bin$ ./erlexec
Environment variable BINDIR is not set

BINDIRって環境変数が必要とな。まだ他にも必要かも知れないけど、それはエラーonデマンドでいいだろう。

sakae@pen:/usr/local/lib/erlang/erts-11.2/bin$ export BINDIR=/usr/local/lib/erlang/erts-11.2/bin
sakae@pen:/usr/local/lib/erlang/erts-11.2/bin$ ./erlexec
Erlang/OTP 23 [erts-11.2] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:1] [hipe]

起動したな。後は、この環境でgdbすれば良い。で、最終的には、 beam.smpが鎮座する。ならば、execveあたりで、変身するんだろうね。

erlexec.c

=>      execv(emu, Eargsp);
(gdb) p emu
$1 = 0x555555563350 "/usr/local/lib/erlang/erts-11.2/bin/beam.smp"

なる程、ここで変身するんだ。と言う事はerlexecってやつは、起動係りなんだな。 何やらそのすぐ下に、色々な起動オプションが用意されてた。 詳しい事は、マニュアル erl を参照とな。

erl -man xxx

簡単にマニュアル参照と書いてしまったけれど、上記で提供されているのはWeb上のやつ。いちいちWebを引かないと見られないってのは、昨今のネットワーク込み具合を勘案すると、気が引ける(プロバイダーから、感謝状ぐらい貰えるかな)。

自前のWebサーバーに、manのコンテンツを入れてしまって、外側のネットワークを荒らさないようにしようか。幸いDebianには、nginxが入っているからね。

debian:html$ ls erlang/
COPYRIGHT  doc/  erts-11.2/  lib/  PR.template  README.md

こんな具合にhtdocsの下にerlangってdirを作り、その中に otp_doc_html_23.3.tar.gz を展開。後はhttp://localhost/erlang/doc/ をアクセスするだけだ。全コンテンツが192Mあったぞ。巨大ななあ。まあ、ErlangOSと思って納得しとこう。

それより手軽なのは、昔ながらのman。ロートルには、こちらの方がしっくりくる。

[sakae@fb ~]$ man erl
   :
SEE ALSO
       epmd(1), erl_prim_loader(3), erts_alloc(3), init(3), application(3),
       auth(3), code(3), erl_boot_server(3), heart(3), net_kernel(3), make(3)

これ、FreeBSDにあるmanだ。他に見ておくといいよってのが列挙されてる。どんなmanが有るkは、

[sakae@fb ~]$ cat /usr/local/etc/man.d/erlang.conf
MANPATH /usr/local/lib/erlang/man

ってな事になってる。man3が非常に多い。見ておくと吉ってのが有る。erlからもmanを引けるようになっている。

[sakae@fb ~]$ erl -man erlang
erlang(3)                  Erlang Module Definition                  erlang(3)

NAME
       erlang - The Erlang BIFs.

DESCRIPTION
       By convention, most Built-In Functions (BIFs) are included in this
       module. Some of the BIFs are viewed more or less as part of the Erlang
       programming language and are auto-imported. Thus, it is not necessary
       to specify the module name. For example, the calls atom_to_list(erlang)
       and erlang:atom_to_list(erlang) are identical.
        :
       timestamp() =
           {MegaSecs :: integer() >= 0,
            Secs :: integer() >= 0,
            MicroSecs :: integer() >= 0}

              See erlang:timestamp/0.
         :

いわゆる組み込みコマンド。場合によってはモジュール名のerlangを前置しないといけない場合が有る。

3> atom_to_list(erlang).
"erlang"
4> erlang:atom_to_list(erlang).
"erlang"

頻出するんで、モジュール名は無くても良いとな。

5> timestamp().
 ** exception error: undefined shell command timestamp/0
6> erlang:timestamp().
{1621,28307,733726}

それに対して、使う頻度が少ないやつは、ちゃんとモジュール名を付けろとな。その辺がどうなってるかは、得意のソース嫁。いや、取り合えずmanしろ。

10> erlang:system_info(version).
"10.2"
11> erlang:system_info(os_type).
{unix,openbsd}
12> erlang:system_info(procs).
<<"=proc:<0.0.0>\nState: Waiting\nName: init\nSpawned as: otp_ring0:start/2\nSpawned by: []\nMessage queue length: 0\nNumber "...>>

引数のatomをman erlang から調べると、山程有るな。

17> erlang:m ;; TAB to complation
make_fun/3         make_ref/0         make_tuple/2       make_tuple/3
map_get/2          map_size/1         match_spec_test/3  max/2
md5/1              md5_final/1        md5_init/0         md5_update/2
memory/0           memory/1           min/2              module_info/0
module_info/1      module_loaded/1    monitor/2          monitor_node/2
monitor_node/3     monotonic_time/0   monotonic_time/1
17> erlang:memory().
[{total,8299480},
 {processes,2922520},
 {processes_used,2922000},
 {system,5376960},
 {atom,193733},
 {atom_used,164252},
 {binary,82360},
 {code,2609459},
 {ets,179680}]

erl -man erlangした時、どんなプロセスが走っているか確認。だって、

ob$ ls /usr/local/lib/erlang/man
man1/      man3/      man4/      man6/      man7/      mandoc.db

こんな風にman原稿が用意されてたし、FreeBSDでは、man.confがちゃんと出来ていたしね。

ob$ ps a
80732 p2  Sp       0:17.63 man erlang
53877 p2  S+p      0:00.21 less -T /tmp/man.xp1dXifbIS /tmp/man.RHfNJHycV8

思った通り、普通のmanに管変わっている。ならば、こんな事も出来るだろう。

ob$ erl -man -k time
asn1ct(3) - ASN.1 compiler and compile-time support functions
calendar(3) - Local and universal time, day of the week, date and time conversions.
 :

Web版のdocシステムだとsearchでキーワード検索が出来たけど、この方法で代用出来るな。

erlangをソースからbuildすると、何たらが無いからmanは作れないと言われるけど、ドキュメントシステムの構築ってどうやるのだろう?

**********************  DOCUMENTATION INFORMATION  ******************
*********************************************************************

documentation  :
                xsltproc is missing.
                fop is missing.
                The documentation cannot be built.

ソースを漁ると、man(htmlも)の原稿っぽいのがオイラーの大嫌いなxmlで置いてあるようだ。

ob$ pwd
/tmp/otp_src_23.3/erts/doc/src
ob$ ls
Makefile                   erl_ext_dist.xml           introduction.xml
absform.xml                erl_ext_fig.gif            match_spec.xml
 :

お試しmakeする。

ob$ export ERL_TOP=/tmp/otp_src_23.3/
ob$ gmake
sed -e 's;%RELEASE%;23;' ../../info.src > ../../info
 GEN    ../xml/epmd_cmd.xml
 GEN    ../man1/epmd.1
/bin/sh: xsltproc: not found
gmake: *** [/tmp/otp_src_23.3//make/x86_64-unknown-openbsd6.9/otp.mk:304: ../man1/epmd.1] Error 127

ふーん。君子危うきに近寄らずって事にしておこう。

xsltprocを使ってXMLをCSVに変換する こんな使い方が出来るんだ。何かの時のために覚えておこう。


This year's Index

Home