vaguely

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

【C#】Task の話 その 4

はじめに

さて今回は、 async/await の話。

async/await

その 1 で登場したこのコード。

そのままだとメインスレッドの処理が先に終わってしまい、 Task.Run の中身が実行される前に処理が完了してしまう、という話を書きました。

Program.cs

using System;

namespace TaskSample
{
    class Program
    {
        static void Main(string[] args)
        {
            var player = new TaskSamplePlayer();
            player.Play();
        }
    }
}

TaskSamplePlayer.cs

using System;
using System.Threading;
using System.Threading.Tasks;

namespace TaskSample
{
    public class TaskSamplePlayer
    {
        public void Play()
        {
            Task.Run(() =>
            {
                Console.WriteLine("Hello world!");
            });
        }
    }
}

この程度の処理であれば Thread.Sleep などで少し待ってやれば OK な気もします。

ただ、もっと重い処理を別スレッドで行いたい場合、待ち時間より処理の時間がかかると先ほどと同じ結果が起こります。

これを処理が完了するのを待ってメインスレッドの処理が終わるようにする、という方法にはいくつかあり、その一つが async/await です。

※下記のコードは C# 7.1 以上で実行する必要があります。

Program.cs

using System;
using System.Threading.Tasks;

namespace TaskSample
{
    class Program
    {
        static async Task Main(string[] args)
        {
            var player = new TaskSamplePlayer();
            await player.PlayAsync();
            
            // player.Play() の処理が完了した後に実行される.
            Console.WriteLine("end");
        }
        
    }
}

TaskSamplePlayer.cs

using System;
using System.Threading;
using System.Threading.Tasks;

namespace TaskSample
{
    public class TaskSamplePlayer
    {
        public async Task PlayAsync()
        {
            await Task.Run(() =>
            {
                Console.WriteLine("Hello world!");
            });
        }
    }
}

C# 7.1 から Main メソッドに async キーワードを付けられるようになりました。

これでシンプルに PlayAsync() の処理を待つことができます。

IL で比較する

スラスラわかる C# などによると、 async/await の内 async キーワード自体には何か機能があるわけではなく、 await を下位互換性を保ちながら導入するために必要なもの、という位置づけのようです。

ということは、 await を追いかけるとどのようなことをしているかがわかりそうです。

これを確かめるため、 ReSharper 先生の IL Viewer に力を借りることにします。

元のコード (TaskSamplePlayer.cs)

using System;
using System.Threading.Tasks;

namespace TaskSample
{
    public class TaskSamplePlayer
    {
        public async Task PlayAsync()
        {
            await Task.Run(() => { Console.WriteLine("hello"); });
        }
        public Task Play()
        {
            return Task.Run(() => { Console.WriteLine("hello");});
        }
    }
}

これの PlayAsync() と Play() を比較してみます。

PlayAsync()

// Type: TaskSample.TaskSamplePlayer 
// Assembly: TaskSample, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
// MVID: XXXX
// Location: C:\Users\example\OneDrive\Documents\workspace\TaskSample\TaskSample\bin\Debug\netcoreapp2.2\TaskSample.dll
// Sequence point data from C:\Users\example\OneDrive\Documents\workspace\TaskSample\TaskSample\bin\Debug\netcoreapp2.2\TaskSample.pdb

