Entries

スポンサーサイト

上記の広告は1ヶ月以上更新のないブログに表示されています。
新しい記事を書く事で広告が消せます。

Snow LeopardとNSOperationQueue

タグ: Objective-C Mac NSOperation

Snow LeopardでGrand Central Dispatchが導入され、はたしてNSOperationQueueはどうなったのか。

Mac Dev Center: NSOperationQueue ClassReference

In Mac OS X v10.6 and later, operation queues use the libdispatch library (also known as Grand Central Dispatch) to initiate the execution of their operations.

「Mac OS X 10.6以降のNSOperationQueueの中身はGrand Central Dispatchである」となるほど。

As a result, operations are always executed on a separate thread, regardless of whether they are designated as concurrent or non-concurrent operations.

「その結果、オペレーションはコンカレントであろうとなかろうと常に別のスレッドで実行されることになった」ってまじで。
ということは、以前書いたコンカレントオペレーション開始スレッド調査用コードをMac OS X 10.6.1で実行すると……

<NSThread: 0x10010aeb0>{name = (null), num = 1} main
<NSThread: 0x10010d650>{name = (null), num = 2} (1)Concurrent start
<NSThread: 0x100305e00>{name = (null), num = 3} (1)Concurrent thread ->
<NSThread: 0x10010d650>{name = (null), num = 2} (2)Noncurrent ->
<NSThread: 0x100305e00>{name = (null), num = 3} (1)Concurrent thread <-
<NSThread: 0x10010d650>{name = (null), num = 2} (2)Noncurrent main
<NSThread: 0x10010d650>{name = (null), num = 2} (2)Noncurrent <-
<NSThread: 0x10010e650>{name = (null), num = 4} (3)Concurrent start
<NSThread: 0x100305e00>{name = (null), num = 5} (3)Concurrent thread ->
<NSThread: 0x100305e00>{name = (null), num = 5} (3)Concurrent thread <-
<NSThread: 0x10010d650>{name = (null), num = 2} (4)Concurrent start
<NSThread: 0x10010eb60>{name = (null), num = 6} (4)Concurrent thread ->
<NSThread: 0x10010d650>{name = (null), num = 2} (5)Noncurrent ->
<NSThread: 0x10010eb60>{name = (null), num = 6} (4)Concurrent thread <-
<NSThread: 0x10010d650>{name = (null), num = 2} (5)Noncurrent main
<NSThread: 0x10010d650>{name = (null), num = 2} (5)Noncurrent <-
<NSThread: 0x10010e650>{name = (null), num = 4} (6)Concurrent start
<NSThread: 0x100306250>{name = (null), num = 7} (6)Concurrent thread ->

なるほど、Mac OS X 10.5.6では Concurrent start は常に「1つ前のオペレーションが終わったスレッド」で行われていたが、Mac OS X 10.6.1では常に「1つ前のオペレーションが終わったスレッド以外」で行われている。
これは「startがメインスレッドから呼ばれることを前提に実装」していた場合などに問題になるかもしれない。

あとは……依存関係を持つオペレーションのキャンセルについても何か変更があるようだ。

In Mac OS X v10.6 and later, if an operation is in a queue but waiting on unfinished dependent operations, those operations are subsequently ignored.

「Mac OS X 10.6以降では、依存するオペレーションが完了するのを待っているオペレーションをキャンセルした場合、その依存関係は無視して処理が進められる」
ほう。

#import <Foundation/Foundation.h>

@interface MyOperation : NSOperation
{
    int number;
}
@property int number;
@end

@implementation MyOperation
@synthesize number;
- (void)start
{
    NSLog(@"(%d) start", self.number);
    [super start];
}
- (void)main
{
    [NSThread sleepForTimeInterval:0.4];
    NSLog(@"(%d) end main", self.number);
}
- (void)cancel
{
    NSLog(@"(%d) cancel", self.number);
    [super cancel];
}
@end

int main()
{
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    NSOperationQueue *queue = [[[NSOperationQueue alloc] init] autorelease];
    MyOperation *ops[3];
    for (int i = 0; i < 3; i++)
    {
        ops[i] = [[[MyOperation alloc] init] autorelease];
        ops[i].number = i;
    }
    [ops[1] addDependency:ops[0]]; // (1)は(0)に依存する
    [ops[2] addDependency:ops[1]]; // (2)は(1)に依存する
    for (int i = 0; i < 3; i++)
    {
        [queue addOperation:ops[i]]; 
    }
    [NSThread sleepForTimeInterval:0.1];
    [ops[1] cancel]; // (1)をキャンセル
    [queue waitUntilAllOperationsAreFinished];
    [pool drain];
}

Mac OS X 10.5.8でこれを実行すると、たとえ(1)をキャンセルしても、(1)が依存する(0)が終わるまで(2)は実行されない 。

.060 Hoge[880:1003] (0) start
.160 Hoge[880:10b] (1) cancel
.461 Hoge[880:1003] (0) end main
.463 Hoge[880:1103] (1) start
.464 Hoge[880:1003] (2) start
.865 Hoge[880:1003] (2) end main

