SSブログ

曲がった迷路その34 - NSThreadとNSAutoreleasePool [曲がった壁を持つ迷路の生成]

前回の続き。このまま休みに入ると何をやってたのか忘れてしまうのでキリがいいところまで書いておく。

世間で言われているようにNSOperationのmainメソッドの中でAutoreleasePoolのインスタンスを作らないとリークするか、というとそうではない
ではどうなっているのか?

どうなっているのかを見るためにちょっと実験してみる。

NSAutoreleasePool

NSAutoreleasePoolにはドキュメントされてないけどデバグに使うと思われるメソッドがある。
+ (void)showPools;
これはFoundation frameworkのclass-dumpを読んでいて見つけた。こういう隠しメソッドと言うかデバグ痕跡のようなものがときどきある。こういうドキュメントされていないメソッドはOSのバージョンによって動作が違ったり、極端な場合全然違う意味だったりそもそも存在しなかったりするので注意が必要である。見つけたバージョンはMacOS X10.5.7のFoundation.framework 6.5.8。

これをtestClassのmainメソッドの中に埋めてみる。
- (void)main
{
    int count = 0;
    [NSAutoreleasePool showPools];   // (1)
    while (count < maxc) {
        [NSThread sleepForTimeInterval:1.0];
        count ++;
    }
}
これをコンパイルすると、(1)のところで警告が出るけど放ったらかしておく。それでさっきのコードを実行してみる。

NSThreadでdetachした方は
ThreadAndPool[31510] No autorelease pools.
とでて、そのあと同じリークするよ、というのが続く。

NSInvocationOperationの方は
ThreadAndPool[31550] Pool (level = 0): 1 objects ====================
ThreadAndPool[31550] 	0x1051c0 (NSInvocationOperation)
と出る。これはNSAutoreleasePoolのインスタンスがあって、そこには一つのオブジェクトが登録されている、ということになる。

ここで問題なのは、このログにあるPool (level = 0)という記述で、このlevelというはNSAutoreleasePoolのインスタンスの入れ子の数を表しているらしい。

そうだとすると、これはmain()関数の先頭で作られたインスタンスで、その証拠にmain()関数で作られたNSInvocationOperationのインスタンスが登録されている、となっている。

つまり、NSOperationQueue、あるいはstartメソッドでNSAutoreleasePoolのインスタンスが作られているからリークしなくなったわけではない、ということになる。

じつはNSOperationはずるをしてthreadを起こしてないんじゃないか、と思ってmain()関数の方に
    NSLog(@"main function on main thread? %d", [NSThread isMainThread]);
TestClassのmainメソッドの中に
    NSLog(@"TestClass on main thread? %d", [NSThread isMainThread]);
と書いて、NSInvocationOperationを使った方を実行してみると
ThreadAndPool[31924] Pool (level = 0): 1 objects ====================
ThreadAndPool[31924] 	0x1051c0 (NSInvocationOperation)
2009-08-07 23:04:55.509 ThreadAndPool[31924:1503] TestClass on main thread? 0
2009-08-07 23:04:55.515 ThreadAndPool[31924:10b] main function on main thread? 1
と表示されて、main()関数の方はYES(=1)でTestClassのほうはNO(=0)となった。つまり、ちゃんと別threadで動いているということになる。

もちろん、これはNSLogなんかで書かなくてもデバッガの上で確認できる。
デバッガのスタックトレースを見ると、どちらの場合もpthread(_pthread_startと言う関数が新しいthreadで呼ばれて)が起きて__NSThread__main__という関数から呼ばれていることがわかる。

まとめ

今回の実験をまとめてみると
  • NSThreadでdetachした場合、NSAutoreleasePoolのインスタンスを作らないとリークする
  • NSOperation(のサブクラス)のmainメソッドの中でNSAutoreleasePoolを作らなくてもよい
  • それはNSInvocationOperationを使うときも同じ
  • NSOperation、NSInvocationOperationの場合、NSAutoreleasePoolはmain threadにあるインスタンスが使われる
