CountdownEvent

System.Threading.CountdownEvent は同期プリミティブであり、特定の回数シグナル状態になった後、待機スレッドのブロックを解除します。 CountdownEvent は、通常は ManualResetEvent または ManualResetEventSlim を使用して、イベントをシグナル化する前に変数を手動でデクリメントする必要があるシナリオ用に設計されています。 たとえば、fork/join シナリオでは、単にシグナル数 5 の CountdownEvent を作成し、スレッド プールで 5 つの作業項目を開始し、完了時に各作業項目呼び出し Signal を使用できます。 Signal を呼び出すたびに、シグナル数が 1 だけデクリメントします。 メイン スレッドでは、シグナル数がゼロになるまで、Wait の呼び出しがブロックされます。

Note

レガシ .NET Framework の同期 API と対話する必要がないコードの場合は、fork と join の並列実行をさらに簡単に表すために System.Threading.Tasks.Task オブジェクトまたは Invoke メソッドの使用を検討してください。

CountdownEvent には以下の追加機能があります。

  • 取り消しトークンを使用して、待機操作を取り消すことができます。

  • インスタンスの作成後に、シグナル数をインクリメントできます。

  • Reset メソッドを呼び出すことで Wait が返された後、インスタンスを再利用できます。

  • インスタンスは、WaitAll などの他の .NET の同期 API との統合のために WaitHandle を公開します。

基本的な使用方法

CountdownEventThreadPool 作業項目を使用する方法を次の例に示します。

IEnumerable<Data> source = GetData();
using (CountdownEvent e = new CountdownEvent(1))
{
    // fork work:
    foreach (Data element in source)
    {
        // Dynamically increment signal count.
        e.AddCount();
        ThreadPool.QueueUserWorkItem(delegate(object state)
         {
             try
             {
                 ProcessData(state);
             }
             finally
             {
                 e.Signal();
             }
         },
         element);
    }
    e.Signal();

    // The first element could be run on this thread.

    // Join with work.
    e.Wait();
}
// .,.
Dim source As IEnumerable(Of Data) = GetData()
Dim e = New CountdownEvent(1)

' Fork work:
For Each element As Data In source
    ' Dynamically increment signal count.
    e.AddCount()

    ThreadPool.QueueUserWorkItem(Sub(state)
                                     Try
                                         ProcessData(state)
                                     Finally
                                         e.Signal()
                                     End Try
                                 End Sub,
                                  element)
Next
' Decrement the signal count by the one we added
' in the constructor.
e.Signal()

' The first element could also be run on this thread.
' ProcessData(New Data(0))

' Join with work:
e.Wait()

CountdownEvent と Cancellation

次の例は、取り消しトークンを使用して、CountdownEvent での待機操作を取り消す方法を示しています。 基本的なパターンでは、.NET Framework 4 で導入された統合取り消しのモデルに従います。 詳細については、「マネージド スレッドのキャンセル」を参照してください。

class CancelableCountdownEvent
{
    class Data
    {
        public int Num { get; set; }
        public Data(int i) { Num = i; }
        public Data() { }
    }

    class DataWithToken
    {
        public CancellationToken Token { get; set; }
        public Data Data { get; private set; }
        public DataWithToken(Data data, CancellationToken ct)
        {
            this.Data = data;
            this.Token = ct;
        }
    }
    static IEnumerable<Data> GetData()
    {
        return new List<Data>() { new Data(1), new Data(2), new Data(3), new Data(4), new Data(5) };
    }
    static void ProcessData(object obj)
    {
        DataWithToken dataWithToken = (DataWithToken)obj;
        if (dataWithToken.Token.IsCancellationRequested)
        {
            Console.WriteLine("Canceled before starting {0}", dataWithToken.Data.Num);
            return;
        }

        for (int i = 0; i < 10000; i++)
        {
            if (dataWithToken.Token.IsCancellationRequested)
            {
                Console.WriteLine("Cancelling while executing {0}", dataWithToken.Data.Num);
                return;
            }
            // Increase this value to slow down the program.
            Thread.SpinWait(100000);
        }
        Console.WriteLine("Processed {0}", dataWithToken.Data.Num);
    }

    static void Main(string[] args)
    {
        EventWithCancel();

        Console.WriteLine("Press enter to exit.");
        Console.ReadLine();
    }

