ホームに戻る
出典 :
関連 :
目次 :
シグナル処理
シグナル処理の概念に関してはタスク(スレッド)間通信を参照のこと。
あるタスクが別タスクからのシグナル(イベントフラグ)を待機し、シグナルを契機として処理を再開するような処理の場合、シグナル(イベント)の待機には「イベント待機ハンドラ」を用いる。
(「イベントハンドラ」とは異なる。イベント待機ハンドラはセマフォやミューテックスと同様、「同期オブジェクト(シンクロナイザー)」に含まれる。)
実装例
「コンテンツのダウンロードをいったん中断し、『OK』ボタン押下後に再開する」処理を行う。
使用するイベント待機ハンドラによって、コメント箇所の実装が変化する。
// ダウンロードタスク
public async Task<string> DownloadAsync()
{
using (var client = new HttpClient())
{
var response = await client.GetAsync(@"http://hoge.example.com/contents.dat");
using (var stream = new StreamReader(await response.Content.ReadAsStreamAsync()))
{
// コンテンツの1行目のみを読み込む
var first = stream.ReadLine();
}
// ----------------------------------------
// この部分の実装が変化
// (シグナル待ち状態に移行)
// ----------------------------------------
// 処理を再開
return await response.Content.ReadAsStringAsync();
}
}
// ダウンロード継続承認
// (「OK」ボタン押下時の処理)
public void DownloadAcceppted(object sender, EventArgs args)
{
// ----------------------------------------
// この部分の実装が変化
// (シグナル送信 ⇒ シグナル状態に移行)
// ----------------------------------------
}
System.Threading.CountdownEvent による実装
CountdownEvent は、オブジェクト生成時に指定した回数だけシグナルを受けると、待ち状態を解除するハンドラである。
(例 : CountdownEvent(3) であれば、CountdownEvent.Signal() が3回コールされた時点でシグナル状態となる。)
// イベント待機ハンドラ CountdownEvent の初期化
private readonly CountdownEvent condition = new CountdownEvent(1);
// ダウンロードタスク
public async Task<string> DownloadAsync()
{
using (var client = new HttpClient())
{
var response = await client.GetAsync(@"http://hoge.example.com/contents.dat");
using (var stream = new StreamReader(await response.Content.ReadAsStreamAsync()))
{
// コンテンツの1行目のみを読み込む
var first = stream.ReadLine();
}
// シグナル待ち状態に移行
condition.Wait();
// シグナル状態を解除
condition.Reset();
// 処理を再開
return await response.Content.ReadAsStringAsync();
}
}
// ダウンロード継続承認
// (「OK」ボタン押下時の処理)
public void DownloadAcceppted(object sender, EventArgs args)
{
// シグナル送信 ⇒ シグナル状態に移行
condition.Signal();
}
ここでは、シグナル状態に移行(待ち状態の解除)後、待機ハンドラを再利用することを考慮してシグナル状態を解除( Reset() )している。
System.Threading.AutoResetEvent による実装
// イベント待機ハンドラ AutoResetEvent の初期化
private readonly AutoResetEvent condition = new AutoResetEvent();
// ダウンロードタスク
public async Task<string> DownloadAsync()
{
using (var client = new HttpClient())
{
var response = await client.GetAsync(@"http://hoge.example.com/contents.dat");
using (var stream = new StreamReader(await response.Content.ReadAsStreamAsync()))
{
// コンテンツの1行目のみを読み込む
var first = stream.ReadLine();
}
// シグナル待ち状態に移行
condition.WaitOne();
// タスクの待ちが解除されると自動的にシグナル解除
// 処理を再開
return await response.Content.ReadAsStringAsync();
}
}
// ダウンロード継続承認
// (「OK」ボタン押下時の処理)
public void DownloadAcceppted(object sender, EventArgs args)
{
// シグナル送信 ⇒ シグナル状態に移行
condition.Set();
}
AutoResetEvent はシグナル状態に移行すると、シグナル待ちのタスクが再開された時点で自動的にシグナル状態が解除される。
(明示的に Reset() を呼ぶ必要が無い。)
System.Threading.ManualResetEvent による実装
CountdownEvent と同様、手動でシグナル状態を解除する必要がある。以下のメソッドを使用する。
- シグナル待機 : ManualResetEvent.WaitOne()
- シグナルセット : ManualResetEvent.Set()
- シグナルリセット : ManualResetEvent.Reset()
シグナル状態が自動で解除されないため、複数のタスクを待たせるのに使用できる。
どのように使い分けるか
CountdownEvent は AutoResetEvent や ManualResetEvent と比較してオーバーヘッドが小さい(1/20程度)。
このため.NET 4.0以降の環境では、CountdownEvent の使用が推奨される。
また、同じく.NET 4.0から追加された ManualResetEventSlim は ManualResetEvent よりもオーバーヘッドが小さく、以下の機能が追加されている。
- ManualResetEventSlim.Wait() は即座にスレッドを中断させるのではなく、コンストラクタで指定された期間スピンウェイトを行う。
(スピンウェイト期間中にシグナル状態となれば高い応答性が得られるが、スピンウェイト中はCPU資源の消費が増える。)
- 待ち状態のキャンセルが可能である。
詳細は出典元を参照。
クラス |
ManualResetEvent |
AutoResetEvent |
CountdownEvent |
ManualResetEventSlim |
待機するシグナルの数 |
1 |
1 |
可変 |
1 |
シグナル状態の解除 |
手動 |
自動 |
手動 |
手動 |
備考 |
旧式 |
旧式 |
オーバーヘッドが小さい |
オーバーヘッドが小さい スピンウェイトあり 待ち状態キャンセル可 |
Producer-Consumer パターン
データを生成する処理(Producer)とデータを使用する処理(Consumer)を異なるスレッドで非同期に実行するモデルのこと。イベント待機ハンドラの使用を前提としている。
Producerはデータをエンキューする際にシグナルをセットし、Consumerはシグナルを契機にタスクを起床してキューからデータを回収することで、効率的にコンピュータ資源を利用できる。
- イベントトリガをバッファ(イベントキュー)に格納する : Producer
- イベントトリガをイベントキューから取り出して実行する : Consumer
実装例
// Producer-Consumer テンプレートクラス
class BlockingQueue<T>
{
private readonly Queue<T> _queue = new Queue<T>(); //< データキュー
private readonly ManualResetEventSlim _resetevent = new ManualResetEventSlim(); //< イベント待ちハンドラ
private readonly object _lock = new object(); //< ロックオブジェクト
// デストラクタ
~BlockingQueue()
{
// イベント待ちハンドラを破棄
_resetevent.Dispose();
}
// データを生成 (Producer)
public void Put(T data)
{
lock (_lock)
{
// データをエンキュー
_queue.Enqueue(data);
// シグナルをセット
_resetevent.Set();
}
}
// データを取得 (Consumer)
public T Take(CancellationToken token)
{
// 無限ループ
while (true)
{
// キャンセル要求があれば例外を投げる
token.ThrowIfCancellationRequested();
lock (_lock)
{
// キューにデータが入っていればデキューして返す
if (_queue.Any())
{
var val = _queue.Dequeue();
return val;
}
// シグナルをクリア
_resetevent.Reset();
}
// シグナル待ちに移行
_resetevent.Wait(Timeout.Infinite, token);
}
}
}
Take() メソッドでは、キューにデータが格納されていなければシグナル待ちに移行し、タスクを休眠させる。
シグナル状態となればタスクは起床し、キューにデータがあればデキューして返す。
連続で Take() をコールすることで、キューが空になるまでデータを回収できる。
ここでは Queue<T> を使用しているため、lock() による排他制御が必要となるが、
CoucurrentQueue<T> に置き換えると複数スレッドからの同時アクセスでも破綻せず、排他の必要が無い。