ManualResetEvent / AutoResetEvent で処理を待たせる

もくじ
https://tera1707.com/entry/2022/02/06/144447

やりたいこと

あるスレッドで行っている処理を、別のスレッドの処理が終わるまで待ってほしい、みたいなケースがあった。なにかよいやり方ないか、調べてみる。

やり方

ManualResetEventもしくはAutoResetEventを使う。

例えばManualResetEventを使う場合、

  • あるスレッドでmanualEvent.WaitOne();して、別スレッドの処理が終わるのを待つようにして、

  • 別スレッド側で処理が終わったらmanualEvent.Set();をすると、

  • 元のスレッドの方で、続きの処理が行われる

という感じ。

manualEvent.Set();をすると、manualEventが「シグナル状態」になり、そうなると、manualEvent.WaitOne();で待っていた(ブロックされていた)処理の続きが走る。

ManualResetEvent と AutoResetEvent の違い

AutoResetEvent

Set()でシグナル状態になった後、WaitOne()を通過したら、自動的にシグナル状態が解除される。

→もう一度WaitOne()に来たときは、次にSet()するまでは再び待ちに入る。

ManualResetEvent

Set()でシグナル状態になった後、WaitOne()を通過しても、シグナル状態のままになる。

→もう一度WaitOne()に来たときも、シグナル状態のままなので、待つことなく通過する。 ⇒もう一度待ちに入ってほしい場合は、manualEvent.Reset()をする必要がある。

サンプルコード

using Microsoft.UI.Xaml;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;

namespace App13
{
    public sealed partial class MainWindow : Window
    {
        ManualResetEvent manualEvent = new(false);
        AutoResetEvent autoEvent = new(false);

        public MainWindow()
        {
            this.InitializeComponent();
        }

        // ManualResetEventで待つタスクスタート
        private void Button_Click(object sender, RoutedEventArgs e)
        {
            Task.Run(()=>
            {
                Debug.WriteLine("ManualResetEventで待ちます");
                manualEvent.WaitOne();
                Debug.WriteLine("ManualResetEventで待ちます 1個終わり");
                manualEvent.WaitOne();
                Debug.WriteLine("ManualResetEventで待ちます 2個終わり");
                manualEvent.WaitOne();
                Debug.WriteLine("ManualResetEventで待ちます 3個終わり。終わり。");
            });
        }

        // ManualResetEvent のシグナルON
        private void Button_Click_1(object sender, RoutedEventArgs e)
        {
            Debug.WriteLine("ManualResetEvent シグナルにします");
            manualEvent.Set();
        }

        private void Button_Click_4(object sender, RoutedEventArgs e)
        {
            manualEvent.Reset();
        }

        // AutoResetEventで待つタスクスタート
        private void Button_Click_2(object sender, RoutedEventArgs e)
        {
            Task.Run(() =>
            {
                Debug.WriteLine("AutoResetEventで待ちます");
                autoEvent.WaitOne();
                Debug.WriteLine("AutoResetEventで待ちますで待ちます 1個終わり");
                autoEvent.WaitOne();
                Debug.WriteLine("AutoResetEventで待ちますで待ちます 2個終わり");
                autoEvent.WaitOne();
                Debug.WriteLine("AutoResetEventで待ちますで待ちます 3個終わり。終わり。");
            });
        }

        // AutoResetEvent のシグナルON
        private void Button_Click_3(object sender, RoutedEventArgs e)
        {
            Debug.WriteLine("AutoResetEvent シグナルにします");
            autoEvent.Set();
        }

        private void Button_Click_5(object sender, RoutedEventArgs e)
        {
            manualEvent.Dispose();
            autoEvent.Dispose();
        }

        private void Window_Closed(object sender, WindowEventArgs args)
        {
            manualEvent.Dispose();
            autoEvent.Dispose();
        }
    }
}
<Window
    x:Class="App13.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:App13"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d" Closed="Window_Closed">

    <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center">
        <Button Content="ManualResetEventで待つタスクスタート" Click="Button_Click"/>
        <Button Content="ManualResetEvent のシグナルON" Click="Button_Click_1" />
        <Button Content="ManualResetEvent のシグナルOFF" Click="Button_Click_4" />
        <Button Content="AutoResetEventで待つタスクスタート" Click="Button_Click_2"/>
        <Button Content="AutoResetEvent のシグナルON" Click="Button_Click_3" />
        <Button Content="両方Dispose" Click="Button_Click_5" />
    </StackPanel>
</Window>

AutoとManualの使い分け

下記に説明アリ。

https://stackoverflow.com/questions/8215380/why-no-autoreseteventslim-in-bcl

AutoResetは、1つのスレッドが、自分が握ってるリソースを他からいじられないようにするために使う。

‘ManualReset‘は、複数のスレッドが、1つのスレッドの終了を待つために使う。 (待ちが終わったら、一斉にその1つのスレッドの処理結果を使って複数スレッドの処理が始まる感じ)

ということか。

追記

ManualResetEventクラスには、軽量版のManualResetEventSlimクラスというのがある様子。

下記のMSdocsによると、

https://docs.microsoft.com/ja-jp/dotnet/api/system.threading.manualreseteventslim?view=net-6.0#remarks

Wait()が数十ミリ秒くらいで、同一プロセス内で使うなら、Slimの方が負荷が軽いとある。
(それ以上になるなら、Slimのメリットは薄い、ともある)

使ってみた感想

1つのスレッドが、別の1つのスレッドが行っている何かを待ち合わせるためにこれを使う、程度のシンプルな使い方ならアリだと感じたが、いろんなところでSetするイベントを、いろんなところでWaitしたりIsSetを見たりしだすと、フラグ乱立系の危険なコードになるな、と感じた。

活用の仕方をあまり理解できてないだけだとは思うが、 きちんとした設計をしなくても「待ち」状態を作れてしまって、そこがバグになるととても見つけづらい、解決しにくいバグになりそうな予感がして、個人的に〇〇ResetEvent、あまり好きになれない。

Task.Run(() => { while (!flag) { } }).Wait();

と何が違うんだろうか?

参考

ManualResetEvent クラス

https://docs.microsoft.com/fr-ch/dotnet/api/system.threading.manualresetevent?view=net-6.0

AutoResetEventクラス

https://docs.microsoft.com/en-gb/dotnet/api/system.threading.autoresetevent?view=net-6.0 https://docs.microsoft.com/en-gb/dotnet/api/system.threading.autoresetevent?view=net-6.0