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

XORveR.com の日記

XORveR.com の公式ブログです。

using しているブロック内から yield return した先で処理キャンセルした時に using しているリソースはどうなる?

まえがき

長い処理を行うメソッドがあり、そのメソッドでは、

  1. using で IDisposable なリソースを管理しています。
  2. Task.Run で実行されています。

それを、あと付けでキャンセル可能にしたくなったとします。

方法案1

定石から CancellationToken.IsCancellationRequested でキャンセルさせればOKです。

デメリット

しかしメソッド内に CancellationToken を渡すとなると Task で動作するという前提が追加されてしまいます。
それは単体テストの修正などが厄介なので避けたいです。

方法案2

そこで折衷案としてはメソッドの時々で yield return させて、戻ってきた時にキャンセルされたか判断させるということを考えました。
処理途中では yield return true を返して処理途中であることを伝え、yield return false が返ったら処理終了です。

疑問

ここで疑問が発生。
yield return で戻ってきた時にタスクをキャンセルさせると、using で管理していた IDisposable はどうなるんだろ?

実験

実際に試してみるのが一番です。

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;

namespace YieldTest {
    class Program : IDisposable {
        bool closed = false;
        static void Main(string[] args) {
            Test(50);
            Test(100);
        }
        static void Test(int delay) {
            Debug.WriteLine("--------------------------------");
            Debug.WriteLine("Test.Start delay:" + delay);
            using (CancellationTokenSource tokenSource = new CancellationTokenSource()) {
                CancellationToken token = tokenSource.Token;
                Debug.WriteLine("Test.Start");
                var task = Task.Run(() => {
                    try {
                        // 長い処理を小分けにして呼び出す
                        foreach (var cont in Continual()) {
                            //token.ThrowIfCancellationRequested();
                            if (token.IsCancellationRequested) {
                                Debug.WriteLine("Task.Run.LoopCanceled");
                                return;
                            }
                        }
                        Debug.WriteLine("Task.Run.LoopCompleted");
                    } catch (Exception e) {
                        Debug.WriteLine("Task.Run.Catch : " + e.Message);
                    } finally {
                        // Program.Dispose はループを抜けた時なのか?の調査
                        Debug.WriteLine("Task.Run.LoopExited");
                    }
                }, token).ContinueWith((o) => {
                    // キャンセルされたら後続処理は動かない
                    Debug.WriteLine("ContinueWith.Execute");
                }, token);
                token.Register(() => { Debug.WriteLine("Token.Callback.TokenCanceled"); });
                Debug.WriteLine("Test.Wait " + delay);
                Thread.Sleep(delay);
                if (task.IsCompleted == false) {
                    Debug.WriteLine("Test.Cancel");
                    tokenSource.Cancel();
                    // Cancel してもすぐには終わらないので
                    Debug.WriteLine("Test.Wait " + 100);
                    Thread.Sleep(100);
                }
                Debug.WriteLine("Test.Exit");
            }
        }

        // 長い処理メソッド
        static IEnumerable<bool> Continual() {
            using (var program = new Program()) {
                for (int i = 0; i < 5; i++) {
                    // 長い処理
                    Thread.Sleep(10);
                    // yield で中断
                    Debug.WriteLine("Continual.YieldReturn");
                    yield return true;
                }
                program.closed = true;

                // using を抜けてから yield break すると、何故かうまく回らない
                // yield 終了
                Debug.WriteLine("Continual.YieldBreak");
                yield break;
            }

            //// yield 終了
            //Debug.WriteLine("Continual.YieldBreak");
            //yield break;
        }

        #region IDisposable Support
        private bool disposedValue = false;
        protected virtual void Dispose(bool disposing) {
            if (!disposedValue) {
                disposedValue = true;
                if (disposing) {
                    Debug.WriteLine("Program.Dispose");
                    if (closed == false) {
                        throw new Exception("not closed");
                    }
                }
            }
        }
        public void Dispose() {
            Dispose(true);
        }
        #endregion
    }
}

実行結果

--------------------------------
Test.Start delay:50
Test.Start
Test.Wait 50
Continual.YieldReturn
Continual.YieldReturn
Continual.YieldReturn
Continual.YieldReturn
Test.Cancel
Token.Callback.TokenCanceled
Test.Wait 100
Continual.YieldReturn
Task.Run.LoopCanceled
Program.Dispose
例外がスローされました: 'System.Exception' (YieldTest.exe の中)
Task.Run.Catch : not closed
Task.Run.LoopExited
Test.Exit
--------------------------------
Test.Start delay:100
Test.Start
Test.Wait 100
Continual.YieldReturn
Continual.YieldReturn
Continual.YieldReturn
Continual.YieldReturn
Continual.YieldReturn
Continual.YieldBreak
Program.Dispose
Task.Run.LoopCompleted
Task.Run.LoopExited
ContinueWith.Execute
Test.Exit

foreach を抜けた段階で、どちらのケースでも Dispose が処理されるようですので一安心です。
Dispose で例外が発生(CryptoStream の変換途中の Close とか)したとしても、キャッチして適切に扱えばOKそうです。

では!