これだけではthreadに関しては同じ作業をしているように見えるのになぜ、NSThreadを明示的に呼んだときと、NSOperationなどを使ったときとNSAutoreleasePoolに対する動作が違うのかよくわからない。

どうやら今回のような簡単な場合、mainメソッドの中でautoreleaseした場合、main threadの一番外にあるNSAutoreleasePoolに登録されるようである。そうするとmain threadが終わらないと解放されないオブジェクトになるので、やっぱりmainメソッドの中で専用のPoolを作る方がいい、ということになる。今回、Foundation Toolだったのでこうなっただけで、NSApplicationを作ってイベントループを回すとまた違った動作になるのかもしれない。やってないのでわからない。

Appleとしてはスジが通っているけど、なぜこういう仕様にしたのかよくわからない。NSThreadと同じにNSAutoreleasePoolを作れ、としたほうがずっとわかりやすい。なにか理由があるんだろうな。

そういえば、NSAutoreleasePoolが不思議な動作をするのをときどき見ることがある。
例えば、普通NSAtuoreleasePoolのインスタンスの生成と破棄は入れ子にすることになっているけど、そうなっていないとき(先に作ったインスタンスを先に破棄するとか)には、必ずしも登録されたNSAutoreleasePoolのインスタンスが破棄されるタイミングでreleaseが呼ばれるとは限らない。どういうメカニズムになっているのかよくわからない。

そもそもObjective-CのオブジェクトにReference Countに対応するインスタンス変数がない。いったいretainCountメソッドが返す値はどこに蓄えられているのか(CのAPIであるCoreFoundationにはちゃんとオブジェクトを表す構造体にフィールドがあるらしい)?
デバッガで見てもisaのあとはすぐほかのインスタンス変数が詰まっている。

うーん、不思議だ。奥が深い。

nice!(0)  コメント(6)  トラックバック(0) 

nice! 0

コメント 6

mkino

NSObjectの実体はCore Foundationのオブジェクトなので、そっちの構造体のフィールドを使っています。
by mkino (2009-08-12 17:31) 

decafish

コメントありがとうございます。やっぱりそうなんですか?
不思議に思うのは例えば、Objective-Cのレベル(CoreFoundationではなく)でNSStringを作ってデバッガで「メモリとして表示」で見ると実体はNSCFStringで、isaポインタの後ろにそれらしいフィールドが続いているのがわかるのですが、NSObjectのサブクラスを作って同じように見るとisaのあとにはすぐインスタンス変数が続いていて、_rcの余地がないように見えます。
どうなっているのでしょうか?
by decafish (2009-08-13 19:30) 

SY

mkinoさんのコメントがあるので、尻込みしていましたが…今更フォローしてみます。#気付いていらっしゃらないかもしれませんが、mkinoさんはHMDTの人ですよ。
 NSObjectの実体を上流までたぐればCFRuntimeBaseがあります。ここに_rcが入っています。NSCFStringも実体はCFStringRef。やはりCFRuntimeBaseを持つので、こちらの_rcを使っています。非GCではCocoaのRCとCFのRCは(おそらく)完全に同調してますので、Cocoaの-retainはCFRetain(), -releaseはCFRelease() をやっていそうです。これらのCF関数はCFTypeRefというCFにおけるgenericなクラス?が引数になります。つまり本文中で仰っていた通り、Cocoa環境にRC変数はなく、CF環境にあると思われます。
 isa についての記述はよく分かりませんが、objc/runtime.h, objc/objc.h を見るとobjc1ではisaの中身はClass=objc_class構造体のポインタなので、RCに関する情報は含まないはずです(objc2は不明)。
 GCになるとObjcRC-CFRCが非同調なんで、何がどうなっているのかはよく分かりませんが、CFRetainCount==0 && Cocoa側で強い参照が無い、とdeallocされることは間違いないようです。手前味噌ですが、実験してみた結果のURLを載せておきます。
