LoginSignup
17
9

More than 5 years have passed since last update.

Task.Runでクロージャを使うと意図しない挙動を取ることがある(追記あり)

Last updated at Posted at 2018-09-19

どー言うことか


static void Main(string[] args)
{
    for (int i = 0; i < 10; i++)
    {
        Task.Run(()=> Console.WriteLine(i));
    }

    Console.ReadLine();

}

こんな風に書いたら

6
10
5
8
5
10
8
8
6
5

こんな結果になった。

なぜこんなことが起きたのか

一見すれば、上記のコードはTask.Runメソッドは同期的に実行されるiも、問題なくインクリメントしていくように見えるが、実際は予想できない結果になった。
これは、Task.Runメソッド内でiをキャプチャしてるので、クロージャになっている。そのため、以下のように展開されてしまっているのだ。


using System;
using System.Threading.Tasks;

namespace ConsoleApp1
{
    public class Program
    {
        private class Anonymous
        {
            public int i;

            public void Proc()
            {
                Console.WriteLine(i);
            }
        }

        static void Main(string[] args)
        {

                var anony = new Anonymous();

            for (int i = 0; i < 10; i++)
            {
                anony.i = i;
                Task.Run(anony.Proc);
            }

            Console.ReadLine();

        }
    }
}

このように、匿名クラスのフィールドへいったん1度代入して、その後にそのフィールドをコンソールへ出力する形となる。
なので、従って、anony.iの更新と、Task内でのConsole.WriteLineの実行が制御されることなく行われるので、意図しない予見不可能な結果となる。

わーくあらうんど(追記)

何も難しいこと考えずに、@munielさんや、@acpleさんから頂いたご指摘の通り、
下記のように、ループブロック内で一時変数に代入すれば解決が出来る。


using System;
using System.Threading.Tasks;

namespace ConsoleApp1
{
    public class Program
    {
        static void Main(string[] args)
        {

            for (int i = 0; i < 10; i++)
            {
                var tmp = i;
                Task.Run(() => Console.WriteLine(tmp));
            }
            Console.ReadLine();
        }
    }
}

これが問題なく動く理由

なぜ一時変数に代入することで、問題なく動くようになったのかはデコンパイル結果を見ればわかる。
下記は、デコンパイルした結果を再構成したソース。


using System;
using System.Threading.Tasks;

namespace ConsoleApp1
{
    public class Program
    {
        class Anonymous
        {
            public int tmp;

            public void Proc()
            {
                Console.WriteLine(tmp);
            }
        }
        static void Main(string[] args)
        {

            for (int i = 0; i < 10; i++)
            {
                var anonymous=new Anonymous();
                anonymous.tmp = i;
                Task.Run(anonymous.Proc);
            }
            Console.ReadLine();
        }
    }
}



最初と異なり、匿名クラスのインスタンスが、ループ毎に作成されそのフィールドに値が代入されてProcメソッドが呼ばれる。
従って、Procが見ているtmpフィールドは実行されるタスク毎に独立する形となり、齟齬無く実行されることになる。

但し、匿名クラスがループ毎に作成されると言うことはループの数が大きい場合、GCが走りまくってパフォーマンスが悪化する恐れがあることは
意識しておかないとマズいかも知れない。

まとめ

λの中でクロージャを使用した場合、大体は意図通りに動くけど、時として上記のようにぱっと見、意図通り動くはずのものが動かないことがある。
なので、クロージャを試用したときはその変数はローカル変数とは別の性質を持つと言うことを意識する必要があると思う。

また、ワークアラウンドのご指摘を頂いた@munielさんや、@acpleさん
有り難うございました!

17
9
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
9