pthread and erlang
pthread
erlangを始めたんだけど、その中でthreadってのが使われていた。threadってどんな物なの? 古くは、Javaに触った時に出てきてたような気がするが、それ以来ですよ。
スレッドってどうやって追ったらいいの? ggしたら、 GDBの上級トピック なんてのに出会ったぞ。それから少しは資料の収集。
どうやら、標準が有るようだ。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に変換する こんな使い方が出来るんだ。何かの時のために覚えておこう。