Xamarin.iOS Parallel.ForEach
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 |
当初、図のように、iPhone4 A4 1コア(シングルコア)で「10030:10032 差なし」、iPhone6 A8 2コア(デュアルコア)で「10011:5024 約半分」、というような結果が出たので、いい感じ・・・と思っていたのですが、
デュアルコアであるはずのMac mini のシュミレータで、3分の1というような変な値が出たので、おかしいなと色々やっているうちに、ループが抜け落ちていることに気が付きました。
ループの欠落
下の図は、何回か実機(iPhone6)で実行した結果ですが、パラレルで順番が変わっているのは良しとして、欠落しているのは不味いでしょ。
30,40が無い | 40,60が無い | 90が無い |
[追記]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(); };
[追記]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(); };
結論としては、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