home link download back number special issue

HMDT - Special Issue - Objective-C 最適化 - 並行プログラミングの値の更新の問題


以下のドキュメントは、Obj-C Optimization: Concurrent Programming, a quick refresher を翻訳したものです。Mac OS X でのObjective-C の最適化手法について述べられています。


Mulle kybernetiK - Tech Info: v0.2

Obj-C 最適化:並行プログラミングの値の更新の問題

ここでは、並行プログラミングの、値の更新の問題を取り扱うぜ。なんで、++myVariable はスレッドセーフにならないんだ?

(c) 2002 Mulle kybernetiK - text by Nat!

並行プログラミングの、値の更新の問題

知っての通り、共有されている変更可能なデータは、ロックを使って保護しないといけないんだ。Cocoa/TasksAndConcepts/ProgrammingTopics/Multithreading で説明されているようにな。こんな風な、グローバル変数を増加することでさえ、

static int  numberOfFiles;

void  incrementNumberOfFilesRead()
{
    ++numberOfFiles;
}

ぜんぜんスレッドセーフじゃないんだ。この、1 行の C の命令は、実際には 3 つかそれ以上の PPC の命令なんだ。これに似てるか、そのものか何だけど:

	lwz	r11,0(r9)
	addi	r0,r11,1
	stw	r0,0(r9)

それぞれの PPC 命令が実行されるときに、割り込みが発生する可能性があるんだ。このスレッドの実行が中断されて、プロセッサの興味が他のスレッドに向いてしまう。この別のスレッドは、同じグローバル変数にアクセスするために、同じコードを実行することがあるんだ!

これがどんな問題を引き起こすか、2 つのスレッドが同じコードを実行する、2 つのケースを見てみよう。最初の例では、2 番目のスレッドへのコンテキストスイッチは、問題がない。値の増加はすでに終わっているからな:

thread #0 thread #1 variable contents
lwz r11,0(r9) 0 (read)
addi r0,r11,1
stw r0,0(r9) 1 (written)
lwz r11,0(r9) 1 (read)
addi r0,r11,1
stw r0,0(r9) 2 (written)

だけど、これもよく起きるんだけど、コンテキストスイッチが値増加の途中で起こった場合には、こうなる:

thread #0 thread #1 variable contents
lwz r11,0(r9) 0 (read)
lwz r11,0(r9) 0 (read)
addi r0,r11,1
stw r0,0(r9) 1 (written)
addi r0,r11,1
stw r0,0(r9) 1 (written)

見ての通り、結果がいっしょじゃないよな!

Foundation をリソースを共有するために使う

スレッド間でリソースを共有するときの、一般的な解決方法は、ロックを使うことだ(ミューテックスとか、セマフォとか言われているやつね)。Foundation は、そのために NSLock 一族を提供しているんだ(NSLockNSRecursiveLockNSConditionLockNSDistributedLock)。次のコードは、仮に作った NSMutableDictionary クラスだ。ロックを使って、スレッドセーフにしているんだ:
@interface SafeMutableDictionary : NSMutableDictionary
{
   NSLock                *lock_;
   NSMutableDictionary   *internalDictionary_;
}
@end

// init/dealloc for lock and dictionary omitted...

- (void) setObject:(id) object
            forKey:(id) key
{
   NSException   *exception;

   exception = nil;
   [lock_ lock];
NS_DURING
   [internalDictionary_ setObject:object
                           forKey:key];
NS_HANDLER
   exception = localException;
NS_ENDHANDLER
   [lock_ unlock];
      
   [exception raise];
}

// would need to code this for objectForKey: as well

気をつけてほしいのは、ロックのための 2 つの呼び出しがあるんだけど、クリティカルセクションの中で例外が発生したら、それを捕まえないといけないんだ。もし捕まえなかったら大変なことが起こるぜ!ロックはロックしつづけてしまうから、次にこれが呼ばれたとき、デッドロックになるんだ(1)。あと、IMP を使えば、少し最適化できるよ(前の記事を見てね)。

これはそんなに遅く見えないんだけど、テストをやってみると遅い。私がやったときは、スレッドセーフのコードは 9.2 秒で、普通のコードは 5.2 秒だった。(NS_DURING を使わないと 7.0 秒で、IMP を使って NS_DURING を使わないと 6.8 秒だったよ)

スレッドセーフを必要として、何度も呼ばれるようなルーチンは、パフォーマンスの重荷になることが証明されたね!

こっからテストケースをダウンロードできるよ。これは PorjectBuilderWO で作られているんで、ProjectBuilderWo をインストールしてないなら、新しい ProjectBuilder にインポートしないといけない。または、カスタムインストールオプションを使うと、ProjectBuilderWO を developer CD からインストールできるよ。


(1) この場合、object が nil の時だけ、例外が発生するんだ。だから、自分で nil チェックをすれば、例外を回避することができて、NS_DURING のコードは消せるんだ。


Home | Link | Download | Back Number | Speciall Issue

mailto: mkino@xd5.so-net.ne.jp

HMDT