    static void EventWithCancel()
    {
        IEnumerable<Data> source = GetData();
        CancellationTokenSource cts = new CancellationTokenSource();

        //Enable cancellation request from a simple UI thread.
        Task.Factory.StartNew(() =>
             {
                 if (Console.ReadKey().KeyChar == 'c')
                     cts.Cancel();
             });

        // Event must have a count of at least 1
        CountdownEvent e = new CountdownEvent(1);

        // fork work:
        foreach (Data element in source)
        {
            DataWithToken item = new DataWithToken(element, cts.Token);
            // Dynamically increment signal count.
            e.AddCount();
            ThreadPool.QueueUserWorkItem(delegate(object state)
             {
                 ProcessData(state);
                 if (!cts.Token.IsCancellationRequested)
                     e.Signal();
             },
             item);
        }
        // Decrement the signal count by the one we added
        // in the constructor.
        e.Signal();

        // The first element could be run on this thread.

        // Join with work or catch cancellation.
        try
        {
            e.Wait(cts.Token);
        }
        catch (OperationCanceledException oce)
        {
            if (oce.CancellationToken == cts.Token)
            {
                Console.WriteLine("User canceled.");
            }
            else
            {
                throw; //We don't know who canceled us!
            }
        }
        finally {
            e.Dispose();
            cts.Dispose();
        }
        //...
    } //end method
} //end class
Option Strict On
Option Explicit On

Imports System.Collections
Imports System.Collections.Generic
Imports System.Linq
Imports System.Threading
Imports System.Threading.Tasks

Module CancelEventWait

    Class Data
        Public Num As Integer
        Public Sub New(ByVal i As Integer)
            Num = i
        End Sub
        Public Sub New()

        End Sub
    End Class

    Class DataWithToken
        Public Token As CancellationToken
        Public _data As Data
        Public Sub New(ByVal d As Data, ByVal ct As CancellationToken)
            Me._data = d
            Me.Token = ct
        End Sub
    End Class

    Class Program
        Shared Function GetData() As IEnumerable(Of Data)
            Dim nums = New List(Of Data)
            For i As Integer = 1 To 5
                nums.Add(New Data(i))
            Next
            Return nums
        End Function

        Shared Sub ProcessData(ByVal obj As Object)
            Dim dataItem As DataWithToken = CType(obj, DataWithToken)
            If dataItem.Token.IsCancellationRequested = True Then
                Console.WriteLine("Canceled before starting {0}", dataItem._data.Num)
                Exit Sub
            End If

            ' Increase this value to slow down the program.
            For i As Integer = 0 To 10000

                If dataItem.Token.IsCancellationRequested = True Then
                    Console.WriteLine("Cancelling while executing {0}", dataItem._data.Num)
                    Exit Sub
                End If
                Thread.SpinWait(100000)
            Next
            Console.WriteLine("Processed {0}", dataItem._data.Num)


        End Sub

        Shared Sub Main()
            DoEventWithCancel()
            Console.WriteLine("Press the enter key to exit.")
            Console.ReadLine()
        End Sub

        Shared Sub DoEventWithCancel()
            Dim source As IEnumerable(Of Data) = GetData()
            Dim cts As CancellationTokenSource = New CancellationTokenSource()

            ' Enable cancellation request from a simple UI thread.
            Task.Factory.StartNew(Sub()
                                      If Console.ReadKey().KeyChar = "c"c Then
                                          cts.Cancel()
                                      End If
                                  End Sub)

            ' Must have a count of at least 1 or else it is signaled.
            Dim e As CountdownEvent = New CountdownEvent(1)

            For Each element As Data In source
                Dim item As DataWithToken = New DataWithToken(element, cts.Token)

                ' Dynamically increment signal count.
                e.AddCount()

                ThreadPool.QueueUserWorkItem(Sub(state)
                                                 ProcessData(state)
                                                 If cts.Token.IsCancellationRequested = False Then
                                                     e.Signal()
                                                 End If
                                             End Sub,
                                            item)
            Next
            ' Decrement the signal count by the one we added
            ' in the constructor.
            e.Signal()
            ' The first element could be run on this thread.
            ' ProcessData(source(0))

            ' Join with work or catch cancellation exception
            Try
                e.Wait(cts.Token)
            Catch ex As OperationCanceledException
                If ex.CancellationToken = cts.Token Then
                    Console.WriteLine("User canceled.")
                Else : Throw ' we don't know who canceled us.

                End If
            Finally
                e.Dispose()
                cts.Dispose()
            End Try
        End Sub
    End Class
End Module

待機操作では、それをシグナル化するスレッドが取り消されないことに注意してください。 通常、取り消しは論理操作に適用され、待機が同期中のすべての作業項目だけでなく、イベントでの待機を含めることができます。 この例では、各作業項目には同じ取り消しトークンのコピーが渡されるため、取り消し要求に応答することができます。

参照