読者です 読者をやめる 読者になる 読者になる

SIN@SAPPOROWORKSの覚書

C#を中心に、夜な夜な試行錯誤したコードの記録です。

Xamarin.iOS Parallel.ForEach

【 Xamarin 記事一覧 】

Parallel.ForEach

以前(Xamarin.Android Parallel.Foreachを回してみる - SIN@SAPPOROWORKSの覚書)に、Androidでは問題なく威力を発揮したParallel.ForEachを、今回はiPhoneで回して見ました。


public override void ViewDidLoad() {
    base.ViewDidLoad();

    var button = UIButton.FromType(UIButtonType.RoundedRect);
    button.Frame = new RectangleF(0, 20, (float)View.Frame.Width, 40);
    button.SetTitle("Start", UIControlState.Normal);
    View.AddSubview(button);

    var textView = new UITextView(new CGRect(0, 60, (float)View.Frame.Width, (float)View.Frame.Height - 60));
    View.AddSubview(textView);

    button.TouchUpInside += (sender, args) => {
        var collection = new[] { 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 };
        var sb = new StringBuilder();

        //----- 通常ループ
        var stopwatch = System.Diagnostics.Stopwatch.StartNew();
        foreach (var item in collection) {
            sb.Append(item + "\r\n");
            Thread.Sleep(1000);
        }
        stopwatch.Stop();
        sb.Append(stopwatch.ElapsedMilliseconds.ToString("Normal : 0[ms]\r\n"));

        //----- 並列ループ
        stopwatch.Restart();
        System.Threading.Tasks.Parallel.ForEach(collection, item => {
            sb.Append(item + "\r\n");
            Thread.Sleep(1000);
        });
        stopwatch.Stop();
        sb.Append(stopwatch.ElapsedMilliseconds.ToString("Parallel : 0[ms]\r\n"));

        textView.Text = sb.ToString();
    };
}

iPhone4 iPhone6
f:id:furuya02:20141223231332p:plain:w150 f:id:furuya02:20141223231334p:plain:w150

当初、図のように、iPhone4 A4 1コア(シングルコア)で「10030:10032 差なし」、iPhone6 A8 2コア(デュアルコア)で「10011:5024 約半分」、というような結果が出たので、いい感じ・・・と思っていたのですが、

デュアルコアであるはずのMac mini のシュミレータで、3分の1というような変な値が出たので、おかしいなと色々やっているうちに、ループが抜け落ちていることに気が付きました。

ループの欠落

下の図は、何回か実機(iPhone6)で実行した結果ですが、パラレルで順番が変わっているのは良しとして、欠落しているのは不味いでしょ。

30,40が無い 40,60が無い 90が無い
f:id:furuya02:20141223232227p:plain:w150 f:id:furuya02:20141223232228p:plain:w150 f:id:furuya02:20141223232230p:plain:w150

[追記]StringBuilderが怪しい?

欠落しているのに、処理時間が同じなのもちょっと変なので、以下のコードで再度確認したところ、Tasks.Parallel.ForEachは無実のようです。

button.TouchUpInside += (sender, args) => {
    //100回のループを50回テストする
    var sb = new StringBuilder();
    for (var i = 0; i < 50; i++) {
        var total = 0;
        System.Threading.Tasks.Parallel.ForEach(
            Enumerable.Range(1, 100),
            () => 0,
            (value, state, local) => local += value,
            local => Interlocked.Add(ref total, local)
        );
        sb.Append(string.Format("try : {0} total : {1}\n", i, total));
    }
    textView.Text = sb.ToString();
};

f:id:furuya02:20141224004721p:plain:w150

[追記]StringBuilderをNSMutableStringに変更

Stringbuilderの使用をやめ、NSMutableStringに変更したところ、まったく問題が無くなりました。

button.TouchUpInside += (sender, args) => {
    var collection = new[] { 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 };
    var sb = new NSMutableString(); 

    //----- 通常ループ
    var stopwatch = System.Diagnostics.Stopwatch.StartNew();
    foreach (var item in collection) {
         sb.Append(new NSString(item + "\r\n"));
         Thread.Sleep(1000);
    }
    stopwatch.Stop();
    sb.Append(new NSString(stopwatch.ElapsedMilliseconds.ToString("Normal : 0[ms]\r\n")));

    //----- 並列ループ
    stopwatch.Restart();
    System.Threading.Tasks.Parallel.ForEach(collection, item => {
        sb.Append(new NSString(item + "\r\n"));
        Thread.Sleep(1000);
    });
    stopwatch.Stop();
    sb.Append(new NSString(stopwatch.ElapsedMilliseconds.ToString("Parallel : 0[ms]\r\n")));

    textView.Text = sb.ToString();
};

f:id:furuya02:20141224010215p:plain:w150 f:id:furuya02:20141224010217p:plain:w150 f:id:furuya02:20141224010218p:plain:w150

結論としては、iOSでStringBuilderがスレッドセーフになっていないって事でいいのでしょうか・・・

[追記]StringBuilderはスレッドセーフでは無い

ノイエさん(http://neue.cc/)から、「iOSじゃなくてもStringBuilderはThreadSafeではないのでは?」と指摘を受けました。

っと合わせて、lockが最小になるサンプルを頂きました。
https://gist.github.com/neuecc/ac773bff6a38a3b7248e

再度、StringBuilder版に戻してみます。

button.TouchUpInside += (sender, args) => {
    var collection = new[] { 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 };

    var result = new StringBuilder();
    //----- 通常ループ
    var stopwatch = System.Diagnostics.Stopwatch.StartNew();
    foreach (var item in collection) {
        result.Append(item + "\r\n");
        Thread.Sleep(1000);
    }
    stopwatch.Stop();
    result.Append(stopwatch.ElapsedMilliseconds.ToString("Normal : 0[ms]\r\n"));

    //----- 並列ループ
    stopwatch.Restart(); 
    System.Threading.Tasks.Parallel.ForEach(collection, () => new StringBuilder(), (x, option, sb) => {
        sb.Append(x + "\r\n");
        Thread.Sleep(1000);
        return sb;
    }, sb => {
        lock (result) {
            result.Append(sb.ToString());
        }
    });
    stopwatch.Stop();
    result.Append(stopwatch.ElapsedMilliseconds.ToString("Parallel : 0[ms]\r\n"));

    textView.Text = result.ToString();
};

完成?

参考

Xamarin.Android Parallel.Foreachを回してみる - SIN@SAPPOROWORKSの覚書
Bugzilla Main Page
xin9le.net | My Technical Adversaria



【 Xamarin 記事一覧 】