マシン状態を Interlocked 系の仕組みで管理する

読者の皆さんは、画像処理中に画像処理のリクエストが来たらどういうアプローチをとりますか?下記の3種類が考えられます。

(A) 現在の画像処理が終わるまでリクエストを保留する。
(B) 現在の画像処理を中断してリクエストを受ける。
(C) 画像処理をしていたらリクエストは無視する。

アプリケーションによりけりですが、カメラを使った部品の外観検査装置の場合、

(A)は、システムが滞留してしまって最後には動かなくなってしまう可能性が高いです。
(B)は、その部品の途中でやめたので、その部品は不良品という扱いをすることになります。
(C)も、その部品の検査をしなかったので、その部品は不良品という扱いをすることになります。

皆さんのお仕事はどうですか?私の場合は (C) にします。外観検査装置は検査タクトがあらかじめ決められている場合が多く、途中で画像処理を中断して脱出するというのは意外と面倒なのです。

だとしたら、そもそも画像処理をスタートしないというのがラクですしバグも出にくいです。

(C)を実現するのに、なんとなくフラグを使うというやり方はお勧めできません。カメラを使った画像処理ソフトウェアは多くの場合、画像のキャプチャがコールバック関数、または、スレッド関数になっている場合が多く、そこから個別の画像処理のコードをコールする場合、変数の原子性(アトミック性)を厳密に管理しなければなりません。

具体的に語ると C# では、変数の内容確認、書き換え、条件評価には、Interlocked 系のメソッド、Volatile 系のメソッドを利用しなければなりません。

ということで、(3) のやり方を実現する方法を、2種類のサンプルでお知らせします。

ひとつは「アイドル中」「リクエストされた」「画像処理中」という3つのステートを管理する方法です。

もうひとつは「アイドル中」「画像処理中」という2つのステートと、画像処理リクエスト用の一つの変数を利用する方法です。

勝手に、前者を「3ステート型」と命名し、後者を「2ステート + リクエスト変数型」と命名します。

3ステート型

WindowsFormsのプログラムでサンプルを示します。ボタンを一つ配置して button1_Click();に動作を定義します。button1_Click();では現在のマシンステートを確認して、画像処理中だったら画像処理をリクエストしないというコードになっています。

ここで WorkerLoop() はアプリケーション起動中はずっと動いている無限ループ、ThreadExecProcessing() は無限ループ内で起動される画像処理スレッドのイメージでお読みください。

namespace aaa
{
	public static class Sts
	{
		public const int Idle = 0;
		public const int Requested = 1;
		public const int Proc = 2;
	}
}

using System;
using System.Diagnostics;
using System.Threading;
using System.Windows.Forms;

namespace aaa
{

	public partial class Form1 : Form
	{

		private int MachineSts = Sts.Idle;

		public Form1()
		{
			InitializeComponent();
		}

		private void Form1_Load( object sender, EventArgs e )
		{
			this.Text = "このプログラムは「3ステート型」のサンプルです.";
		}

		private void button1_Click( object sender, EventArgs e )
		{
			
			// 現在のマシンステートの状況を確認しつつリクエストする.
			// 第3引数がマッチしたら、第2引数に変更する. 戻り値は変更前の値である.
			// if の評価寸前に第3引数が Idle      だったら突入する.
			// if の評価寸前に第3引数が Requested だったら突入しない.
			// if の評価寸前に第3引数が Proc      だったら突入しない.
			if ( Interlocked.CompareExchange( ref MachineSts, Sts.Requested, Sts.Idle ) == Sts.Idle )
			{
				// この時点でマシンステートは Requested になっている.

				Debug.WriteLine( "画像処理をリクエストしました." );
			}
			else
			{
				Debug.WriteLine( "画像処理中なのでリクエストしませんでした." );
			}

		}

		private void WorkerLoop( object prms )
		{

			for (;;)
			{
				// 現在のステートがリクエストだったら、ステートをプロセッシングにする.
				// 第3引数がマッチしたら、第2引数に変更する. 戻り値は変更前の値である.
				// if の評価寸前に第3引数が Requested だったら突入する.
				// if の評価寸前に第3引数が Idle      だったら突入しない.
				// if の評価寸前に第3引数が Proc      だったら突入しない.
				if ( Interlocked.CompareExchange( ref MachineSts, Sts.Proc, Sts.Requested ) == Sts.Requested )
				{
					// この時点でマシンステートが Proc になっている.

					// 画像処理スレッドを起動する.
					ThreadExecProcessing();
				}
			}

		}

