ようになんにもしないのが悪いのでせうか ? それでは , なにか中身のある実行文を書い てみましよう , 次のように while( ! eSwitch ) + + i ; 〃無意味な式文 こんなことをしても , 黄色いボタンがピ ンクのボタンに変わるという目的は実現し ません。黄色いボタンをクリックしても , 黄色いままです (Fig. 5 ) 。 ローカル変数を複数個参照するこんな式 C 言語フォーラム 137 Fig. 7 javap ー c の出力 ( 4 ) : Etest. java の main ( ) # 23 <Field bo 引 ean eSwitch> / / それを a へストアする ( 結果 : a=b;) / / b をオペランドスタックのトップへ Fig. 6 javap ー c の出力 ( 3 ) . 無意味な式文の場合 ( その 2 , 抜粋 ) 18 ifeq 13 15 getstatic istore—l 13 iload-2 10 go セ 0 15 14 Method void main( java. lang. string[ ] ) 6 invokevirtual # 12 <Method int read( ) > 3 getstatic # 9 く Fie ld java. 土 0. InputStream in> //System. in は static 0 goto 3 9 dup 10 土 s セ 0 て e ー 1 11 bipush 98 13 if—icmpne 3 / / 読んだ文字を 2 個作り / / その一つを変数 ch へポップ / / ' b ' をオペランドスタックへ ( 98==ASCI I ' b ' ) / / ch ! = ' b ' なら 3 へ行く ( 実際は ch を無視 : 本文参照 ) 文では ? : int a = 0 , b = 0 ; while( ! eSwich ) 出します。 ます。すなわち , 何らかのメソッドを呼び 中で実行の流れを一時的によそへ移してみ を試みているわけですが , 今度は while 節の 理由が分からないまま闇雲にいろんなこと 空文ループがどーしてだめなのか , その but, why?? メソッド呼び出しなら OK , があるのであった ! この点に実は , 今回のドジの大きな伏線 コンパイラではない ? んけ。 ・・ぬぬぬ , すると javac はワンパス ちょこざいな ! ch なんて結局使わんや ップする ) からです。 使用する ( オペランドスタックから計 2 回ポ 変数 ch への代入と if 命令による比較の計 2 回 ッドの返し値を複製しているのは , それを , ランドスタックのトップにある read() メソ お , 9 番のところで dup 命令により , オペ 造にとくに変わった点はありませんね。な この Etest.java でも , ループ部分の処理構 これも参考のために見ておきましよう (Fig. 7 ) 。 んなハ 4 コードになっているのでしようか ? 見た , " 普通の " ケースの空文ループは , ど さきほど , Etest.java というプログラムで しかし , これもやはりだめです (Fig. 6 ) 。 16 getstatic # 10 <Fie ld java. 土 0. PrintStream out> ldc # 1 く string nwe 're gonna exit now. ” > 21 invokevirtua ー # 11 く Method VOid print ln( java.lang. string)> 24 go セ 0 28 27 POP return 28 19 from tO target type Exception tab : 0 24 <CIass java. iO. IOExcepti011> クラス EmptyTest の中に static void nop() { } という無内容のメソッドを作って , それを 呼び出してみます : while( ! eSwitch ) nop();//static メソッドを呼び出す しかし , これまただめでした。 javap の出 力は Fig. 8 です。 static メソッド ( クラスメソッド ) ではなく て , ランタイムに作られる ( instan ⅱ ate され る ) オプジェクトに伴って動的に配置される メソッド , すなわちインスタンスメソッド ならどうでせうか ? void nop() { } //non-static なメソッド これを while 節の中で呼び出します : while( ! eSwitch ) nop() ; ″インスタンスメソッドを 〃呼び出す どうせこれもだめだろう ? ところがところが ! これでついにピン クのボタンが出現したのであります (Fig. 9 ) 。これまでと , どこがどー違うんでしよ うか ? Fig. 9 の jav 叩の出力を見るかぎり , これ までととくに変わったところはありません ねー現在のメソッド ( カレントメソッド ) は main() です。そのローカル変数としてオ プジェクトがあり , そのオプジェクトのメ ンバを参照するときには 11 番のように aloa d_n 命令がまずあって , それによりオプジ ェクトの参照をオペランドスタックに乗せ ます。 aload ー 1 は , カレントメソッドの最も 冒頭のローカル変数を操作します。 JVMS を見ると , メソッド冒頭で宣言される第一 ローカル変数をオペランドスタックに乗せ る命令は aload ー 0 のはずですけど , 実際は al oad ー 1 が使われています。 aload ー 0 は一体何 を運ぶのか ? 理由は依然として ? ? でありますが , と にかく「 static メソッドはノー ! 」「インスタ ンスメソッドは OK ! 」でありました。それ では , 自クラスではなくてほかのクラスの static メソッドはどうでしようか ? java. la ng. Thread クラスの static メソッド sleep ( ) を while( ! eSwitch ) { 呼んでみましよう :
をぐるぐると実行し続けています。 仮に CMP 命令 ( 5 行目 ) を実行中に 00 取 ] キー押し下げによる割り込みが起きると , プロセッサはその命令を実行して結果を EF LAGS レジスタに記録し , CS:EIP レジスタ の値 ( = 次の JE 命令を指している ) や現時点 の EFLAGS レジスタの値をスタックにセー プしてから割り込みハンドラ ( ここでは 10 叩 End ( ) 関数 ) の実行を開始します。そして割 り込みハンドラのリターン時にはスタック から CS:EIP や EFLAGS の値が再びこれらの レジスタに復旧され , 正しい状態の中で 次の JE 命令 ( 6 行目 ) の実行が始まります。 JE 命令の実行中に割り込みが起きると , やはり上と同じような仕組みで , 割り込み ハンドラからのリターン後には CMP 命令 ( 5 行目 ) から main ( ) の実行が再開します。 割り込みハンドラ loopEnd() の中で eSwitch フラグの値が 1 にセットされますから CMP 命令の結果は not equal , そこで while ループ は破れて JE 命令の条件が偽となり , 実行の 流れはフォールアウト ( 下へ落ちる ) しま す。 emp 呼 . c の 29 行目以降が実行され , 元の 割り込みべクタが復旧されて成功メッセー ジが表示されます。 8 : 17 : } 空文そのものが問題ではない ? Java の空文を while 節で使う場合 , この Emp tyTest.java のように " イベントハンドラがフ ラグをセットしてくれるのを待つ " という特 殊な状況ではなく , もっと普通の ( ? ) 状況 では正常に動作します。たとえば List4 のよ うな単純なキー入力を待つプログラムで , そ のことを確認できます。 Java の InputStream. read ( ) メソッドによる キーポードからの 1 文字入力は , キー 押し下げによる確定が必要です。テストプ ログラム Etest.java は , ユーザがキーポード の手前中央にある囘のキーを入力すると空 文を実行している while ループが破れて 14 行 目以降が実行され , 終了します。このプロ グラムの動作は , 期待どおりです。 ではなぜ , EmptyTest.java では空文によ る while ループから正常に脱けないのでしよ うか ? JDK に付随している jav 叩というプ ログラムを使って , EmptyTest. class を " 逆ア センプル " してみましよう。その結果が , F ig. 1 です。 JVM (Java V1rtual Machine) という仮想プ C プログラムの wh ⅱ e と空文 ( empty. asm より引用 ) List 3 1 : 2 : 3 : 5 : 6 : 7 : je —main endp ( 中略 ) 4 : 昶 50 : jmp ( 中略 ) —ma1n proc cmp near short ② 2250 word p して DGROUP:—eSwitch, 0 short 22250 List 4 Etest. java 1 : 2 : 3 : 4 : 5 : 6 : 7 : 9 : 10 : 11 : 12 : 13 : 14 : 16 : / * * Etest. java * “普通の”状況における Java の空文の動作を確認する }catch( IOException e ) { } system. out. println( gonna exit now. while( ( ch = system. in. read() ) try{ int ch; public static void main( string args[ ] ) { class Etest import java. iO. も / / fO て IOException * Apr. 2000 , hiwa ( 岩谷宏 ) ロセッサのアーキテクチャは , われわれに とってお馴染みの本物のプロセッサ , lntel の 8 阪 86 や Motorola の 68000 などと相当違い ます。詳しくは ffeJavaV11tual Machine Spec ⅲ ca ⅱ on ( JVMS ) の 3 章を読んでください。通 常のプロセッサとの最大の違いは , " レジ スタというものがない”という点でしよう。 J Ⅵ 4 では命令のオペランド ( 操作対象データ , 演算対象データ ) をもつばら , " オペランド スタック " と呼ばれるローカルなスタック に ( から ) ロード ( ストア ) します。 私同様 JVM について詳しくない読者は , E mptyTest. class の逆アセンプル結果を見ても よく分からない点が多いと思います。しか し今回の while ループの問題に関連する肝心 な部分は , コード番号 8 から 14 にかけての , 8 go す 0 11 1 1 getstatic # 23 く Field boolean eSwitch> 14ifeqII の箇所です。 goto 命令は直後に 2 バイトの分岐先オフセ ット値を伴いますから , 計 3 バイトです。上 の goto 11 は , 次のような 3 バイトでしよう。 0xa7 0X00 0X03 こで a7 は go 命令のバイトコード , そ して阪開 03 は " 3 バイト先 " というオフセット 値を表しています。なお , この 2 バイトは 無符号整数なので , goto による後方分岐は 不可能です。コード番号 8 から始まるこの goto 命令の , 次のコード番号は 8 + 3 = 11 で 11 となります。こんなところになぜ goto 命令 が必要かというと , while 節の場合には「条 件検査をまずやって偽ならフォールアウト C 言語フォーラム 135 現在値 , すなわち初期値のままならば false ちろんここで積まれるのはフラグ eSwitch の 次の命令のオペランドとして提供します。も ンドスタック " に積むことによって , それを スの static フィールドの値を前述の " オペラ そして 11 番にある getstatic 命令は , クラ ているようです。 が何もない場合でもそのパターンを踏襲し コード構造になることが多いので , 実行文 する , 真なら節の実行文を実行する」という
この EmptyTest. java プログラムは , Java 言語のシンタクスにおける " 空文 " の機能を 確認することが目的です。 Java の空文 (JSL 14.5 ) の書き方は C 言語と同じで , セミコロ ンを書きます : / / Java の空文 = = C の空文 このテストプログラムは , ウインドウ全 域を専有する大きなボタンを作り , その背 景色を黄色 , ボタンのテキストを " 初めは処 女のごとく " にします。テキストそのものに は何も意味がありません。 次に , パッケージグローバル注 1 ] な変数 eS witch の値が false である間 , 何もせずに待ち ます。それを 27 行目の while と 28 行目の空文 で実現しています・・・・・・いる " つもり " です。 [ 注 1 ] クラスのメンバに付けるアクセス 制御のための修飾子は : public p rotected p rivate なにも付けない の 4 種類です。最後の " なにも付けない " は " デフォルトアクセス " ( JSL6.6.1 , 6.6.5 ) と呼ばれ , 同一バッケージ内からのアク セスが可能です。このような小さな Java プログラムでは往々にして package 宣言 を省略しますが , その場合は , ひとつの コンパイル単位 ( = ひとつのソースファイ ル ) やひとつのディレクトリの中に収め られた各クラスがひとつの " 無名パッケ ージ " ( JSL7.42 ) に属すると見なされます。 変数 eSwitch は , ボタンに対するアクシ ョンリスナのメソッド内 ( 41 行目 ) で frue に セットされます。ですから , 最初に表示さ れる黄色いボタンをクリックすれば , 27 , 28 行のループが破れて 30 行目以降が実行さ れることにより , ボタンの色がピンクに変 わり , テキストが " 終わりは脱兎のごとし " に変わるはずです。 ところが現状の Emp rest プログラムでは , 黄色いボタンをクリックしてもピンクのボ タンには変わりません。ただ , イベントハ 134 C MAGAZINE 2000 6 ンドラ Experience. actionPerformed ( ) が実行 された証拠に黄色いボタンが不活色に変化 します ( 40 行目の setEnabIed (false) の結果 ) 。 空文に関するプログラムのこの振る舞い は , 長年 C 言語で培われてきた私のプログ ラミング常識に反します。イベントハンド ラの中でフラグ eSwitch の値が frue になるは ずなのに , なぜ main ( ) の while ループを脱け ることができず , 30 行目以降が実行され ないのでしようか ? List 1 に示した EmptyTest.java と似たよう なプログラムを C で書いてみましよう。それ が次に見ていただく emp . c プログラムです (List 2 ) 。ご覧のようにこの empty. c では , Ja va の awt/swing コンポーネントの addAction Listener() に相当するイベントハンドラ登 録メソッドが Turbo C/Borland C でお馴染 みの setvect() 関数です。同じく Java の Action Listener. actionPerformed ( ) に相当するハン ドラメソッドが , void interrupt というキー ワードで修飾される , やはり Turbo C/Bor land C 固有の割り込み関数です。 このプログラムで , 27 ~ 28 行目の while ル ープを脱けさせるためには , 古い PC -9801 List 2 empty. c 3 : / * 警告 ! 2 : / * Apr. 2000 hiwa * / - C の空文をテストする * / / * empty. c 30 : 29 : 28 : 26 : 25 : 24 : 23 : 22 : 21 : 20 : 19 : 14 : 11 : 7 : 6 : 5 : 4 : 1 : シリーズのコンピュータの上ではキーポー ドの左上のほうにあるげーを押して ください。 PC ー 9801 の BIOS ではこのキー だけ特別扱いされ , 単独で 5 番という割り込 みべクタ番号を与えられています。 IBM PC クローン機の上では , キーポードの右 上のほうにある新キーがこのように 単独で割り込みべクタ・・・・・・やはり同じく 5 番・・・・・・を与えられています。 C の空文 + while ループはこのような場合 にも正常に ( 。 r 期待どおりに ) 動作します。 すなわちキーなどを押すとその割り 込みハンドラの中で eSwitch が 1 にセットさ れ , ループが破れて 29 行目以降が実行さ れ , プログラムは正常に終了します。 この C プログラムの while と空文の部分 ( 27 , 28 行目 ) は , アセンプリ言語 ( 機械語 ) のコ ードでは List 3 のようになっています。何の 変哲もないコードです。この , empty. asm からの引用を見ると , 実質的な実行内容は 5 行目の CMP 命令 ( 比較命令 ) と 6 行目の JE 命 令 (jump 迂 equ 命令 ) であることが分かりま す。すなわちこの while ループは , ] キ ー ( など ) が押されるまで , CMP と JE@2@50 / * Turbo/BorIand C / C 十十でコンパイルしてください * / #include <stdio. h> #include <dos . h> 8 : #define CopyKeyIntNum 5 / / [ COPY ] キーの割り込みべクタ番号 10 : int eswi し c 場 12 : void interrupt IoopEnd( ) / / 新たな割り込みハンドラ eswitch = 1 ー 17 : main() void interrupt ( * 引 dCopyKeyHandl er ) ( eSwi tch = 0 ー puts( npress [COPY] key セ 0 exit on an Old PC ー 98. puts( npress [Sys Rq] key *for a whi le* in a Win-PC's DOS—Prompt. puts( [Sys Rq] is [Shift] 十 [print Screen] . oldCopyKeyHandler = getvect( CopyKeyIntNum setvect( CopyKeyIntNum, loopEnd / / 新ハンドラを登録 while( eSwitch = setvect( CopyKeyIntNum, oldCopyKeyHandler / / 旧状復帰 puts( empty statement is normal, thank you. ″ 18 : { 15 : } 13 : (
の 0 ①セヲフロクラミング 行われないという状態になるだけです。と くに , main 関数は処理をするスレッドを起 動するだけという作り方をしたときは , mai n 関数の終了が , 即 , プロセスの終了にな るので , main 関数を終了する前に起動した スレッドが処理を終了するのを待つような 処理をする必要があります。 List14 でコメ #include く time. h> #include く stdio. h> ファイルシェアリング め , メッセージは表示されません。 がスレッドの処理終了を待つ処理で , これ がコメントアウトされていると foo 関数のス レッドを待たずに main 関数が終了するた ントアウトされている , while(cnt); ふたつのスレッドで , 同じファイルに書 き込むことになったとき , 意図したように 書き込めないことがあります。一方のスレ ッドでファイルを開いたままで時間のかか る処理をしながらファイルへの書き込みを しているとき , 別のスレッドが同じファイ すが , f001 関数の "Task:test1" と f002 関数の キストファイル test は Fig. 8 のようになりま たとえば , List 15 の実行後に作成されたテ 順不同で行われるという結果になります。 書き込みと 2 番目のスレッドの書き込みが ルに書き込もうとすると最初のスレッドの ふたつのスレッドによるファイルの同時書き込み #include く s し d 土 0 . h > #include <stdlib. h> #include <string. h> く e てて no . h> <process. h> volatile int cnt = の # incl ude #include ff lush(fp); fprintf(fp, "Task:%s%n",p); for(i=0 く 1 の i 十十 ) { return ー cnt— printf( "fopen error[name:%s] : printf( ”宅 s start*n" ,p); time—t t; int 土ー FILE *fp; void fool(void *p) 'p,strerror(errno) t = me ( / * 時間の掛かる処理の代わり void f002 ( VO 土 d *p) cnt— fclose(fp); while(t 十 2>time(NULL) FILE *fp; int time—t セー printf( ″宅 s start%n" ,p); if い ()p = fopen("test","a"))){ printf( "fopen error[name:%s] : cnt— return ー for(i=0; i く 1 の i 十十 ) { fprintf (fp, "Task:%s%nn ,p); fflush(fp); s 基 n ” ,p,strerror(errno) - fsopen 関数を使った排他制御 #include <stdlib. h> #include く string. ね> <errno . h> く process. h> <time . h> #include く share . h> vo ねし e int cnt #include #include #include 1 6 void f002 ( VO 土 d *p) cnt— fclose(fp); while(t 十 2>time(NULL) 潺 t = time(NULL); / * 時間の掛かる処理の代わり * / ff lush(fp); fprintf(fp, "Task:%s%n" ,p); fo て ( i = 土く 1 の土十十 ) { return ー cnt- printf( "fopen error[name:%s] : 宅 s 基 n ” ,p,strerror(errno) if( ! ()p = -fsopen( ntest" , ” a ″ ,SH-DENYWR) ) ) { printf("%s start%n",p); time—t t ー FILE *fp; void fool(void *p) し = time(NULL); / * 時間の掛かる処理の代わり * / fflush(fp); fprintf( fp, "Task:%s%nn,p); for ( i = i く 1 i 十十 ) { return ー cnt— printf( "fopen error[name:%s] : %s*n",p,strerror(errno) if い ()p = —fsopen( "test" , ” a ” ,SH-DENYWR) ) ) { printf( ″宅 s start%n" 'P); time—t セー int i ー FILE *fp; int t = time(NULL); / * 時間の掛かる処理の代わり * / while(t 十 2>time(NULL) cnt— fclose(fp); return 0 ー whi ( cn に if(ret = = -1 ) exit(l); ,ret); ret = ー begin セ耻 ead ( f002 , 1024 , ” tes し 2 ” cnt 十十一 ー 1 ) ex 辻 ( 1 if(ret printf( "ret=%d*n",ret); ret = —beginthread(f001,1024, ″ tes セ 1 ” cn し十十一 int て e セー main ( ) 土 n while(t 十 2>time(NULL) fclose(fp); cnt- main ( ) int て e セー cnt 十十一 ret = —beginthread(f001,1024,"test1" printf( ”て et = 宅 d n ″ ,ret); -1 ) exit(l); if(ret cn セ十十一 ret = —beginthread(f002,1024, "test2" printf( ″て et = 宅 d 基 n ” ,ret); if(ret = = -1 ) exit(l); while(cnt); return 0 ー 特集 1 セーフテイプログラミング 15
セヲフワクラミ スレッド間の競合 List 20 の例では , 共有リソースがひとつ だけでしたが , 当然 , 共有リソースがふた つ以上になることもあります。この場合 クリティカルセクションの設定が , デッド ロックを起こさないように注意しなければ なりません。 つまり , 一方のスレッドが , あるリソー スにクリティカルセクションを設定し , 別 のスレッドがもうひとつのリソースにクリ ティカルセクションを設定したとします。 クリティカルセクションを使わない場合 # incl ude く process. h> # incl ude くセ土 me. h> # incl ude く std lib. h> #include <stdio. h> #include く windows . h> volatile int cnt = 0 ー void f002 ( vo 土 d * void fool(void * ary [ 幻 ifCOUL = cnt 十十一 int 土ー int main ( ) int ary[20]; for(i = の土く 2 土十十 ) { / * データの初期化 * / = —beginthread(f001,1024,NULL) ) { pe てて 0 て ( "—beginthread e てて 0 て″ if COUL = = -beginthread(f002,1024,NULL) ) { cn し十十一 exit( 1 perror ( "—beginthread error ″ ex 址 ( 1 return 0 ー while(cnt); time—t t ー int void f001 ( void *p ) ary[ 幻 = 19 - for(i = の i く 1 i 十十 ) { cnt— fo て ( 土 = i く 2 i 十十 ) { void f002(void *p) ary[i] = 19 ー for(; 土く 2 土十十 ) ( t = time(NULL); そして , それらのクリティカルセクション を設定したまま , さらに , お互いに別のス レッドが使用しているリソースを使おうと , クリティカルセクションが空くのを待っと , 両方のスレッドが待ちの状態に入ってしま い , プログラムが止まってしまいます (Fig. 13 ) 。このようなテッドロックを起こすプロ グラムの例は List21 のようになります。 List 21 を実行すると , ふたつのスレッドはそれ ぞれ 2 番目の EnterCriticalSection 関数で止 まってしまいます。 このようなエラーを起こさないためには 同時にふたつのリソースを占有することが ないようにします。デッドロックを解消す るために , Ⅱ st21 のふたつの配列を同時に 占有しないように書き換えると List 22 のよ うになります。 List 22 では配列血 pa Ⅳを一 時的なデータ作成用の変数として , 配列 a Ⅳ 1 や a Ⅳ 2 から読み出したデータを入 れています。また , 書き込むデータの書き 込みも血 pa 配列上にあらかじめ構成して おいて , 一気に書き込むという方法をとる ことで , ふたつの配列の同時占有をしない ようにしています。 while(t 十 5 > time(NULL)); / * 他のスレッドが割り込む確率を高めるための cnt— printf( "uvn",ary[i] クリティカルセクションを使った場合 cn セ十十一 InitializeCriticalSection(&cs); ary は ] for(i = i く 2 の i 十十 ) { / * テータの初期化 * / int 土ー int main ( ) CRITICAL—SECTION cs; int a て y [ 20 volatile 土 n セ cnt void f002 ( vo 土 d * void f001 ( void * #include <process. h> #inc lude く time. h> #include <stdlib. h> #include く stdio. h> #include く windows . h> DeleteCriticalSection(&cs); while(cnt); exit(l); perror ( ”—beginthread error" ifCOUL = = -beginthread(f002,1024,NULL) ) { cnt 十十一 ex 辻 ( 1 pe てて 0 て ( n—beginthread error ” if COUL = = —beginthread(f001,1024,NULL) ) { cnt— LeaveCriticalSection(&cs); = 19 ー ary [ 幻 for(; 土く 2 i 十十 ) { t = time(NULL); = 19 - ary は ] for(i = 土く 1 i 十十 ) { EnterCritical Section ( &CS time—t セー int i ー void f001 ( VO 土 d *p) return 0 ー for(i = i く 2 の i 十十 ) { EnterCritica lSection ( &cs 土 n し void f002(void *p) while(t 十 5 > time(NULL)); / * 他のスレッドが割り込む確率を高めるための printf("%d%n",ary[il I•eaveCriticalSection(&cs); cnt— 特集 1 セーフテイプログラミング 19
'Task:test2" が書き込まれる順番はバラバラ を開けない SH DENYWR を指定しているの たがって , こはエラーとして処理するの になっています。また , この結果は実行す で , f002 関数のなかの _fsopen 関数は Permis ではなく , ファイルが使用できるまで待つ るごとに変化します。 sion denied ( 許可の拒否 ) というエラーを起 ようにするべきです。その部分を変更した スレッド f001 と f002 の書き込みをそれぞ プログラムが List 17 で , 出力されたテキス こしているわけです。 トファイル test が Fig. 10 です ( 当然ですが れまとめて行いたい場合には , 通常の f 叩 en ここで , f002 関数ではエラーの場合は直 関数を使うのではなく , 排他制御を行える ちに戻っていますが , 排他制御によるエラ Permission denied 以外のエラーの場合い -fs 叩 en 関数を使ってファイルを開きます。 ーは通常のファイルのエラーとは違い , foo くら待ってもエラーは解消されないので , 排他制御とは , ファイルを開いている間 , 1 関数の処理が終われば解消されます。し どのエラーが起きたかをチェックしなけれ ほかのスレッドやプロセスはそのファイル 工ラー処理を修正 ( 解放されるまで待つ ) を開けないようにコントロールすることで す。ー fs 叩 en 関数を使って開いたファイルに 対して , ほかのスレッドやプロセスから , 書き込み・読み込みモードによって , 開く ことを許可したり , 禁止したりすることが できます (TabIe 4 ) 。 List 15 のプログラムを一 fs 叩 en 関数を使っ て書き直すと , List 16 のようになり , その 実行結果は Fig. 9 のようになります。画面 には「 fopen error[name:test2] : Permission denied 」というエラーメッセージが出てい ます。これは , f 。 02 関数がファイルを開こ うとしたときに , まだ , f001 関数がファイ ルを開いたままの状態だったので , 排他制 御が働いているのです。 fo 。 1 関数が書き込 みモードではほかのスレッドからファイル Fig. 8 List 15 によって作成されたテキストフ アイル test の中身 Task:test1 Task:test2 Task:test1 Task:test2 Task:test1 Task:test2 Task:test1 Task:test2 Task:test1 Task:test2 Task:test2 Task:test1 Task:test1 Task:test2 Task:test1 Task:test2 Task:test1 Task:test2 Task:test1 Task:test2 1St 1 7 # incl ude く stdio. h> ー #include く stdlib. h> #include く string. h> #include く e てて no. h > #include く process. h> #inc lude く time. h> # incl ude く share. h> ー volatile int cnt void fool(void *p) FILE *fp; int 土ー time—t し一 printf( ”宅 s start%n" を p while い ()p = -fsopen( "test" ,"a",SH-DENYWR) ) ) { if(errno ! = EACCES) { printf( "fopen error[name:%s] : %s%n",p,strerror(errno) cnt - return ー ー void f002(void *p) FILE *fp; 土 n し土ー time—t t; printf( ”宅 s start%n" ,p); while い ()p = -fsopen( "test" , ″ a ″ ,SH—DENYWR) ) ) { if(errno ! = EACCES) { printf( "fopen e てて 0 て [ na 爪 e : 宅 s ] : %s%n",p,strerror(errno) cnt- return ・ fo て ( i = の i く 1 の i 十十 ) { fprintf(fp, "Task:%s*n",p); fflush(fp); し = time(NULL); / * 時間の掛かる処理の代わり * / whi le(t+2>time(NULL) fclose(fp); cnt— for ( i = の i く 10 十十 ) { fprintf(fp, "Task:%s%nn,p); fflush(fp); t = time(NULL); / * 時間の掛かる処理の代わり * / while(t 十 2>time(NULL) fclose(fp); cnt- int main ( ) int て e セー cnt 十十一 て e セ = —beginthread(f001,1024, ntestl"); printf( "ret=%d%n" ,ret); if(ret -1 ) exit(l); cnt 十十一 ret = -beginthread(f002 , 1024 , "test2" printf( ”て et d n ″ ,ret); if(ret = = -1 ) exit(l); while(cnt); return 0 ー 16 C MAGAZINE 2000 6
を実現します。 列に値を設定する途中でⅱ me 関数を使って のスレッドがアクセスできないようにして これを使用しないからといって , C/C + + 時間を遅延させています。実はこの遅延処 おきます。 EnterCriticalSection 関数は , ほ のランタイム関数や W ⅲ dowsAPI 関数が工 理がないと , List 19 のような単純な処理で かのスレッドが引数のクリティカルセクシ ラーを起こすわけではありませんが , 意図 は , スレッド f001 の途中でスレッド f002 に ョンオプジェクトを使っていなければ , した結果を得られないという意味から考え 切り替わる確率が低すぎて問題点を見せら のオプジェクトを使用中の状態にして戻っ ると , 決して安全なプログラムとはいえま れないのです。事実 , List 19 ではこの遅延 てきます。このオプジェクトがほかのスレ 処理を無効にするだけでプログラムが一見 ッドによって使用中であれば , LeaveCritica せん。 List 19 は , クリティカルセクションを使 正常に動作するようになります。ただし , ISection 関数によってオプジェクトが解放 わずに一方のスレッド f001 関数で配列 a Ⅳ 本質的には問題が存在するので , 何千回に されるまで待ちます。このときは , オプジ に値を設定し , その値をもう一方のスレッ 1 回か何万回に 1 回かはわかりませんが , foo ェクトが解放されると , EnterCriticalSectio ド fo 。 2 関数で読み出すという例です。実行 2 関数の表示に異常が起きるということが n 関数から戻ってきます。 あります。実際のプログラミングでも単に 結果は , Fig. 11 のように f 。 01 関数が書き List19 の配列 a Ⅳへのアクセスにクリティ 換える前でもなく , 後でもない , 書き換え メッセージの表示などの主処理と直接関係 カルセクションを使うと List 20 のようにな の途中の結果を取り出しています。たとえ のない処理を追加しただけで , 動作が不安 ります (Fig. 12 ) 。 EnterCriticalSection 関数 ば , これが 20 個の装置の 1 組のサンプリン 定になることがあります。 List 19 のような や LeaveCriticaISection 関数の引数となるク グデータのようなものだとすると , 1 回目 問題はこのようなバグの原因となることが リティカルセクションオプジェクトはあら のサンプリングデータと 2 回目のサンプリ 予想されます。 かじめ , InitializeCriticaISection 関数を使っ ングデータが混じった情報は , 何の役にも 一般にはこういった複数のスレッドにま て作っておきます。使用済みとなったら , D 立ちません。 たがる共有リソースへのアクセスには , ア eleteCriticalSection 関数でオプジェクトを削 ところで , List19 を見ると f 。 ol 関数で配 クセス前に EnterCriticalSection 関数でほか 除します。 List 18 GetLastError 関数と Fo 「 matMessage 関数の使用例 , 1 一 1 #include く stdio . h> #include く stdlib. h> ヨ #include く string. h> # inc lude く e てて no . h > #inc lude く process. h> #include く time. h> #include く share. h> #inc lude く windows. h> vo latil e 土 n セ cnt = の void f001 ( void *p ) FILE *fp; int 土ー time—t t; DWORD e てて 0 て COde ー LPSTR lpMsgBuf; printf( ”宅 s start%nn ,p); while( ! ()p = -fsopen( "test" , ″ a ″ ,SH-DENYWR) ) ) { = GetLastError ( if(errorcode ! = ERROR—ACCESS—DENIED) { FormatMessage ( FORMAT—MESSAGE—ALLOCATE—BUFFER ー FORMAT—MESSAGE—FROM—SYSTEM ー FORMAT—MESSAGE—IGNORE—INSERTS, NULL, errorcode, MAKELANGID ( LANG—NEUTRAL , SUBLANG—DEFAULT ) , (LPTSTR) &lpMsgBuf , 0 ,NULL ); printf( "fopen error[name:%s] : 宅 s 基 n ” ,p, lpMsgBuf LocaIFree( IpMsgBuf); cnt— return ー FILE *fp; int time—t セー DWORD errorcode; LPTSTR lpMsgBuf; printf( ”宅 s start*nn,p); while( ! ()p = -fsopen( "test" , "a",SH-DENYWR) ) ) { errorcode = GetLastError ( if(errorcode ! = ERROR—ACCESS—DENIED) { FormatMessage ( FORMAT—MESSAGE—ALLOCATE—BUFFER ー FORMAT—MESSAGE—FROM—SYSTEM ー FORMAT—MESSAGE—IGNORE—INSERTS, NULL , errorcode , MAKELANGID(LANG—NEUTRAL, SUBLANG—DEFAULT) , (LPTSTR) &lpMsgBuf, O,NULL printf( "fopen error[name:%s] : s 基 n ” ,p, IpMsgBuf); Local Free ( IpMsgBuf cnt— return ー int main ( ) int て e セー cnt 十十一 ret = —beginthread(f001,1024, ” tes し 1 ” printf( ”て e に = 宅 d 基 n ” ,ret); if ( て e し ー 1 ) ex 辻 ( 1 cn セ十十一 ret = —beginthread(f002,1024, "test2" printf( ″て et = d 基 n ” , て e セ 土 f ( て e し -1 ) exit(l); while(cnt); return 0 ー for(i=0 く 1 の土十十 ) { fprintf(fp, "Task:%s%n",p); ff lush(fp); t = time(NULL); / * 時間の掛かる処理の代わり * / whi le(t 十 2>time(NULL) fclose(fp); cnt- fo て ( i=0 く 10 十十 ) { fprintf (fp,"Task:%s*n",p); fflush(fp); セ = time(NULL); / * 時間の掛かる処理の代わり while(t 十 2>time(NULL) fclose(fp); cnt— void f002 ( void *p ) 18 C MAGAZINE 2000 6
Fig. 11 List 19 の実行結果 ( クリティカルセクションなし ) Fig. 12 List20 の実行結果 ( クリティカルセクションあり ) , コマンドフロンプト (C) Copyright 1985-1999 Microsoft Corp. :V>List19 - ロ X ロ X コマンドプロンプト (C) 「 ight 1985 ー 1999 M に「 0S0 日「 p. 〕 : わ Li 20 日ー - 材し 0 リ 1 ー《 ( リ「リイら 0 《 / 」 1 ー へのアクセスとで , 処理が異なるというこ ードを確認するためには WSAGetLastError ネットワークエラーへの対策 とはほとんどありません。ネットワーク固 関数を使います。 有の問題があるとすれば , ネットワーク用 WinSock では , WSA で始まる一連の関数 ネットワーク対策といっても , 前述した の API を使用した処理についてです。たと 群を使用します。 WinSock によるプログラ ローカルディスク上のファイルへのアクセ えば , List 23 は W1ndows ソケット (WmSoc ミングでは , ソケットオプジェクトを作成 k) を使用したプログラムですが , 工ラーコ スとファイルサーバ上のリモートファイル して , プログラムを実行する PC の IP アドレ クリティカルセクションの競合で止まるプログラム #include く windows . h> #include く stdio . h> #include く stdlib. h> #include く time . h> #include <process. h> void f001 ( vo 土 d * void f002(void * volatile 土 n セ cnt = 0 ー int aryl [ 20 ]; CRITICAL—SECTION csl; 土 n セ ary2 [ 20 ]; CRITICAL_SECTION cs2; int main ( ) ヨ int 土 + + ) { / * データの初期化 * / for(i = の土く 2 aryl [ 幻 for(i 土十十 ) { 土く 2 の ary2 [ 幻 = 19 ー InitializeCriticalSection(&cs1); InitializeCriticalSection(&cs2); cnt 十十一 if(¯0UL = = —beginthread(f001,1024,NULL) ) { pe てて 0 て ( "—beginthread e てて 0 て″ exit(l); List 21 void f001 ( VO 土 d *p) int 土ー セ土 me ーセセー EnterCriticalSection(&cs1); for(i = 土く 1 の土 + + ) { aryl [ 幻 = 19 ー セ = time(NULL); while(t + 5 > time(NULL)); / * 他のスレッドが割り込む確率を高めるための ダミー * / EnterCriticalSection(&cs2); for(; 土く 2 の土十十 ) { aryl[i] = a 2 は LeaveCriticalSection(&cs2 LeaveCriticalSection(&cs1); cnt— return 0 ー void f002 ( vo 土 d *p) time—t t; 卩 EnterCriticalSection(&cs2); fo て ( 土 = の土く 1 の土十十 ) { ary2 は ] t = time(NULL); while(t 十 5 > t 土 me ( ) / * 他のスレッドが割り込む確率を高めるための EnterCriticalSection(&cs1); for(; 土く 2 の土 + + ) { ary2 [ 幻 = aryl は LeaveCriticalSection(&cs1); LeaveCriticalSection(&cs2); cnt— cnt 十十一 ifCOUL = —beginthread ( f002 , 1024 , NULL ) ) { perror ( n—beginthread e てて 0 て″ ex 辻 ( 1 while(cnt); fo て ( 土 土く 2 の土十十 ) { printf ( ″ a て yl [%d ] i, a て YI [ 幻 for(i = の i く 2 の土十十 ) { printf ( ” ary2 [%d ] 1 , 矼 Y2 は ] DeleteCriticalSection(&cs1); DeleteCriticalSection(&cs2 20 C MAGAZINE 2000 6
スにそのオプジェクトをバインドし , 通信 先の PC の IP アドレスとコネクトすることで , 通信状態を確立するようになっています 0 #include く stdio. h> #include <time . h> Fig. 13 スレッド A リソースの競合 リソース 2 EnterC 「 iticalSection リソース 1 EnterCriticalSection リソース 1 空き スレッド リソース 2 空き スレッド スレッド B Ente 「 C 「 iticalSection リソース 2 EnterCriticalSection リソース 1 セヲフラミ クで起きるエラーとそれに対応する WSAGe 数で取り出すことができます。ネットワー 詳細なエラーコードを WSAGetLastError 関 WSA で始まる関数でエラーが起きると , WinSock を使う場合のエラー処理 ルを実装しないという使い方もあります。 りも速度を優先して , 工ラー訂正プロトコ ムしなければなりませんが , 工ラー訂正よ トコルを定義して , それに従ってプログラ 工ラーなどが起きたときの再送要求のプロ UDP プロトコルを使用する場合には通信 ベルでその対策をする必要はありません。 うで行われるため , ソケットを利用するレ ーへの対応処理はソケットライプラリのほ 用する場合には , これらのランタイムエラ 通信というとデータ工ラーへの対策や , 再送要求などが必要だと思われますが , ソ ケットオプジェクトを TCP プロトコルで使 t レ stE な関数が返す工ラーメッセージを Tabl e5 に示します。ちょうど , 通常の Windows の OS レベルエラーコードを GetLastError 関 クリティカルセクションの競合がないプログラム ヨ #inc lude #incl ude <process. h> #include <stdlib. h> <Windows . h> ary2 [ 幻 a 1 は ] for(i = 土く 2 の土十十 ) { for(i = 土く 2 の土十十 ) { int int main ( ) CRITICAL—SECTION cs2 int ary2 [ 20 CRITICAL—SECTION CSI 土 n セ aryl [ 20 volatile int cnt = void f002(void * void f001 ( void * = 19 ー 1 1 ァータの初期化 * / InitializeCriticalSection(&cs1); InitializeCriticalSection(&cs2); cnt 十十一 ifCOUL = = -beginthread(fool ,1024,NULL) ) { pe てて 0 て ( ”—beginthread e てて 0 て” ex ( 1 cnt 十十一 ifCOUL = = —beginthread(f002,1024,NULL) ) { perror ( "—beginthread e て ro て” List 22 void fool(void *p) 1 ー time—t t; while(t 十 5 > me ( 阯 ) / * 他のスレッドが割り込む確率を高めるための t = time(NULL); tmpary[i] = 矼 YI は fo て土く 2 の土十十 ) { EnterCriticalSection(&cs1); tm は ] for(i = i く 1 土十十 ) ( int セ mpa て y [ 20 time—t int 新 void f002(void *p) cnt— LeaveCriticalSection(&cs1); a て yl は ] = tmpary[i]; for(i = の i く 2 土 + + ) { EnterCriticalSection(&cs1); LeaveCriticalSection(&cs2); while(t 十 5 > time(NULL)); / * 他のスレッドが割り込む確率を高めるための t = time(NULL); tmpary[i] = ary2[i]; for(; i く 2 土十十 ) ( EnterCritica lSection ( &cs2 = 19 tmpary[i] fo て ( 土 = i く 1 土十十 ) { 土 n に tmpary [ 20 ex 辻 ( 1 while(cnt); fo て ( 土 = の i く 2 i + + ) { printf("aryl[*d] = 宅 d n ” for(i = i く 2 i 十十 ) { printf( ″ a て y2 は d ] = d n ” DeleteCriticalSection(&cs1); DeleteCriticalSection(&cs2); return 0 ー i, yl は ] 土 , a 2 [ 幻 LeaveCriticalSection(&cs1); EnterCriticalSection(&cs2); for(i = 土く 2 土十十 ) { ary2[i] = tmpary[i); LeaveCritical Section ( &cs 2 特集 1 セーフテイプログラミング 21
プログラミングレッスン 3 a 言語 マ早クーー、 言語を始めよ のような修飾子を導入するのです。この修 にも vola ⅱ le があるではありませんか ! そ 飾子の意味は , 「この変数に関しては最適化 うだよなー なんといっても Java 言語のべ コンパイルをしてもよい / してください」と ースは C / C + + だから , あって当然か・・・・・・ いう指示です。あるいは javac/java に最適化 というわけで , 今回の Java の空文の一件 オプションがあってもよいでしよう。今の は急転直下 , アンチクライマックスの解決 プロセッサとメモリは結構高速ですから , デ とあいなりました。 List 1 の EmptyTest.java フォルトを「最適化なし」にしても通常の実 の状態 , すなわち while 節の実行文が空文で 務アプリケーションでは目に見える性能劣 ある状態のままで , 12 行目の変数宣言 化は絶対に生じないと私は思います。 static boolean eSwitch; 最初なぜ私は , Java には volatile がないと を次のように書き換えます。 いう印象でいたのか ? それは , 「 Java は C/ static VO は 1 ⅱ e boolean eSwitch; たったこれだけのことで , めでたく黄色 C + + に比べるとずっと高級な高級言語です から , プログラマに低レベルな注意力を要 いボタンはピンクのボタンへと変わってく 請するような仕組みはもはや存在しないは れるのであります。 、の言語を語めよう a 言語プロ ? ラミイグレッス / 上 ) ずである」という漠然とした気持ちでいたか 問題はあっけなく解決。こうしても jav 叩 らですね。しかもスレッドやイベントハン の出力は前とまったく同じですから , volatile 結城浩犀るプゴグちミゾツネ心名あ ドラをフル活用する Java 言語では , 「この変 への対応は Java バイトコード (JVM の命令 ) = 年めのし語人門書。本を ( 上巻 ) ま、 の知識や経験のない初心者 数がどこかよそで変えられている / 使われて →実プロセッサの実行コードという翻訳過 いるかも知れへん = 最適化はバグにつなが 程で行われていることが明らかです。 る」という状況が非常に多くなっています。 でもなぜ , non-static メソッドや static メ ソッドでもほかのクラスのメソッドの場合 言い換えると C で volatile を使わなくては ならないような特殊な場面が , Java ではど には , この最適化を免れてしまうのか ? ちらかというと一般的な場面です。それに それはおそらく , VMC の実装において , また , いくらコンパイラの知能に限界があ これらのケースでは , 変えられていないよ うに見える変数でも呼ばれたメソッドの中 るといっても , while( ! f00 ) で変えられているかも知れへんから最適化 みたいな節があれば「この f00 の値はいつか せんでおこう , という知恵が働いているの づら どこかで・・・・・・ソース面からは見えない場所 でしよう。 で・・・・・・変えられるのだな」と子どもでも判断 しかしですね , と私はここで声を大にし できるでしよう。変わりうるからこそ , while て言いたい。そもそもプログラムを書いて の条件式になっているわけですよ ! だか 「この変数は volatile にしとか いくときに ら小賢しく最適化をせずに , ループの毎回 んとやばいな」ということを , いちいち , 正 で f 。。を律儀に馬鹿正直にフェッチすべき 確に判断するのはたいへんです。実務プロ なのです。 グラマにそこまで細かい低レベルの注意を 「 Java ではデフォルトで最適化なしにせよ』 求めるのは , 酷であります。 C 言語の経験 という主張には , もちろん賛否両論がある や , オプティマイジングコンパイラの問題 でしよう。でも実務プログラミングにおけ 点に関する知識のない , プログラミングに るユーザプログラミング / ビジネス現場プロ →新から入った初心者プログラマにと Java ロロロ グラミングのすそ野を Java によって拡大す っては , これは尚更のことです。 るためには , 「楽屋裏におけるコンパイラの ですので言語の設計 / 実装方式としては , 仕事ぶりのような , 低レベルの知識や低レ 「デフォルトでは最適化をしない』を貫くの ベルへの注意は要らない」が絶対的な条件 が正解ではないでせうか ? そして vola ⅲ e であるはずです。 とは逆に , 経験豊富なべテランプログラマ そしてむしろ , 最適化をプログラマが必 のための修飾子として , たとえば nonvolatile B5 変 376 ページ ISDN 4-7973-0803-6 本体価格 2,400 円 4 ~ 言語 プロクラミンク をレッスン 0 「ツら羡クトめ 版出劵 オプジクト指向を始めよう Ja 言語プログラミングレッスン ( 下 ) 結城浩著 結城浩によプ朝ツラミング初心者のための ・ Java 言語入門書体書て下巻 ) 、ではぐ上巻 の知識を踏まえ、クラス、クラスの継承例外 , ′スレッド、バヅケツなど、オブジェクト指向 の概念を、多くのサンプルプログラムを使って、 明快に解説しているいーこ B5 変 344 ページ ISDN 4-7973-1 OI 0-3 本体価格 2400 円 好評発売中 ! SOFT ソントバングバブリッシング株式会 BANK 物れP://b00トS. softbank. c 0. jp/ ublishing EL : 03-5642- 白 T00 ~ 示価格は税別 ) C 言語フォーラム 139