Mac OS X 10.6.1で実行すると、(1)をキャンセルした時点で(2)が実行される 。

.076 Hoge[2879:1303] (0) start
.176 Hoge[2879:a0f] (1) cancel
.176 Hoge[2879:1503] (1) start
.177 Hoge[2879:2c03] (2) start
.479 Hoge[2879:1303] (0) end main
.578 Hoge[2879:2c03] (2) end main

「(2)は本当は(0)と(1)に依存しているんだけど、(1)が(0)に依存しているから、(1)に依存していれば(2)は(0)に依存しなくてもいいよね( ´∀`)」とか考えて実装していたらやばいかな。

Hippo Mocksなるものを見つけた

タグ: C++ Boost

http://www.assembla.com/wiki/show/hippomocks/
Hippo Mocks

( ´∀`) 逆立ちしたカバなーんだ

Excite ウェブページ翻訳
カバは馬鹿にします。

( ゚д゚ )


まあ名前はさておき。
C++のMockフレームワークはmockppくらいしか使ったことがないが、

  • ヘッダファイルを一つincludeするだけで使える
  • ライブラリのリンクは必要なし
  • Mockクラスを定義する必要もなし

という使い勝手はなかなか魅力的に見える。
試しに最新版(Hippo Mocks 3.1)をVC8上でBoost 1.40.0のユニットテストフレームワークと組み合わせて使ってみよう。

#define BOOST_TEST_MAIN
#include <boost/test/included/unit_test.hpp>
#include <boost/spirit/include/qi.hpp>
#include <boost/foreach.hpp>
#include <hippomocks.h>

// まだ実装のないインタフェース
class Receiver
{
public:
    virtual ~Receiver() {}

    virtual void Number(size_t n) = 0;
    virtual void Value(const char* value) = 0;
};

// アルファベットをスペースでトークン分割して receiver に渡す関数
bool tokenize(const char* source, Receiver& receiver)
{
    using namespace boost::spirit;

    std::vector<std::string> values;
    bool result = qi::parse(source, source + strlen(source), raw[*ascii::alpha] % ' ', values);

    receiver.Number(values.size());
    BOOST_FOREACH(const std::string& value, values)
    {
        receiver.Value(value.c_str());
    }
    return result;
};

// Hippo Mocksの文字列ポインタ比較を特殊化
template <>
struct comparer<const char*>
{
    static inline bool compare(const char* a, const char* b)
    {
        return strcmp(a, b) == 0;
    }
};

BOOST_AUTO_TEST_CASE(Test)
{
    MockRepository mocks;
    Receiver* iamock = mocks.InterfaceMock<Receiver>();
    mocks.ExpectCall(iamock, Receiver::Number).With(3);
    mocks.ExpectCall(iamock, Receiver::Value).With("foo");
    mocks.ExpectCall(iamock, Receiver::Value).With("bar");
    mocks.ExpectCall(iamock, Receiver::Value).With("baz");
    // 以下のように呼ばれることを期待
    // iamock->Number(3); 
    // iamock->Value("foo"); 
    // iamock->Value("baz"); 
    // iamock->Value("baz"); 
    BOOST_CHECK(tokenize("foo bar  baz", *iamock));
}
Running 1 test case...
unknown location(0): fatal error in "Test": std::exception: Function Receiver::Number(4) called with
 mismatching expectation!
