はじめに
私は勉強がてらGithubにてHandbrakeBatchRunnerという、オープンソースのHandbrakeを使った動画をバッチ変換を行うソフトを作っています。
その開発中に覚えたTips集などを、Qiitaで今後記事にしていきたいと思っています。
今回のはその第一弾となっています。
前提環境
VisualStudio 2019
.NET Core 3.1
WPF(C#)
今回のお話
動画変換をする際に、他のウインドウで既に変換中の場合は完了を待ちたいなというのを思い実装していました。
「んん~~~~動画を2重に変換したところで遅いし待ち合わせしないとなあ」
「ほかプロセス待つんだったら、やっぱしMutexだよね!」
~Mutexを使うプログラミング中~
「ぐああああ!DisposeしてもReleasMutexしないと残ってめんどくせえええ!」
「しゃーない。ラッパー自分で作って自動開放しよう。」
~ラッパー作り作り~
「よしよしusingで開放されるようにしたし。取得できるまで長いからawaitしちゃお♪」
実行
System.ApplicationException : Object synchronization method was called from an unsynchronized block of code
「ホーリーシット !!!」
解説
はい。
MutexってWIN32APIのラッパーなので色々特殊な上に古いんですよね。
開放がReleaseMutexを明示的に呼ばないとされなかったり、なかなか曲者です。
開放用にラッパークラス作って、今回は変換中を長く待ったりするしawaitしようかなと思った結果が前項のApplicationExceptionになります。
※そもそもApplicationExceptionってユーザの例外用のクラスなんですが、これみたいにフレームワーク内からでちゃったりで現在は非推奨になってますが、こんなところで出くわすとは…
Mutexは取得したスレッドからでないとReleaseMutexできない仕様のため、awaitを使うとスレッドが別になり、今回の問題にぶちあたりました。時間がかかる処理の割にawaitとは相性が悪いです。
プロセスまたいで待合せをやろうと思うことも少ないためか、海外フォーラムのCodeProjectやStackOverFlowでも解決策が出てなかったんで、今回記事にしてみようかなと思い書いています。
「まじか。こんなことやろうとする変態は自分ぐらいなんや…」
「おっしゃ自分で解決しよ!」ウキウキ
※予め種明かししておくと、作りが無理があって実用的ではないかも…
※他こうしたほうがいいじゃない?などあればどしどし頂きたいです!
ソース
それではソースで見ていきましょう。
Mutexの普通の使い方
普通に書くとこうなります。ReleaseMutexを別でやらないといけないし、本当はTry-finallyでやらないと実行されないケースもあってめんどくさいです。
private void Button_Click(object sender, RoutedEventArgs e)
{
using (var mutex = new Mutex(false,"MutexTest"))
{
if (mutex.WaitOne())
{
// 1多重で実行したい処理
}
mutex.ReleaseMutex();
}
}
ラッパー作りましょう
なおMutexは継承禁止クラスのため委譲パターンで作るよりないです。
public class MutexWrapper : IDisposable
{
/// <summary>
/// 実インスタンス
/// </summary>
private Mutex instance;
/// <summary>
/// コンストラクタ
/// </summary>
/// <param name="initiallyOwned"></param>
/// <param name="name"></param>
public MutexWrapper(bool initiallyOwned, string name)
{
instance = new Mutex(initiallyOwned, name);
}
/// <summary>
/// 取得
/// </summary>
/// <returns></returns>
public virtual bool WaitOne()
{
return instance.WaitOne();
}
#region "Disposeサポート"
private bool disposedValue = false;
protected virtual void Dispose(bool disposing)
{
if (!disposedValue)
{
if (instance != null)
{
instance.ReleaseMutex();
instance.Dispose();
instance = null;
}
disposedValue = true;
}
}
~MutexWrapper()
{
Dispose(false);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
#endregion
}
使う方はこう。開放がusingに任せられるようになったのでスッキリしました。
ここまでは他の記事、ブログなどでよくありますね。
using (var mutex = new MutexWrapper(false, "MutexTest"))
{
if (mutex.WaitOne())
{
// 1多重で実行したい処理
}
}
じゃあawaitは?
ラッパーにこんなのを足します。
/// <summary>
/// 非同期取得
/// </summary>
/// <returns></returns>
public virtual Task<bool> WaitOneAsync()
{
return Task.Run(() =>
{
return instance.WaitOne();
});
}
使う方はこう。UIをブロックしなくなって更に良い!
using (var mutex = new MutexWrapper(false, "MutexTest"))
{
if (await mutex.WaitOneAsync())
{
// 1多重で実行したい処理
}
}
と思いきや、これだとawaitでWaitOneを実行するので、Mutexの取得時と開放時の実行スレッドが異なることになりMutex的には駄目です。
黒魔術的解決
この問題を解決どうするのか。黒魔術で対応します。
メンバ変数に以下の変数を足して
/// <summary>
/// 取得完了イベント
/// </summary>
private CountdownEvent waitEndEvent = new CountdownEvent(1);
/// <summary>
/// 開放イベント
/// </summary>
private CountdownEvent releaseEvent = new CountdownEvent(1);
/// <summary>
/// 取得結果
/// </summary>
private bool waitResult;
非同期取得メソッドはこう変えて
/// <summary>
/// 非同期取得
/// </summary>
/// <returns></returns>
public virtual Task<bool> WaitOneAsync()
{
Task.Factory.StartNew(() =>
{
MutexControlTask();
}, TaskCreationOptions.LongRunning);
return Task.Factory.StartNew(() =>
{
// 取得完了まで待受
waitEndEvent.Wait();
waitEndEvent.Reset();
return waitResult;
},TaskCreationOptions.LongRunning);
}
Mutexの取得から開放まで1スレッドで処理します。
/// <summary>
/// Mutexの取得、開放タスク
/// </summary>
private void MutexControlTask()
{
// Mutex取得開始
waitResult = instance.WaitOne();
waitEndEvent.Signal();
// Mutexの開放まで待機
releaseEvent.Wait();
releaseEvent.Dispose();
// Mutex開放
instance.ReleaseMutex();
instance.Dispose();
instance = null;
}
開放時の処理(Disposeメソッド)はこう。
protected virtual void Dispose(bool disposing)
{
if (!disposedValue)
{
releaseEvent.Signal();
waitEndEvent.Dispose();
disposedValue = true;
}
}
全て繋げるとこうなります。
using System;
using System.Threading;
using System.Threading.Tasks;
namespace MutexTest
{
public class MutexWrapper : IDisposable
{
/// <summary>
/// 実インスタンス
/// </summary>
private Mutex instance;
/// <summary>
/// 取得完了イベント
/// </summary>
private CountdownEvent waitEndEvent = new CountdownEvent(1);
/// <summary>
/// 開放イベント
/// </summary>
private CountdownEvent releaseEvent = new CountdownEvent(1);
/// <summary>
/// 取得結果
/// </summary>
private bool waitResult;
/// <summary>
/// コンストラクタ
/// </summary>
/// <param name="initiallyOwned"></param>
/// <param name="name"></param>
public MutexWrapper(bool initiallyOwned, string name)
{
instance = new Mutex(initiallyOwned, name);
}
/// <summary>
/// 取得
/// </summary>
/// <returns></returns>
public virtual bool WaitOne()
{
return instance.WaitOne();
}
/// <summary>
/// 非同期取得
/// </summary>
/// <returns></returns>
public virtual Task<bool> WaitOneAsync()
{
Task.Factory.StartNew(() =>
{
MutexControlTask();
}, TaskCreationOptions.LongRunning);
return Task.Factory.StartNew(() =>
{
// 取得完了まで待受
waitEndEvent.Wait();
waitEndEvent.Reset();
return waitResult;
},TaskCreationOptions.LongRunning);
}
/// <summary>
/// Mutexの取得、開放タスク
/// </summary>
private void MutexControlTask()
{
// Mutex取得開始
waitResult = instance.WaitOne();
waitEndEvent.Signal();
// Mutexの開放まで待機
releaseEvent.Wait();
releaseEvent.Dispose();
// Mutex開放
instance.ReleaseMutex();
instance.Dispose();
instance = null;
}
#region "Disposeサポート"
private bool disposedValue = false;
protected virtual void Dispose(bool disposing)
{
if (!disposedValue)
{
releaseEvent.Signal();
waitEndEvent.Dispose();
disposedValue = true;
}
}
~MutexWrapper()
{
Dispose(false);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
#endregion
}
}
終わりに
う~~~ん。クレイジー!
(誰もやらないわけだ)
※追記
awaitやるために、内部でスレッド走らせてローレベルなシグナル処理したり
ぶっちゃけ本末転倒な感じだと思います。
ただ意地で実現したかったんです(・ω・)
サンプルについて
GitHubに動かして試せるようにサンプルを入れました。
リンク:サンプルソース
01_MutexAndAwait
.NET Core 3.1 + WPF C#で作っています。