		private void ThreadExecProcessing()
		{
			try
			{
				// 具体的な画像処理をしているつもり.
				Debug.WriteLine( "画像処理中." );
			}
			catch ( Exception excp )
			{
				// 何かエラーログを残すならここ.
				Debug.WriteLine( $"Error: {excp.Message}" );
			}
			finally
			{
				// かならずマシンステートをアイドルに戻す.
				Interlocked.Exchange( ref MachineSts, Sts.Idle );
			}
		}
	}
}

2ステート + リクエスト変数型

こちらも WorkerLoop() ThreadExecProcessing() は疑似コードのイメージでお読みください。

button1_Click();では現在のマシンステートを確認せずにむやみに画像処理リクエストをします。しかし WorkerLoop() の内部で、自分が画像処理中だったら、そのリクエストを無視するというコードになっています。

namespace bbb
{
	public static class Sts
	{
		public const int Idle = 0;
		public const int Proc = 1;
	}
}

using System;
using System.Diagnostics;
using System.Threading;
using System.Windows.Forms;

namespace bbb
{
	public partial class Form1 : Form
	{

		private int MachineSts = Sts.Idle;
		private int FlagReqProc = 0;

		public Form1()
		{
			InitializeComponent();
		}

		private void Form1_Load( object sender, EventArgs e )
		{
			this.Text = "このプログラムは「2ステート+リクエスト変数」型のサンプルです.";
		}

		private void button1_Click( object sender, EventArgs e )
		{
			// 現在のマシンステートの状況を確認せずむやみにリクエストする.
			Interlocked.Exchange( ref FlagReqProc, 1 );
		}

		private void WorkerLoop( object prms )
		{
			for (;;)
			{
				// リクエスト変数が 1 だったら実行しつつ、変数を 0 に戻す.
				if ( Interlocked.Exchange( ref FlagReqProc, 0 ) == 1 )
				{
					// この時点でリクエスト変数は 0 になっている.
					// トリガは受けとったほうでゼロリセットする思想である.

					// 現在のマシンステートの状況を確認しつつリクエストする.
					// 第3引数がマッチしたら、第2引数に変更する. 戻り値は変更前の値である.
					// if の評価寸前に第3引数が Idle だったら突入する.
					// if の評価寸前に第3引数が Proc だったら突入しない.
					if ( Interlocked.CompareExchange( ref MachineSts, Sts.Proc, Sts.Idle ) == Sts.Idle )
					{
						// この時点でマシンステートは Proc になっている.

						ThreadExecProcessing();
					}
					else
					{
						Debug.WriteLine( "画像処理中なのでリクエストを無視しました." );
					}

				}
			}
		}

		private void ThreadExecProcessing()
		{
			try
			{
				// 具体的な画像処理をしているつもり.
				Debug.WriteLine( "画像処理中." );
			}
			catch ( Exception excp )
			{
				// 何かエラーログを残すならここ.
				Debug.WriteLine( $"Error: {excp.Message}" );
			}
			finally
			{
				// かならずマシンステートをアイドルに戻す.
				Interlocked.Exchange( ref MachineSts, Sts.Idle );
			}
		}
	}
}

どちらを選ぶかは好みですけれど、私は後者の方が好きです。コール側のコードがシンプルになるからです。

多くの場合は WorkerLoop() 側は煩雑なコードになりやすいです。もとから煩雑なところに多少の煩雑さが加わっても、たかがしれているという考え方です。

どちらも起動した画像処理スレッドの中で、何があっても、必ずもとのステートに戻すことを忘れないでください。具体的には

Interlocked.Exchange( ref MachineSts, Sts.Idle ); というコードのことです。

Interlocked.Exchange() と Interlocked.CompareExchange() を if の条件で使うときは若干のクセがあるので、コード内のコメントをよく読んでご理解ください。

Volatile.Read() と Volatile.Write() は特に難しいものではありません。