by SY (2010-01-17 17:35) 

decafish

コメントありがとうございます。
もちろんmkinoさん存じております。
リンク先のGC環境でのretainCountのお話、興味深く拝見しました。CFはCocoaとは別のレイヤでも動作する必要がある(例えばCocoaからCのライブラリにCFStringRefが渡されるなど)のでこうなっているのでしょうか。大変面白いです。
しかし、例えばNSStringのオブジェクトを作ってCFStringRefが帰ってきた場合にそこのメモリを見るとCFRuntimeBaseがあってretain、releaseすると_rcのフィールドが変わることが確認できるのですが、NSObjectの単純なサブクラスを作ってインスタンスを作った場合にはisaしかなく、retain、releaseしても少なくともその近傍は何も変化しません。
このことが一番よくわからないところです。
by decafish (2010-01-18 06:33) 

SY

>もちろんmkinoさん存じております。
 これは失礼しました。自分だと喜んでしまうので、お気づきでないのかと(^^;
さてNSObjectについてですが、メモリの中は僕には見ても分からないので済みません。以下僕なりの説明になりますが... typedef void * CFTypeRef; なので NSObject *ns とし、強引にキャストして次の関数に渡してみます。
CFTypeID cfTypeID = CFGetTypeID((CFTypeRef)ns);
CFStringRef str = CFCopyTypeIDDescription(cfTypeID);
はそれぞれ、値、"CFType" が返ってきます。これらはNSObjectのクラスがCFRuntimeに登録済みである事を示します。CFのインスタンスの要件はCFRuntimeBaseを持つ事。よって、NSObjectにはCFRuntimeBaseがどこかにあるだろうと推察できます。
 では、CFTypeRef を作る関数はというと CFTypeRef _CFRuntimeCreateInstance()。これをさがすと(CFのソースも一部?ほぼ全部?オープン)、
http://opensource.apple.com/source/CF/CF-550/CFRuntime.c
にあります。関数内部でCFRuntimeBase *memory が確保されています。で、返り値はというとこの memory です。つまり CFTypeRef = CFRuntimeBase から始まる領域のポインタ、です。というわけでNSObject* = CFRuntimeBase* となるはずなので、objc1ではRCは CRRuntimebase._rc でしょう。ですが、objc2では次のようになっています。
typedef struct __CFRuntimeBase {
 uintptr_t _cfisa; // objc1:_isa
 uint8_t _cfinfo[4]; // objc1の_infoとは別物。_infoは消えた?
#if __LP64__
 uint32_t _rc;
#endif
} CFRuntimeBase; // declared in CFRuntime.h
 objc2のCFRetain()の本体は_CFRetain()で, CFRuntime.cの下の方にあります。で、いじっているのが64bitでは_rc、32bitでは_cfinfo[4](使われている__CFBitfieldGetValueはCFInternal.hに)となっていて分けられています。なのでこれらがRCの正体かと。よってobjc1も2もRCはCFRuntimeBaseにあるという結論です。32/64bitもGCも絡んでいるので、ちょっと読み難いですが(笑)#ちなみに、Dynamic objcでは、Toll-Free(2), p129-130 にobjc1の_CFRuntimeCreateInstance()の解説があります。
by SY (2010-01-18 20:43) 

decafish

丁寧なコメントありがとうございます。僕も先日からmkinoさんの記事とCoreFoundationのソースとをにらめっこしています。
_cfinfo[]はGCと両立させるために変更されたようですね。上位2バイトが以前の_info、最後の1バイトがrcの役割を果たしているように読めました。64bitになったときにどうなっているのかは、ちゃんと追えていません。I/O Kitでもそうですが低レベルの表現の多いコードは読み慣れていなくて難しいです。
せっかくコメントを頂いているのでこの話はちょっとまとめてみます。
よろしくお願いします。
by decafish (2010-01-19 07:55) 

コメントを書く

お名前:
URL:
コメント:
画像認証:
下の画像に表示されている文字を入力してください。

トラックバック 0