Expections set:
test.cpp(47) Expectation for Receiver::Number(3) on the mock at 0x003AB3F8 was not satisfied.
test.cpp(48) Expectation for Receiver::Value(foo) on the mock at 0x003AB3F8 was not satisfied.
test.cpp(49) Expectation for Receiver::Value(bar) on the mock at 0x003AB3F8 was not satisfied.
test.cpp(
test.cpp(56): last checkpoint

*** 1 failure detected in test suite "Master Test Suite"

Hippo Mocksのメッセージが見切れてる(´・ω・`)
Boost.Testがカットしちゃったのか。
「期待される関数呼び出しが成されたか」を全て列挙してくれるのは親切といえば親切だが、見切れると辛い。

ちなみにこの例は「tokenize()が連続するスペースを一つのデリミタと見なさない」のが期待に沿わない部分なので、parse部分を次のように変更すればテストに合格するようになる。

// qi::phrase_parse を使えばいい話だが
bool result = qi::parse(source, source + strlen(source), raw[*ascii::alpha] % +char_(' '), values);
Running 1 test case...

*** No errors detected

Boost.Testの使い方再確認

タグ: C++ Boost

どうにも私のBoost.Testに関する知識が古い(1.34.1のドキュメント相当?)ので更新を図る。

とりあえず基本的なことは

この辺を読めば大体解る。
あとややこしいのは「unit_test.hppをincludeする前に何をdefineするか」だが、これはおおよそ以下の様にまとめられる。

#if boost_unit_test_frameworkライブラリをリンクする
# if ライブラリを動的リンクする
#  define BOOST_TEST_DYN_LINK
# endif
#else
# define BOOST_TEST_NO_LIB
#endif

// 以上はどちらかというとソースよりコンパイラオプションで定義

#if (! テストモジュールを複数のcppに分けて実装する) || 複数のcppの中のどれか一つ

# if (! ライブラリを動的リンクする) && テストモジュールの初期化を定義する
#  if 初期化にはinit_unit_test_suiteではなくinit_unit_testを使う
#   define BOOST_TEST_ALTERNATIVE_INIT_API
#  endif
# elif マスターテストスイートに名前を付ける
#  define BOOST_TEST_MODULE 名前
# else
#  define BOOST_TEST_MAIN
# endif

# if (! boost_unit_test_frameworkライブラリをリンクする)
#  include <boost/test/included/unit_test.hpp>
# else
#  include <boost/test/unit_test.hpp>
# endif

#else
# include <boost/test/unit_test.hpp>
#endif

例えば以下のように書き始めると、わざわざboost_unit_test_frameworkライブラリをビルドしておかなくても、ヘッダをincludeするだけでユニットテストフレームワークを使うことができる。
「とりあえず使う」分には楽だが、その分コンパイルは遅い。

// 複数のcppの中のどれか一つ
#define BOOST_TEST_NO_LIB // boost/test/included/unit_test.hpp をincludeする場合は省略可
#define BOOST_TEST_MAIN
#include <boost/test/included/unit_test.hpp>
// それ以外
#define BOOST_TEST_NO_LIB
#include <boost/test/unit_test.hpp>

RubyとURIと角括弧

タグ: Ruby

Redmine 0.8.4のリポジトリブラウザで角括弧を含む名前のファイルをダウンロードしようとしたところ

リポジトリに、エントリ/リビジョンが存在しません。

と表示されてダウンロードできない現象に見舞われた。
REDMINE_ROOT/log/mongrel.log を覗いてみると

svn: URL 'http://example.com/repos/trunk/[hoge].txt' は適切に URI エンコードされていません

とある。
「URI構文で角括弧を使えるのはIPリテラルの囲いとしてのみ(Uniform Resource Identifier (URI): Generic Syntax §3.2.2 Host)」なので、この文字列はたしかに正しいURIではない。

Redmineはsvnに渡すパスを REDMINE_ROOT/lib/redmine/scm/adapters/subversion_adapter.rb でエンコードしているが、Ruby 1.8.7の URI.escape はデフォルトではURIの予約文字である角括弧をエンコードしない

$ ruby -e "require 'uri'; p URI.escape('http://example.com/repos/trunk/[hoge].txt')"
"http://example.com/repos/trunk/[hoge].txt"
$ ruby -e "require 'uri'; p URI.escape('http://[::1]/repos/trunk/[hoge].txt')"
"http://[::1]/repos/trunk/[hoge].txt"

またsvn 1.6.3も、URLと思われる引数の「スペース」やら「非ASCII文字」やらはエンコードしてくれるのだが、これもまた角括弧はエンコードしない。

$ svn log 'http://example.com/repos/trunk/[ho ge].txt'
svn: URL 'http://example.com/repos/trunk/[ho%20ge].txt' は適切に URI エンコードされていません

結果、角括弧を含むURLはsvn自身のURI構文チェックに引っかかってエラーとなる。

さて、これはどのように対処するのが良いだろう。

前述のトラッカに添付されているパッチを RUBYLIB_ROOT/1.8/uri/common.rb に適用すれば、 URI.escape は期待通りのエンコードをしてくれるようになる。

$ ruby -e "require 'uri'; p URI.escape('http://example.com/repos/trunk/[hoge].txt')"
in 1
"http://example.com/repos/trunk/%5Bhoge%5D.txt"
$ ruby -e "require 'uri'; p URI.escape('http://[::1]/repos/trunk/[hoge].txt')"
in 1
"http://[::1]/repos/trunk/%5Bhoge%5D.txt"
$ ruby -e "require 'uri'; p URI.escape('[hoge].txt')"
in 2
"%5Bhoge%5D.txt"

「どうせホスト副構成要素で角括弧なんか使わないよ」というのであれば、 escape の第2引数で角括弧を含めてエンコードするよう指定するという手もある。

$ ruby -e "require 'uri'
> p URI.escape('http://example.com/repos/trunk/[hoge].txt',  /[^-_.\!~*'()a-zA-Z\d;\/?:@&=+$,]/n)"
"http://example.com/repos/trunk/%5Bhoge%5D.txt"
$ ruby -e "require 'uri'
> p URI.escape('http://[::1]/repos/trunk/[hoge].txt',  /[^-_.\!~*'()a-zA-Z\d;\/?:@&=+$,]/n)"
"http://%5B::1%5D/repos/trunk/%5Bhoge%5D.txt"

影響範囲や問題点を踏まえ、適切な方法を選ぼう。

Appendix

タグ

Blog内検索

上記広告は1ヶ月以上更新のないブログに表示されています。新しい記事を書くことで広告を消せます。