.class public auto ansi beforefieldinit
  TaskSample.TaskSamplePlayer
    extends [System.Runtime]System.Object
{
  .class nested private sealed auto ansi serializable beforefieldinit
    '<>c'
      extends [System.Runtime]System.Object
  {
    .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor()
      = (01 00 00 00 )

    .field public static initonly class TaskSample.TaskSamplePlayer/'<>c' '<>9'

    .field public static class [System.Runtime]System.Action '<>9__0_0'

    .field public static class [System.Runtime]System.Action '<>9__1_0'

~省略~

    .method assembly hidebysig instance void
      'b__0_0'() cil managed
    {
      .maxstack 8

      // [10 34 - 10 35]
      IL_0000: nop

      // [10 36 - 10 63]
      IL_0001: ldstr        "hello"
      IL_0006: call         void [System.Console]System.Console::WriteLine(string)
      IL_000b: nop

      // [10 64 - 10 65]
      IL_000c: ret

    } // end of method '<>c'::'b__0_0'
~省略~
  } // end of class '<>c'

  .class nested private sealed auto ansi beforefieldinit
    'd__0'
      extends [System.Runtime]System.Object
      implements [System.Runtime]System.Runtime.CompilerServices.IAsyncStateMachine
  {
    .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor()
      = (01 00 00 00 )

    .field public int32 '<>1__state'

    .field public valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder '<>t__builder'

    .field public class TaskSample.TaskSamplePlayer '<>4__this'

    .field private valuetype [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter '<>u__1'

    .method public hidebysig specialname rtspecialname instance void
      .ctor() cil managed
    {
      .maxstack 8

      IL_0000: ldarg.0      // this
      IL_0001: call         instance void [System.Runtime]System.Object::.ctor()
      IL_0006: nop
      IL_0007: ret

    } // end of method 'd__0'::.ctor

    .method private final hidebysig virtual newslot instance void
      MoveNext() cil managed
    {
      .override method instance void [System.Runtime]System.Runtime.CompilerServices.IAsyncStateMachine::MoveNext()
      .maxstack 3
      .locals init (
        [0] int32 V_0,
        [1] valuetype [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter V_1,
        [2] class TaskSample.TaskSamplePlayer/'d__0' V_2,
        [3] class [System.Runtime]System.Exception V_3
      )

      IL_0000: ldarg.0      // this
      IL_0001: ldfld        int32 TaskSample.TaskSamplePlayer/'d__0'::'<>1__state'
      IL_0006: stloc.0      // V_0
      .try
      {

        IL_0007: ldloc.0      // V_0
        IL_0008: brfalse.s    IL_000c
        IL_000a: br.s         IL_000e
        IL_000c: br.s         IL_0066

        // [9 9 - 9 10]
        IL_000e: nop

        // [10 13 - 10 67]
        IL_000f: ldsfld       class [System.Runtime]System.Action TaskSample.TaskSamplePlayer/'<>c'::'<>9__0_0'
        IL_0014: dup
        IL_0015: brtrue.s     IL_002e
        IL_0017: pop
        IL_0018: ldsfld       class TaskSample.TaskSamplePlayer/'<>c' TaskSample.TaskSamplePlayer/'<>c'::'<>9'
        IL_001d: ldftn        instance void TaskSample.TaskSamplePlayer/'<>c'::'b__0_0'()
        IL_0023: newobj       instance void [System.Runtime]System.Action::.ctor(object, native int)
        IL_0028: dup
        IL_0029: stsfld       class [System.Runtime]System.Action TaskSample.TaskSamplePlayer/'<>c'::'<>9__0_0'
        IL_002e: call         class [System.Runtime]System.Threading.Tasks.Task [System.Runtime]System.Threading.Tasks.Task::Run(class [System.Runtime]System.Action)
        IL_0033: callvirt     instance valuetype [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter [System.Runtime]System.Threading.Tasks.Task::GetAwaiter()
        IL_0038: stloc.1      // V_1

        IL_0039: ldloca.s     V_1
        IL_003b: call         instance bool [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter::get_IsCompleted()
        IL_0040: brtrue.s     IL_0082
        IL_0042: ldarg.0      // this
        IL_0043: ldc.i4.0
        IL_0044: dup
        IL_0045: stloc.0      // V_0
        IL_0046: stfld        int32 TaskSample.TaskSamplePlayer/'d__0'::'<>1__state'
        IL_004b: ldarg.0      // this
        IL_004c: ldloc.1      // V_1
        IL_004d: stfld        valuetype [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter TaskSample.TaskSamplePlayer/'d__0'::'<>u__1'
        IL_0052: ldarg.0      // this
        IL_0053: stloc.2      // V_2
        IL_0054: ldarg.0      // this
        IL_0055: ldflda       valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder TaskSample.TaskSamplePlayer/'d__0'::'<>t__builder'
        IL_005a: ldloca.s     V_1
        IL_005c: ldloca.s     V_2
        IL_005e: call         instance void [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder::AwaitUnsafeOnCompletedd__0'>(!!0/*valuetype [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter*/&, !!1/*class TaskSample.TaskSamplePlayer/'d__0'*/&)
        IL_0063: nop
        IL_0064: leave.s      IL_00b8
        IL_0066: ldarg.0      // this
        IL_0067: ldfld        valuetype [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter TaskSample.TaskSamplePlayer/'d__0'::'<>u__1'
        IL_006c: stloc.1      // V_1
        IL_006d: ldarg.0      // this
        IL_006e: ldflda       valuetype [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter TaskSample.TaskSamplePlayer/'d__0'::'<>u__1'
        IL_0073: initobj      [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter
        IL_0079: ldarg.0      // this
        IL_007a: ldc.i4.m1
        IL_007b: dup
        IL_007c: stloc.0      // V_0
        IL_007d: stfld        int32 TaskSample.TaskSamplePlayer/'d__0'::'<>1__state'
        IL_0082: ldloca.s     V_1
        IL_0084: call         instance void [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter::GetResult()
        IL_0089: nop
        IL_008a: leave.s      IL_00a4
      } // end of .try
      catch [System.Runtime]System.Exception
      {

        IL_008c: stloc.3      // V_3
        IL_008d: ldarg.0      // this
        IL_008e: ldc.i4.s     -2 // 0xfe
        IL_0090: stfld        int32 TaskSample.TaskSamplePlayer/'d__0'::'<>1__state'
        IL_0095: ldarg.0      // this
        IL_0096: ldflda       valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder TaskSample.TaskSamplePlayer/'d__0'::'<>t__builder'
        IL_009b: ldloc.3      // V_3
        IL_009c: call         instance void [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder::SetException(class [System.Runtime]System.Exception)
        IL_00a1: nop
        IL_00a2: leave.s      IL_00b8
      } // end of catch

      // [11 9 - 11 10]
      IL_00a4: ldarg.0      // this
      IL_00a5: ldc.i4.s     -2 // 0xfe
      IL_00a7: stfld        int32 TaskSample.TaskSamplePlayer/'d__0'::'<>1__state'

      IL_00ac: ldarg.0      // this
      IL_00ad: ldflda       valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder TaskSample.TaskSamplePlayer/'d__0'::'<>t__builder'
      IL_00b2: call         instance void [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder::SetResult()
      IL_00b7: nop
      IL_00b8: ret

    } // end of method 'd__0'::MoveNext

    .method private final hidebysig virtual newslot instance void
      SetStateMachine(
        class [System.Runtime]System.Runtime.CompilerServices.IAsyncStateMachine stateMachine
      ) cil managed
    {
      .custom instance void [System.Diagnostics.Debug]System.Diagnostics.DebuggerHiddenAttribute::.ctor()
        = (01 00 00 00 )
      .override method instance void [System.Runtime]System.Runtime.CompilerServices.IAsyncStateMachine::SetStateMachine(class [System.Runtime]System.Runtime.CompilerServices.IAsyncStateMachine)
      .maxstack 8

      IL_0000: ret

    } // end of method 'd__0'::SetStateMachine
  } // end of class 'd__0'

  .method public hidebysig instance class [System.Runtime]System.Threading.Tasks.Task
    PlayAsync() cil managed
  {
    .custom instance void [System.Runtime]System.Runtime.CompilerServices.AsyncStateMachineAttribute::.ctor(class [System.Runtime]System.Type)
      = (
        01 00 2b 54 61 73 6b 53 61 6d 70 6c 65 2e 54 61 // ..+TaskSample.Ta
        73 6b 53 61 6d 70 6c 65 50 6c 61 79 65 72 2b 3c // skSamplePlayer+<
        50 6c 61 79 41 73 79 6e 63 3e 64 5f 5f 30 00 00 // PlayAsync>d__0..
      )
      // type(class TaskSample.TaskSamplePlayer/'d__0')
    .custom instance void [System.Diagnostics.Debug]System.Diagnostics.DebuggerStepThroughAttribute::.ctor()
      = (01 00 00 00 )
    .maxstack 2
    .locals init (
      [0] class TaskSample.TaskSamplePlayer/'d__0' V_0,
      [1] valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder V_1
    )

    IL_0000: newobj       instance void TaskSample.TaskSamplePlayer/'d__0'::.ctor()
    IL_0005: stloc.0      // V_0
    IL_0006: ldloc.0      // V_0
    IL_0007: ldarg.0      // this
    IL_0008: stfld        class TaskSample.TaskSamplePlayer TaskSample.TaskSamplePlayer/'d__0'::'<>4__this'
    IL_000d: ldloc.0      // V_0
    IL_000e: call         valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder::Create()
    IL_0013: stfld        valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder TaskSample.TaskSamplePlayer/'d__0'::'<>t__builder'
    IL_0018: ldloc.0      // V_0
    IL_0019: ldc.i4.m1
    IL_001a: stfld        int32 TaskSample.TaskSamplePlayer/'d__0'::'<>1__state'
    IL_001f: ldloc.0      // V_0
    IL_0020: ldfld        valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder TaskSample.TaskSamplePlayer/'d__0'::'<>t__builder'
    IL_0025: stloc.1      // V_1
    IL_0026: ldloca.s     V_1
    IL_0028: ldloca.s     V_0
    IL_002a: call         instance void [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder::Startd__0'>(!!0/*class TaskSample.TaskSamplePlayer/'d__0'*/&)
    IL_002f: ldloc.0      // V_0
    IL_0030: ldflda       valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder TaskSample.TaskSamplePlayer/'d__0'::'<>t__builder'
    IL_0035: call         instance class [System.Runtime]System.Threading.Tasks.Task [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder::get_Task()
    IL_003a: ret

  } // end of method TaskSamplePlayer::PlayAsync
~省略~
} // end of class TaskSample.TaskSamplePlayer

Play()

// Type: TaskSample.TaskSamplePlayer 
// Assembly: TaskSample, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
// MVID: XXXX
// Location: C:\Users\example\OneDrive\Documents\workspace\TaskSample\TaskSample\bin\Debug\netcoreapp2.2\TaskSample.dll
// Sequence point data from C:\Users\example\OneDrive\Documents\workspace\TaskSample\TaskSample\bin\Debug\netcoreapp2.2\TaskSample.pdb

.class public auto ansi beforefieldinit
  TaskSample.TaskSamplePlayer
    extends [System.Runtime]System.Object
{
  .class nested private sealed auto ansi serializable beforefieldinit
    '<>c'
      extends [System.Runtime]System.Object
  {
    .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor()
      = (01 00 00 00 )

    .field public static initonly class TaskSample.TaskSamplePlayer/'<>c' '<>9'

    .field public static class [System.Runtime]System.Action '<>9__0_0'

    .field public static class [System.Runtime]System.Action '<>9__1_0'

~省略~

    .method assembly hidebysig instance void
      'b__1_0'() cil managed
    {
      .maxstack 8

      // [15 35 - 15 36]
      IL_0000: nop

      // [15 37 - 16 26]
      IL_0001: ldstr        "hello"
      IL_0006: call         void [System.Console]System.Console::WriteLine(string)
      IL_000b: nop

      // [16 26 - 16 27]
      IL_000c: ret

    } // end of method '<>c'::'b__1_0'
  } // end of class '<>c'

~省略~

  .method public hidebysig instance class [System.Runtime]System.Threading.Tasks.Task
    Play() cil managed
  {
    .maxstack 2
    .locals init (
      [0] class [System.Runtime]System.Threading.Tasks.Task V_0
    )

    // [14 9 - 14 10]
    IL_0000: nop

    // [15 13 - 16 29]
    IL_0001: ldsfld       class [System.Runtime]System.Action TaskSample.TaskSamplePlayer/'<>c'::'<>9__1_0'
    IL_0006: dup
    IL_0007: brtrue.s     IL_0020
    IL_0009: pop
    IL_000a: ldsfld       class TaskSample.TaskSamplePlayer/'<>c' TaskSample.TaskSamplePlayer/'<>c'::'<>9'
    IL_000f: ldftn        instance void TaskSample.TaskSamplePlayer/'<>c'::'b__1_0'()
    IL_0015: newobj       instance void [System.Runtime]System.Action::.ctor(object, native int)
    IL_001a: dup
    IL_001b: stsfld       class [System.Runtime]System.Action TaskSample.TaskSamplePlayer/'<>c'::'<>9__1_0'
    IL_0020: call         class [System.Runtime]System.Threading.Tasks.Task [System.Runtime]System.Threading.Tasks.Task::Run(class [System.Runtime]System.Action)
    IL_0025: stloc.0      // V_0
    IL_0026: br.s         IL_0028

    // [17 9 - 17 10]
    IL_0028: ldloc.0      // V_0
    IL_0029: ret

  } // end of method TaskSamplePlayer::Play
~省略~
} // end of class TaskSample.TaskSamplePlayer

PlayAsync() の圧倒的長さ。。。

特に気になるのはこの辺り。

  • System.Runtime.CompilerServices.TaskAwaiter
  • System.Runtime.CompilerServices.IAsyncStateMachine
  • System.Runtime.CompilerServices.AsyncTaskMethodBuilder
  • try - catch

上記のキーワードでググると有益な情報がどかどか出てくるので、これ僕が何か書く必要ないやん。。。てなります。
(ありがとうございます)

なお私も ILSpy を使ってみようかと思ったのですが、オプションから async メソッドをデコンパイルしない、というオプションが見つからなかったため断念しました。
GitHub の Isssue を見てると C# の複数バージョンに対応するため?に省かれたっポイですが。

なので細かいところはそちらを参照、というところなのですが、乱雑にまとめますと

  1. await を使うと IAsyncStateMachine を継承した構造体が作られる
    (リンク先を見ると StateMachine という名前らしいです)
  2. 非同期メソッドが完了したかなどの状態は TaskAwaiter が持つ
  3. 1.が AsyncTaskMethodBuilder を使った非同期メソッドの生成や 2.の登録・監視などを行う

といった感じでしょうか。

TaskAwaiter はコードを見ると interface を継承していますが、中身は空のようでした。

また、 await で処理を待ち受けることができる型は、 Awaitable パターンに沿っていること、という制限があるようです。

この Awaitable パターンというのは特定の型を指すのではなく、 GetAwaiter() など処理に必要なメソッドが実装されていれば良い、ということのようです。

動的な型を持つ言語や Go の interface もこんな感じでしたね。

この辺りで発生するエラーをキャッチするために try - catch があるのかもしれませんね。

IL のコードを載せたために長くなってしまったので、ここで切ります。

参照

【C#】Task の話 その 3

はじめに

前回満を持して?登場した ThreadPool クラスを追いかけますよ、というお話。

ThreadPool について

まず ThreadPool とは?という話から。

スレッドの生成には時間・リソース的な負荷が大きい、という問題があります。

プログラミング .NET Framework によると、Thread の生成時に 400 もの dll がロードされるとか。。。

これをスレッドが必要になるたびに繰り返すのは無駄が大きいため、生成したスレッドを使用後も破棄せず置いておき、必要に応じて再利用する、ためのもの、と理解しています。

Microsoft Docs によると、「タスクの実行、作業項目の送信、非同期 I/O の処理、他のスレッドの代理で行う待機、およびタイマーの処理に使用できるスレッドのプールを提供します」とのこと。

なおスレッドにはメインスレッド( UI スレッド)と、バックグラウンドで動くバックグラウンドスレッドがありますが、ここでプールされるのはバックグラウンドスレッドです。

ThreadPool クラスのコードを読む

では話を戻して ThreadPool.UnsafeQueueUserWorkItemInternal から追いかけてみましょう。

ThreadPool.cs

~省略~
internal static void UnsafeQueueUserWorkItemInternal(object callBack, bool preferLocal)
{
    Debug.Assert((callBack is IThreadPoolWorkItem) ^ (callBack is Task));

    EnsureVMInitialized();

    ThreadPoolGlobals.workQueue.Enqueue(callBack, forceGlobal: !preferLocal);
}
~省略~

名前からすれば処理が実行されるのは ThreadPoolGlobals.workQueue.Enqueue でしょう。

が、その前に気になるものがありますね。

VM 。。。?

EnsureVMInitialized()

ThreadPool.cs

~省略~
// The thread pool maintains a per-appdomain managed work queue.
// New thread pool entries are added in the managed queue.
// The VM is responsible for the actual growing/shrinking of 
// threads. 
private static void EnsureVMInitialized()
{
    if (!ThreadPoolGlobals.vmTpInitialized)
    {
        EnsureVMInitializedCore(); // separate out to help with inlining
    }
}

[MethodImpl(MethodImplOptions.NoInlining)]
private static void EnsureVMInitializedCore()
{
    InitializeVMTp(ref ThreadPoolGlobals.enableWorkerTracking);
    ThreadPoolGlobals.vmTpInitialized = true;
}
~省略~

VM の初期化が終わっていなければ初期化をする、というところは特に疑問もないところです。

コメントから、 VM というのはスレッドを増やしたり減らしたりをコントロールする役割を担っているようです。

ThreadPool.cs

~省略~
[DllImport(JitHelpers.QCall, CharSet = CharSet.Unicode)]
private static extern void InitializeVMTp(ref bool enableWorkerTracking);
~省略~

dll へと処理が移ってしまいました。

QCall 、というクラスがあるのか。。。?と思ったのですが、該当のメソッドは見つかりませんでした。

メソッド名から考えると、下記の ThreadPoolNative クラスが呼ばれているようです。

comthreadpool.cpp

~省略~
void QCALLTYPE ThreadPoolNative::InitializeVMTp(CLR_BOOL* pEnableWorkerTracking)
{
    QCALL_CONTRACT;

    BEGIN_QCALL;
    ThreadpoolMgr::EnsureInitialized();
    *pEnableWorkerTracking = CLRConfig::GetConfigValue(CLRConfig::INTERNAL_ThreadPool_EnableWorkerTracking) ? TRUE : FALSE;

    END_QCALL;
}
~省略~

で、この ThreadPoolMgr クラスというのは下記にあります。

。。。だんだん自分が何しているのかわからなくなってきましたが、まだまだ進めますよ。

win32threadpool.cpp

~省略~
void ThreadpoolMgr::EnsureInitialized()
{
    CONTRACTL
    {
        THROWS;         // Initialize can throw
        MODE_ANY;
        GC_NOTRIGGER;
    }
    CONTRACTL_END;

    if (IsInitialized())
        return;

    DWORD dwSwitchCount = 0;

retry:
    if (InterlockedCompareExchange(&Initialization, 1, 0) == 0)
    {
        if (Initialize())
            Initialization = -1;
        else
        {
            Initialization = 0;
            COMPlusThrowOM();
        }
    }
    else // someone has already begun initializing.
    {
        // wait until it finishes
        while (Initialization != -1)
        {
            __SwitchToThread(0, ++dwSwitchCount);
            goto retry;
        }
    }
}
~省略~

… ThreadpoolMgr::Initialize()

そっ

全く終わる気がしなくなってきたので、この先は次の機会にしたいと思います。

結局 VM とは誰なのか

プログラミング .NET Framework によると、 ThreadPool でスレッドを扱うのは CLR ということでした。

ということは、 .NET Core では CoreCLR ということになります。

そういえば途中で CLRConfig とか見たような気がしますね。

ThreadPoolGlobals.workQueue.Enqueue

ThreadPoolGlobals > ThreadPoolWorkQueue > Enqueue を見てみます。

ThreadPool.cs

~省略~
public void Enqueue(object callback, bool forceGlobal)
{
    Debug.Assert((callback is IThreadPoolWorkItem) ^ (callback is Task));

    if (loggingEnabled)
        System.Diagnostics.Tracing.FrameworkEventSource.Log.ThreadPoolEnqueueWorkObject(callback);

    ThreadPoolWorkQueueThreadLocals tl = null;
    if (!forceGlobal)
        tl = ThreadPoolWorkQueueThreadLocals.threadLocals;

    if (null != tl)
    {
        tl.workStealingQueue.LocalPush(callback);
    }
    else
    {
        workItems.Enqueue(callback);
    }

    EnsureThreadRequested();
}
~省略~

ThreadLocal

変数を全スレッドで共有するのではなく、スレッドごとに保持できるようにするもののようです。

今回は forceGlobal が true (のハズ)なので、 workItems.Enqueue が実行されます。

workItems.Enqueue

workItems というのは ThreadPoolGlobals > ThreadPoolWorkQueue > ConcurrentQueue< object> です。

この Enqueue を見てみます。

Queue クラスは以前触れてみましたが、 ConcurrentQueue はマルチスレッドで利用できる Queue クラスです。

スレッドセーフ

ConcurrentQueue はスレッドセーフとのことですが、複数のスレッドから同時に呼び出されても正しい順番でデータの追加や取り出しができるようです。

この仕組みについても気になるところではありますが、今回はスキップすることにします。

ThreadPool.cs

~省略~
internal void EnsureThreadRequested()
{
    //
    // If we have not yet requested #procs threads from the VM, then request a new thread
    // as needed
    //
    // Note that there is a separate count in the VM which will also be incremented in this case, 
    // which is handled by RequestWorkerThread.
    //
    int count = numOutstandingThreadRequests;
    while (count < ThreadPoolGlobals.processorCount)
    {
        int prev = Interlocked.CompareExchange(ref numOutstandingThreadRequests, count + 1, count);
        if (prev == count)
        {
            ThreadPool.RequestWorkerThread();
            break;
        }
        count = prev;
    }
}
~省略~

ThreadPoolGlobals.processorCount の中身は Environment.ProcessorCount であり、実行している PC のプロセッサー数が取得できます。

ちなみに私の PC で実行したところ、 8 と返ってきました。

Interlocked.CompareExchange

Microsoft Docks によると、 numOutstandingThreadRequests と count が等しい場合、numOutstandingThreadRequests に count + 1 の値が入る、という処理とのこと。

戻り値は numOutstandingThreadRequests の元の値です。

よくわからなかったので実際に試してみることにしました。

var numOutstandingThreadRequests = 0;
var count = 0;
while (count < Environment.ProcessorCount)
{
    int prev = Interlocked.CompareExchange(ref numOutstandingThreadRequests, count + 1, count);
    Console.WriteLine("pre " + prev + " num " + numOutstandingThreadRequests + " cou " + count);
    count = prev;
}

結果は下記のとおりです。

pre 0 num 1 cou 0
pre 1 num 1 cou 0
pre 1 num 2 cou 1
pre 2 num 2 cou 1
pre 2 num 3 cou 2
pre 3 num 3 cou 2
pre 3 num 4 cou 3
pre 4 num 4 cou 3
pre 4 num 5 cou 4
pre 5 num 5 cou 4
pre 5 num 6 cou 5
pre 6 num 6 cou 5
pre 6 num 7 cou 6
pre 7 num 7 cou 6
pre 7 num 8 cou 7
pre 8 num 8 cou 7

prev と count が等しければ ThreadPool.RequestWorkerThread() が実行されて終了となるので、ループは 3 回回ることになります。

ThreadPool.RequestWorkerThread() の中身は。。。

ThreadPool.cs

~省略~
[DllImport(JitHelpers.QCall, CharSet = CharSet.Unicode)]
internal static extern bool RequestWorkerThread();
~省略~

そっ

※これも今後追いかけてみたいところです。

おわりに

ということで、 Task.Run で始まった処理は ConcurrentQueue に Queue として Task.Run の引数である delegate メソッドを追加し、バックグラウンドスレッドをリクエストしたところで終わりました。

このあと OS 側でスレッドの処理が実行される、ということになると考えられます。

Task クラス完全に理解した

先ほどのコードの中にも色々端折った部分としてエラー処理や、 Task 同士に親子関係を持たせる、といったところがあったり、今 Task を使うとなればほぼ確実に登場する、 async/await も気になるところ。

ということで引き続き Task クラスに関連して色々調べてみたいと思います。

参照

【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

【C#】Task の話 その 1

はじめに

.NET Framework/.NET Core/Mono だけでなく、 Unity でも Task クラスが使えるようになって久しい今日この頃。

相変わらずぼんやり何となくしか使えないのもなぁ。ということで、ちょっと調べてみることにしましたよ。というお話。

環境

  • Windows 10 ver.1803 17134.523
  • .NET Core ver.2.2.101
  • Rider ver.2018.3.1

Task クラスについて

そもそも Task クラスってどのようなクラスなのでしょうか。

Microsoft Docs を見てみます。

非同期操作を表します

うむ。わかりやすい。

…すみません嘘です/(^o^)\

ま、まぁとにかく非同期処理に関係するクラスであることはわかりました。

非同期処理に関係するクラス

ここで言う非同期処理、とは、ある処理を、他の処理を止めずに、また他の処理によって止められることなく実行できる、というくらいの意味として進めたいと思います。

では、 C# で非同期処理に関係するクラスって Task を含めてどのようなものがあるのでしょうか。

スラスラわかる C#によると、マルチスレッドに関するクラスとして下記の 4 つが挙げられています。

  1. Thread
  2. ThreadPool
  3. Task
  4. Parallel

1.、2.は OS に近い低級なクラス、3.、4.は1.、2.を利用して、より C# で扱いやすくしてくれる高級なクラス、という位置づけのようです。

中でも一番のベースとなっているのは Thread クラスのようです。

また、キーワードとしては async/await が挙げられますね。

後でも触れる通り、マルチスレッドによる処理 = 非同期処理 ではないと思いますので、他にも関連するクラスはあるかもしれません。

登場人物

今回の話のキーワードとなりそうなものを思いつく限り挙げていきます。

プロセス

そもそもプログラムはどのように動くのでしょうか。

という話に大きく関係するのがプロセスです。

インサイドWindows 第7版 上 によると、プログラムを実行するときに必要になる一揃いのコンテナーであり、その中には下記が含まれるとのこと。

  1. プライベート仮想アドレス領域
    • プログラムが利用できるメモリ領域のアドレス
  2. 実行可能プログラム
    • 実際に処理を実行するプログラムで 1.上に展開する形で動作する
  3. 開かれたハンドルのリスト
    • 色んなリソースを識別するためのもの
  4. セキュリティコンテキスト
    • 管理者、一般ユーザーなどの権限、UAC (ユーザーアカウント制御)の設定状態などを識別するためのアクセストーク
  5. 1つのプロセスID
    • そのプロセスを認識するための ID
  6. 一つ以上の実行スレッド

今回の話に特に関連がありそうなのは、1.、2.、6.ですね。

スレッド

インサイド Windows によると、 Windows が実行をスケジュールするプロセス内のエンティティ、とのこと。

うむ。わかりやry

いろいろググってみたところ、

  • CPU (他の装置や回路の制御、演算処理を行う)の利用単位として扱われる
  • プログラムはスレッドを作成し、実行待ちキューに登録する → CPUは登録されたキューを順に実行する -> スレッドが実行されることでプログラムの処理が実行される
  • 1 つの CPU (1 コア) が同時に実行できるスレッドは 1 つ

といったことのようです。まだぼんやりしていますが。

同時に実行できる処理は CPU 数の分だけ?もっとたくさん実行したい、という話ですが、これを解決するためにマルチタスクやマルチスレッドがあります。

マルチタスク

1つのCPUで2つ以上の処理を同時に実行したい場合、それぞれの処理を高速で切り替えながら処理を実行していくことで、瞬間的にそのCPUが実行できる処理は一つだけでありながら、まるで同時実行できているように見せる技術です。

マルチスレッド

1つのプロセス内にあるスレッドに対して、マルチタスクと同様の考え方でスレッドの高速切り替えを行い、同時実行ができているように見せる技術です。

並行プログラミング(Concurrent Programming)

2 つ以上の処理を同時に実行します。

これを実現する方法として、マルチスレッドや次の並列プログラミングがあります。

逆に言えば、処理が同時に実行できれば方法は問わない、と考えることもできます。

並列プログラミング(Parallel Programming)

複数の CPU コアを使って、それぞれ別々に処理を実行します。

PC が 2 つ以上の CPU コアを持っていることが前提となるため、ハードウェア的な制限はありますが、パフォーマンスの向上に役立ちます。

※定義は上記以外も色々ありそうですが、 Concurrency in .NET - Manning 辺りを参考に、このような理解で進めていきたいと思います。
 間違いに気づいたら修正します。

マルチタスクは置いておくとして、他の 3 つ(特に並列プログラミング)は Task クラスを使って実現できるのか、できるとすればどのように書くのか、などを見ていきたいと思います。

Task クラスを使った簡単な操作

ということで、まずは Task を使った簡単そうな処理を書いて動かしてみます。

Program.cs

using System;

namespace TaskSample
{
    class Program
    {
        static void Main(string[] args)
        {
            var player = new TaskSamplePlayer();
            player.Play();
        }
    }
}

TaskSamplePlayer.cs

using System;
using System.Threading;
using System.Threading.Tasks;

namespace TaskSample
{
    public class TaskSamplePlayer
    {
        public void Play()
        {
            Task.Run(() =>
            {
                Console.WriteLine("Hello world!");
            });
        }
    }
}

これを実行すると、なんと、何も出力されませんw

Task クラスが完全に理解できない。。。(´・ω・`)

つまり、 Task.Run の中の処理は非同期で実行されるため、処理が終わる前にメインの処理が終わってしまう、ということになります。

確認のため Program.cs で処理の完了を待つと正しく 「Hello world!」と表示されます。

Program.cs

using System;

namespace TaskSample
{
    class Program
    {
        static void Main(string[] args)
        {
            var player = new TaskSamplePlayer();
            player.Play();
            Thread.Sleep(100);
        }
    }
}

では、実際に Task.Run では何が行われるのでしょうか。

というところを次回辿ってみます。

参照

Azure DevOps 関西 2018 に参加してきました

はじめに

1/12 に 日本マイクロソフト株式会社 関西支店 で行われた、 Azure DevOps 関西 2018 に参加してきました。

jazug.connpass.com

詳細なレポートが書ければよかったのですが、ど素人ゆえ印象に残った部分を中心にメモ程度に書き残しておきます。

セッション1

@kosmosebi さんによる Azure DevOps の概要紹介です。

www.slideshare.net

私も年末年始で Azure Pipelines を使ってあれこれしていましたが、単体で使うのではなく、Azure Board など他のサービスと組み合わせることでもっと便利に使えそうだな、というのが印象に残りました。

ただ、正直なところ次のセッションで使うデモ用プロジェクトの作り直しなどで必死だったため、公開された資料を改めて読んで色々試してみたいと思います。

セッション 2

私による Azure Pipelines 使ってみた話です。

冒頭からサービス名(×Azure Pipeline -> 〇Azure Pipelines )を間違えるなど、拙い発表ではあったと思いますが、皆様に温かく見守っていただいたおかげでなんとか無事終えることができました。

speakerdeck.com

yaml で何してるの?などの話が(避けてくれてたのかもしれませんが)被らなくて助かった、とは思いましたw

以下反省。

デモ用のプロジェクト

プロジェクトのリポジトリをメインのプロジェクトフォルダで作ってしまい、テストプロジェクトが Git 管理できず困るというトラブルがありました。

PipelineSample <- ここに作る
    LPipelineSample
    LPipelineSample.Test

×

PipelineSample
    LPipelineSample <- ここに作ってしまった
    LPipelineSample.Test

普段 Rider でプロジェクトを作るときに自動で Git init してもらっていたので、まぁ気を付けましょうという話でした。

ディスプレイ設定について

PowerPoint だけ使って話をする場合、ディスプレイを拡張設定にすると手元で時間や次のスライドがサムネで見られたりして便利です。

が、今回のように Rider など別のソフトウェアを合わせて使う場合、カーソルが見つからず困る、ということがありました。

また今どれくらい話をしているか、という時間を PowerPoint で表示されるものを当てにしていたのですが、スライドショーを停止 -> 別のソフトウェアを使って説明 -> スライドショー再開 などしていたため、時間がわからなくなる、といったことがありました。

時間は別で見ることにして、ミラーリングにしておいた方が良かったかな。。。

60分の発表について

これは反省、というか感想ですが、今回初となった 60 分のセッションについて。

いつも LT でお話させてもらうことが多く、前提となる話をどの程度含めるか?といったところに悩むのですが、これだけ時間があると十分時間が取れる(むしろ早く終わりすぎてしまった)ので、助かるなぁと思いました。

あと完全にテンパった状態で始めて、我に返ってもまだ時間があるという苦笑

話の組み立て方など、他の方のお話も参考にブラッシュアップしていきたいところですね(今後チャンスがあるとして)。

セッション 3

Azure DevOps におけるテストと、 Azure Pipelines と他の CI ツールとの比較、といった内容を中心に @Posaune さんがお話されていました。

テスト

Azure DevOps には Azure Pipelines での自動テストと、 Test Plans での手動テスト支援があります。

手動テストの支援ツールというのは、(たくさんあるのかもしれませんが)初めて見たため、面白そうだなぁと思いながら聴いていました。

CI ツールなど自動化のためのソフトウェアをあまり使い慣れていないチームの場合、こういうところから入っていくのがやりやすそう?な気はします。

有料ということなのですが、30 日のお試しとかあるのかしら。

他の CI ツールとの比較

Jenkins や CircleCI などとの比較について。

yaml ではなく visual designer(GUI) で処理が書ける、というのは他のサービスだと少ない( yaml でバリバリ書く)、というのが印象的でした。

yaml での書き方に慣れてくると問題ないのかもしれませんが、特に初めて CI ツールに触れる私のような人には躓きが少なくて良いですね(私は yaml で書こうとしていましたが)。

あとは Microsoft 社が実行環境として VM を提供する辺りも違うところ。

実際には Unity などのビルドは EULA 的な問題がありそう、といった悩ましい問題もあるのですが、 .NET Core や Xcode のビルドなどであれば十分できてしまう、というのはすごいですよね。

ただ、これは次のセッションだったかもしれませんが、モバイルアプリのビルドに特化して考える場合、 App Center を使うことを考えた方が良いようです。

セッション 4

@kkamegawa さんによる Azure DevOps のセキュリティ設定周りのお話。

www.slideshare.net

これまで私がやってきたように、一人でモクモク開発するためだけに使うのであれば良いのですが、会社で導入する、といった規模が大きくなってくるときには必ず考えなければいけない話でもあります。

話の中心となるのは Azure Active Directory であったわけなのですが、全く知識がないためついていけなかった、というのが正直なところ orz

今後 Azure Active Directory について学んでみて、改めて上記資料を読み返してみたいと思います(その頃には新しい資料を出されているかもしれませんが)。

ただ印象に残っているのは、例えばあるプロダクトを作るためのプロジェクトに対する権限を付与する際に、どういう単位で行うか、というお話。

部、課などの単位にしてしまうと、異動したりなくなってしまうかもしれず、その際プロジェクトの権限割り振りはどうするの?となってしまいます。

プロダクトを制作するチームに付与すると、もし開発が中止になるなど、プロダクトが無くなったら権限の付与を考える必要はなくなる。

といった、できるだけ無くなった(変更があった)ときに困らない単位で権限を付与すると良い、という話がありました。

これは Azure Active Directory 以外でも使える話だな、と思いながら聴いていました。

あとリージョンについて、日本では東アジア(香港)辺りのものを使うことになる。

中国にもあるが、これは法律的な必要性から用意されているので中国法人じゃないとアクセスできない(少なくともログインは不可)、という話も(直接今の仕事で必要になるかはわかりませんが)印象的でした。

LT

飛び入りで @shinsukeoda さんによる SQL Server LocalDB のお話。

https://www.slideshare.net/odashinsuke/pipelines-sql-server-localdb

内容はセッションの途中で出た、 CI でテストを行うときの DB を使ったテストをどうするか?という質問に対するものです。

内容もそうなのですが、こういう飛び入りでお話ができてしまう、というのがまず凄いです。

で、その内容ですが、

  • テストにあたって毎回 DB にテーブルを作り、データを入れるというのは非常に手間
  • BACPAC という形式のファイルで元の DB からデータをエクスポートし、それをテスト開始前に読み込むことで簡素化できる
  • インポートには .NET Framework のライブラリが必要 -> PowerShell

また Microsoft-hosted agents で提供されている VM( Windows Server) 上の SQL Server は 2016 であり、SQL Server LocalDB( 開発者を対象とした SQL Server Expressの機能少ない版 )です。

そのため状況によっては使うことができない恐れはありますが、もし使えるのであればこれが安価で簡単なので、まず試してみては?とのこと。

Azure Pipelines で調べていた時も DB を使ったプロジェクトの開発、というところまで到達できなかったため、上記のような情報も参考にしたいと思いました。

おわりに

今回はいつも以上にスポンサーも豪華で、 米Microsoft社、日本マイクロソフト社、RevDebug に加え、SUBWAY 社による軽食までいただけてしまいました。

ありがたい限りです。

あと、セッション中の質問を口頭だけでなく、 Web 上でも受け付けていたのが印象深かったです。

app2.sli.do

会場の盛り上がりとしては口頭が良いのでしょうが、注目をあびる中聞きづらかったり、別のセッション中に質問が思い浮かんだりしたときに聞きづらい、という良さがあったと思います。

最後に、拙い発表であったとは思いますが、お声がけいただいた @kkamegawa さんをはじめ、主催者の方々、スポンサーの皆様、温かく見守ってくださった参加者の皆様、ありがとうございました(..)_

スライドの最後でもお話しましたが、 Azure Pipelines だけでなく、 Jenkins など他の CI ツールも使ってみて、より便利に活用していく方法を学んでみたいと思います。

また、セッション 1 などで紹介されていた Azure DevOps の他のツールとの組み合わせも試してみたいところ。

…当面無料で使えるもの限定となりそうですが(;´Д`)

【ASP.NET Core】Azure Pipelines でコンティニュアスにインテグレーションしてみる その2

はじめに

※ 2019/01/12 更新

  • Azure Pipeline → Azure Pipelines に修正しました。
  • Agent は VM など実行環境上にインストールされているもの、という内容に沿うよう修正しました。

さぁ年も明けましたのでね。張り切っていつも通りぼちぼちとやっていきたいと思います。

前回触れたジョブを実行する Agent から、気になったものをあれこれ調べてみることにします。

Agent について

まずは設定したジョブを実行する環境と Azure Pipelines を連携する Agent から。

Agent は実行環境であるマシン上にインストールされているものです。

以前 Unity プロジェクトをビルドしようとしたときに登録した、自前のマシンを使う Self-hosted agents と、 Microsoft が提供する VM 上で動く Microsoft-hosted agents があります。

ちなみにこれ、下記でいう The Microsoft Azure Virtual Machine Agent が指しているものと同じでしょうか。
(なんか微妙に違う気はします)

なおプロジェクトで使用できる Agent は下記で見ることができます。

https://dev.azure.com/ユーザー名/_settings/agentpools

Microsoft-hosted agents に何がインストールされているかは、上記サイトで Agent を選択 -> Details から見ることができます。

f:id:mslGt:20190105101809j:plain

動きとしては、 Pipeline 実行のたびに新規で VM (Agent がインストールされている) が用意され、 Pipeline の処理が終了するとともに破棄されるとのこと。

前回実行した内容を使って。。。といったことはできないので、設定などの準備を毎回実行するか、 Self-hosted agents として自前で用意するか、という判断となります。

なお上記ページでは Self-hosted agents と比べて Microsoft-hosted agents を強力にプッシュしています。

これは、特に初心者だと自前のホストでやろうとすると色々失敗するでしょ?まずは用意されたものを使ってみ?という意味なのか、 Self-hosted agents の方が Microsoft-hosted agents より Azure ?にかかる負荷が大きい、といった意味なのでしょうか。

といった、どっちでも良いことを考えたり考えなかったりしました。

OAuth token

これも Microsoft Docs で読んだ内容ですが、 Azure Pipelines と Agent との連携はこんな感じだそうです。

  1. Azure Pipelines から Agent を Agent pool に追加
  2. 1.の時 Agent では listener OAuth token をインストールし、ジョブの Queue を待ち受ける
  3. Pipeline 実行時、 Agent は Queue からジョブのリクエストを受ける
  4. 3 の時、 job-specific OAuth token をダウンロードする
  5. ジョブを実行する
  6. ジョブが完了したら、 job-specific OAuth token を破棄し、 Agent は 2 の状態に戻る

2.の時?に実行環境である VM を用意するステップがあるはずなのですが、調べただけだとよくわかりませんでした orz

ビルドの流れ

Azure Pipelines と Agent の流れはわかったようなわからないような状態となりましたが、そもそも GitHub に対して Pull Request や Push を送ったあとビルドが完了するまで、というのはどのような流れとなっているのでしょうか。

  1. Azure Pipelines から GitHub を監視?
  2. ユーザーが GitHub に Pull Request を送る
  3. Azure Pipelines が 2 を受け取り、ジョブの Queue を実行
  4. VM を用意する
  5. Queue はジョブのリクエストを発行する
  6. Agent がリクエストを受け取り、 job-specific OAuth token をインストール
  7. Agent でジョブを実行
  8. ジョブが完了したら Agent から Azure Pipelines にログとともに通知
  9. ジョブ完了後の処理として job-specific OAuth token の破棄などを行う
  10. Agent は次のジョブリクエストを待ち受ける
  11. 全ジョブが完了したら Agent を破棄する? または作り直す?

ところどころよく分かっていませんが、こういうことのようです。

結局 Azure Pipelines は何をしているのか

GitHub に Pull Request や Push が飛んでくるのを監視するのと(これは本当に Azure Pipelines がやっているのかわかりませんが)、ジョブの Queue を実行する、 Agent を管理する、といったことをしています。

つまり、ビルドやテストといった、実際に実行したい内容は(今回の場合) dotnet など他のソフトウェアが実行する、ということです。

ということは、あくまで Azure Pipelines として考えるべきことは、(ビルドやテストなどの)やりたいことをどう呼び出すか、ということとなります。

これは Azure Pipelines に限らず CI/CD ツールを使う上で大切。。。だったりしないのかなぁ。

topic ブランチのビルド設定を追加する

さて、そろそろ実際に動かしてみることにします。

master ブランチのみで開発し続けるのであれば、ローカルでの変更を Push していくだけでビルドされていくのですが、通常ブランチを切って( Microsoft Docs にならって topic ブランチと呼ぶことにします) 開発 -> Pull request -> merge という流れになるかと思います。

が、デフォルトの設定だとビルドの対象は master のみになっており、 topic ブランチの変更を Push してもビルドされません。

また、テストを追加したとしても自動で実行はされません。

ということでこれを変更してみます。
(テストの中身はこちらを参照)

azure-pipeline.yml

~省略~

trigger:
- master
- features/*

pool:
  vmImage: 'Ubuntu-16.04'

variables:
  buildConfiguration: 'Release'

steps:
- script: dotnet build --configuration $(buildConfiguration)
  displayName: 'dotnet build $(buildConfiguration)'
- script: dotnet test
    displayName: 'dotnet test'

pr:
- master
- features/*

最後の pr (Pull Request ビルド対象のブランチ指定)は何となく入れましたが、今回の場合特に意味はないです。

Shell script を実行する

Pull request を送ったあと、 master 側でテストが通ったら自動でマージするようにできないのかな?と思いました。

ということで調べたところ、 Azure Pipelines にはそのような機能がないようです。

これを見るとちょうどマージについて書かれているため、これを試してみることにします。

とりあえず Pull request がきたら、などの条件は置いておくことにします。

上記ページでは bat を使っていますが、実行環境を Ubuntu にしているので Shell script で試してみることにしました。

azure-pipeline.yml

~省略~
steps:
  - script: dotnet build --configuration $(buildConfiguration)
    displayName: 'dotnet build $(buildConfiguration)'
  - script: dotnet test
    displayName: 'dotnet test $(buildConfiguration)'
  - task: ShellScript@2
    inputs:
      scriptPath: git_merge.sh
    displayName: 'git merge'

~省略~
  • task につけられる名前は決まっているようで、今回の場合「ShellScript@2」以外の名前を付けるとエラーとなってしまいました。

ログを見ると、この名前によって Shell script であること、またそのバージョンを指定しているようでした ( Shell scirpt 2.1.3 )。

これで Shell script 自体は実行できます。

で、先ほどのページの Git コマンドをちょっと変更してこのようにしてみました。

git_merge.sh (ちゃんと動きません)

#!/bin/bash

echo SOURCE BRANCH IS %BUILD_SOURCEBRANCH%
if [%BUILD_SOURCEBRANCH% == refs/heads/master]; then 
   echo Building master branch so no merge is needed.
   exit
else
   echo merge
fi

set sourceBranch=origin/%BUILD_SOURCEBRANCH:refs/heads/=%
echo GIT CHECKOUT MASTER
git checkout master
echo GIT STATUS
git status
echo GIT MERGE
git merge %sourceBranch% -m "Merge to master"
echo GIT STATUS
git status
echo GIT PUSH
git push origin
echo GIT STATUS
git status

これを動かすと、下記のようなログが出て、マージはできませんでした。

2019-01-05T00:28:26.6264228Z ##[section]Starting: git merge
2019-01-05T00:28:26.6267544Z ==============================================================================
2019-01-05T00:28:26.6268233Z Task         : Shell Script
2019-01-05T00:28:26.6268617Z Description  : Run a shell script using bash
2019-01-05T00:28:26.6268730Z Version      : 2.1.3
2019-01-05T00:28:26.6268870Z Author       : Microsoft Corporation
2019-01-05T00:28:26.6269023Z Help         : [More Information](https://go.microsoft.com/fwlink/?LinkID=613738)
2019-01-05T00:28:26.6269214Z ==============================================================================
2019-01-05T00:28:26.7689506Z [command]/bin/bash /home/vsts/work/1/s/git_merge.sh
2019-01-05T00:28:26.7756106Z SOURCE BRANCH IS %BUILD_SOURCEBRANCH%
2019-01-05T00:28:26.7756449Z merge
2019-01-05T00:28:26.7756637Z GIT CHECKOUT MASTER
2019-01-05T00:28:26.7761605Z /home/vsts/work/1/s/git_merge.sh: line 4: [%BUILD_SOURCEBRANCH%: command not found
2019-01-05T00:28:26.7761996Z Previous HEAD position was 6105b3a Merge 55ac9c46fb679fef7f3f320765ded4d28e0b04c4 into feb1bcd92a0559c5242b4717b9c11a9a25246217
2019-01-05T00:28:26.7772812Z Switched to a new branch 'master'
2019-01-05T00:28:26.7773575Z Branch 'master' set up to track remote branch 'master' from 'origin'.
2019-01-05T00:28:26.7777038Z GIT STATUS
2019-01-05T00:28:26.7797446Z On branch master
2019-01-05T00:28:26.7798997Z Your branch is up to date with 'origin/master'.
2019-01-05T00:28:26.7799536Z 
2019-01-05T00:28:26.7799811Z nothing to commit, working tree clean
2019-01-05T00:28:26.7801985Z GIT MERGE
2019-01-05T00:28:26.8195510Z merge: %sourceBranch% - not something we can merge
2019-01-05T00:28:26.8198696Z GIT STATUS
2019-01-05T00:28:26.8218335Z On branch master
2019-01-05T00:28:26.8219258Z Your branch is up to date with 'origin/master'.
2019-01-05T00:28:26.8219608Z 
2019-01-05T00:28:26.8219959Z nothing to commit, working tree clean
2019-01-05T00:28:26.8221844Z GIT PUSH
2019-01-05T00:28:28.0116218Z fatal: could not read Username for 'https://github.com': No such device or address
2019-01-05T00:28:28.0137902Z GIT STATUS
2019-01-05T00:28:28.0165334Z On branch master
2019-01-05T00:28:28.0165914Z Your branch is up to date with 'origin/master'.
2019-01-05T00:28:28.0166050Z 
2019-01-05T00:28:28.0166150Z nothing to commit, working tree clean
2019-01-05T00:28:28.0312914Z ##[section]Finishing: git merge

やっぱりまるっとコピーするだけでは無理か。。。

すみませんでした、ということで、VMImage を vs2017-win2016 に変更して、 bat ファイルを実行してみることにしました。

azure-pipeline.yml

~省略~
pool:
  vmImage: 'vs2017-win2016'

steps:
  - script: dotnet build --configuration $(buildConfiguration)
    displayName: 'dotnet build $(buildConfiguration)'
  - script: dotnet test
    displayName: 'dotnet test $(buildConfiguration)'
  - task: ShellScript@2
    inputs:
      scriptPath: git_merge.sh
    displayName: 'git merge'

~省略~

が、今度はバージョンの違いからか dotnet build で失敗する。。。(´・ω・`)

ということで、これはいったん保留とします。

Shell script の方で、要はアカウントの指定ができていないということだと思うので、ここをもう少し調べてみることにします。

まぁそもそも、自動でマージする機能がどこまで必要か?というところは考える必要があるとは思いますが。。。

参照

2018 年の振り返りとか 2019 年とか

はじめに

流行ってる?っぽいので私も何となく。

【2018】家族について

今年のニュースといえば何といっても娘が生まれたことですね。

ただ、10カ月後の今となってみると、あれ?まだ一年たってないんだっけ?という気持ちになっていますw
すくすく健康に育ってくれている、ということでありがたい限りですね。

子供が二人になる、というのは思っていた以上に勝手が違ったりして、まだまだ慣れないところが多く、特に妻への負担が大きくなってしまっているのが気になっています。

来年は娘が大きくなって手がかからないようになるのをちょっと期待しつつ、やり方は考えていかないとな~、と思っています。

【2018】プログラミングについて

今年は特に C# .NET に寄った内容になっています。

mslgt.hatenablog.com

2017年末 - 2018年頭くらいまで Xamarin 、それから C# 、夏ごろから ASP.NET Core 、という感じでやっていたみたいです。

他の言語も当然興味はあるところなのですが、まずは一つの言語をしっかり学びたいな~、と思った結果なのですが、まったく理解できた気がしてないのはなぜでしょうか(白目)。

来年も C# .NET 中心に、もう少し中の処理などに踏み込んで学んでみたいと思っています。

あと、勉強も兼ねて英語版のブログも書いてみたいですね。 ASP.NET Core とか使って自分で作って~とかやってるといつになるかわからないので、まずはどこかのサービスを使おうかな、とは思っています。

【2018】お買い物について

Programming ASP.NET Core

まだ半分くらいしか読めていないのですが。。。

https://www.amazon.co.jp/Programming-ASP-NET-Developer-Reference-English-ebook/dp/B07CVM27L9/ref=sr_1_1_twi_kin_2?ie=UTF8&qid=1546217881&sr=8-1&keywords=Programming+ASP.NET+Core

ASP.NET Core の解説本で、特にプロジェクトを Empty で始めて、 Startup.cs や Program.cs の基本のところから説明してくれているのが非常に助かっています。

なお、今年買った本はまだまだたくさんあるのですが、読み終わったら別途書くことにします(白目)。

Surface Pro 6

色んなところへ連れまわすせいで、ラップトップ氏がお亡くなりに。。。

ということで Surface Pro 6 をお迎えしました。

www.instagram.com

良かったところ

何といってもバッテリーですね。

今までだと勉強会半日とかあると、予備のバッテリーを持っていっても最後まで持つか怪しい、といった状況があり、紙のノートに書き留める、といったことをしていました。

が、Surface だと長時間( 10 時間くらい?)使えるので、今のところバッテリーで困ったことはないですね。

あとタッチパネルが使えるので、タブレットとしても活躍してほしい、という目論見がありました。

こちら、性能自体は文句なし(タブレットモードも初めて使いましたが結構良いと思いました)なのですが、画面をバンバン叩く怪獣と爪でガリガリやってくる怪獣が家にいるため、あまり活躍できていません/(^o^)\

あと Windows Hallo による顔認識も非常に便利ですね。

まれに認識されない、されづらい場合もありますが、調子が良いと一瞬でログインできてしまいます。

悪かったところ

設定の問題だと思うのですが、スリープした瞬間に再度起動してしまい、かつ顔認証されてログインされてしまう、ということがあります。

マウスだったか電源だったかの設定で変更ができるようなのですが、該当する項目が見つからなかったりしてうまく設定できていません。

あと少し前に話題になっていましたが、 OneDrive とローカルのフォルダが混ざって管理しづらい。。。

共有するかを選択すること自体はできるのですが、スクリーンショットを自動で OneDrive に上げられなかったり、前までできていたことができなくなったりしています。。。

Microsoft 的には全部クラウドで共有しちまおうぜ、ということなのかもしれませんが、ローカルでのみ保管しておきたい(一時的な検証用プロジェクトとか)ものもありますし、そもそもクラウドのデータを全部持ってこられるとローカルのストレージが死んでしまうという。。。

この辺りもぼちぼち対策していきたいところです。

反省

ストレージがどれ位必要かをあまり考えずに買ってしまったため、 128GB でやりくりしているのですが、ツラい。。。
(Visual Studio 関連だけで 30GB 使ったり、 Core CLR ビルドしたら 10GB 持っていかれたり)

素直に黒モデル 256GB にしておくべきでした。。。(色に不満はないのですが、今回からの新カラーということで)

Alright, こぼれる

来年出る GRAPEVINE の新アルバム「ALL THE LIGHT」から先行配信された 2 曲。

感想は 2 曲とも一言で、「こうきたか!」でした。

ソウルやファンクといった影響が色濃く、 Alright は同じホーンを使っている Arma ともかなり違った雰囲気。

これじゃアルバムはどんななっちゃうの?と今からワクワクしています。

2019年について

2019 年は転職!!!。。。というのは目標にするものでもない気がするので、これはまぁぼちぼち考えていきます。

家族周りでは息子の入園とかまぁ色々あったり、1 、2 月に登壇予定があったりと年始早々バタバタする気がしてならないのですが、自分なりに頑張っていきます。

ということで、 2019 年もよろしくお願いいたします(..)_