vaguely

和歌山に戻りました。ふらふらと色々なものに手を出す毎日。

【C#】Task の話 その 2

はじめに

前回に引き続きソースコードを読んだり色々してみますよ、というお話。

Task クラスのコードを読む

Task クラスは System.Private.CoreLib.dll に含まれています。

そのソースコードcoreclr で見ることができます。

https://github.com/dotnet/coreclr/blob/master/src/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs

その中で今回呼んでいた Task.Run は下記です。

Task.cs

~省略~
public static Task Run(Action action)
{
    return Task.InternalStartNew(null, action, null, default, TaskScheduler.Default,
        TaskCreationOptions.DenyChildAttach, InternalTaskOptions.None);
}
~省略~

ここでは InternalStartNew を呼んでいるだけのようです。

Task.cs

~省略~
internal static Task InternalStartNew(
    Task creatingTask,
    Delegate action,
    object state,
    CancellationToken cancellationToken,
    TaskScheduler scheduler,
    TaskCreationOptions options,
    InternalTaskOptions internalOptions)
{
    if (scheduler == null)
        ThrowHelper.ThrowArgumentNullException(ExceptionArgument.scheduler);

    Task task = new Task(action, state, creatingTask, cancellationToken, options, internalOptions | InternalTaskOptions.QueuedByRuntime, scheduler);
    task.ScheduleAndStart(false);
    
    return task;
}
~省略~

ここでは 1.Task クラスのインスタンスを作り、 2.Task.ScheduleAndStart を呼んでいます。

これだけ見ると、タスクを作り、スケジュールに追加して実行されるのを待つ、ということのように見えます。

あれ・・・?この話どこかで見たような・・・?

Task クラスのコンストラク

Task.cs

internal Task(Delegate action, object state, Task parent, CancellationToken cancellationToken,
    TaskCreationOptions creationOptions, InternalTaskOptions internalOptions, TaskScheduler scheduler)
{
    if (action == null)
    {
        ThrowHelper.ThrowArgumentNullException(ExceptionArgument.action);
    }

    // Keep a link to the parent if attached
    if (parent != null && (creationOptions & TaskCreationOptions.AttachedToParent) != 0)
    {
        EnsureContingentPropertiesInitializedUnsafe().m_parent = parent;
    }

    TaskConstructorCore(action, state, cancellationToken, creationOptions, internalOptions, scheduler);

    Debug.Assert(m_contingentProperties == null || m_contingentProperties.m_capturedContext == null,
        "Captured an ExecutionContext when one was already captured.");
    CapturedContext = ExecutionContext.Capture();
}
  • 今回は( parent が null なので)スキップされますが、ある Task を別の Task の子にすることができるようです。
  • TaskConstructorCore といういかにも初期化してそうな名前のメソッドでは、(今回は)下記の変数に値をセットしています。
    • m_action : Task.Run で実行される delegate
    • m_stateObject : null
    • m_taskScheduler : ThreadPoolTaskScheduler のインスタンス
    • m_stateFlags : false ...?(よくわかりませんでしたorz)

スラスラわかる C# にて、 Task は ThreadPool を扱いやすくしたもの、といった話が登場していましたが、ついに関連しそうな名前が出てきました。

最後の CapturedContext 、 ExecutionContext も気になるところですね。

ExecutionContext

Microsoft Docsによると 実行中のスレッドに関するすべての情報(セキュリティコンテキスト、論理呼び出しコンテキスト、同期コンテキストなど)を持つコンテナを提供する、とのこと。

この辺りの説明がわかりやすそうな気がします(まだあまりよくわかってない)。

ここでやっていることは、Thread.CurrentThread.ExecutionContext の値が null でも Default (ExecutionContext(false)) でもない場合、 ContingentProperties の m_capturedContext に値がセットされる、ということのようです。

これの意味は ExecutionContext などがわかってくると理解できるものなのでしょうか。。。?

ScheduleAndStart

さあ初期化もできたところでスケジュールしてスタートしましょう。

という ScheduleAndStart ですが、今回知りたい一番重要そうな部分は m_taskScheduler.InternalQueueTask(this); です。

先ほど登場した、 ThreadPoolTaskScheduler に InternalQueueTask があるはずなので追ってみましょう。

https://github.com/dotnet/coreclr/blob/master/src/System.Private.CoreLib/shared/System/Threading/Tasks/ThreadPoolTaskScheduler.cs

…と思ったらありませんでした/(^o^)\

ThreadPoolTaskScheduler クラスは TaskScheduler クラスを継承しているので、そちらを見てみることにします。

https://github.com/dotnet/coreclr/blob/master/src/System.Private.CoreLib/shared/System/Threading/Tasks/TaskScheduler.cs

TaskScheduler.cs

~省略~
internal void InternalQueueTask(Task task)
{
    Debug.Assert(task != null);

#if CORECLR
    if (TplEtwProvider.Log.IsEnabled())
    {
        task.FireTaskScheduledIfNeeded(this);
    }
#endif

    this.QueueTask(task);
}
~省略~

「#if CORECLR 」というのは .NET Core で CoreCLR 上で実行されると有効になるのでしょうか。。。?

ともあれ QueueTask です。

これは派生クラスで override されています。

ThreadPoolTaskScheduler.cs

~省略~
 protected internal override void QueueTask(Task task)
{
    TaskCreationOptions options = task.Options;
    if ((options & TaskCreationOptions.LongRunning) != 0)
    {
        // Run LongRunning tasks on their own dedicated thread.
        RuntimeThread thread = RuntimeThread.Create(s_longRunningThreadWork);
        thread.IsBackground = true; // Keep this thread from blocking process shutdown
        thread.Start(task);
    }
    else
    {
        // Normal handling for non-LongRunning tasks.
        bool preferLocal = ((options & TaskCreationOptions.PreferFairness) == 0);
        ThreadPool.UnsafeQueueUserWorkItemInternal(task, preferLocal);
    }
}
~省略~

おお!? thread.Start とか書いてるぞ?と思ったら、今回は実行されないようです( options が false )。

それはそれとして、ついに ThreadPool クラスが登場しましたね。

https://github.com/dotnet/coreclr/blob/master/src/System.Private.CoreLib/src/System/Threading/ThreadPool.cs

引数の算出に使用されている、 TaskCreationOptions についてはこちらを参照。

面白くなってきたところでいったん切ります。

参照

ExecutionContext