いろいろ備忘録日記

主に .NET とか Go とか Flutter とか Python絡みのメモを公開しています。

セッション跨いで名前付き Mutex を使ったときにうまく動かなかった件 (System.Threading.Mutex, Global Mutex, 所有権, prefix "Global\")

概要

知らなかったので忘れないうちにメモメモ。

プロセスまたいで排他制御したいときに、よく Mutex とか Semaphore とか使いますが

このとき Mutex には名前をつけてやります。

var mutex = new Mutex(false, "hogehoge");

こんな風に。よくアプリの二重起動防止とかで使いますね。

dobon.net

セッション跨いでいる場合は?

んで、知らなかったのが、この使い方だと、同一セッション内では排他制御できるけど

セッション跨いでいる場合は駄目ってことでした。

(まあ、ほぼこんな状況ないと思いますが、、、、あったりするんです現実。。。)

つまり、片方をユーザセッションで起動、もう片方がタスクスケジューラで起動していて別セッションとかって場合です。

この場合、Mutex に付ける名前に

先頭に Global\ ってつけないと駄目

です。知らなかったです。

stackoverflow.com

docs.microsoft.com

ちゃんと書いてあった・・・。読めよ俺・・。

On a server that is running Terminal Services, a named system mutex can have two levels of visibility. If its name begins with the prefix "Global\", the mutex is visible in all terminal server sessions. If its name begins with the prefix "Local\", the mutex is visible only in the terminal server session where it was created. In that case, a separate mutex with the same name can exist in each of the other terminal server sessions on the server. If you do not specify a prefix when you create a named mutex, it takes the prefix "Local\". Within a terminal server session, two mutexes whose names differ only by their prefixes are separate mutexes, and both are visible to all processes in the terminal server session. That is, the prefix names "Global\" and "Local\" describe the scope of the mutex name relative to terminal server sessions, not relative to processes.

サンプル

一応サンプルです。

まず、2個 console プロジェクト作成

(1) ユーザセッション側で起動するやつ

$ mkdir MutexExampleUser
$ cd MutexExampleUser
$ dotnet new console

ソースはこんな感じ。

using System;
using System.Threading;

namespace MutexExampleUser
{
    class Program
    {
        const string MUTEX_NAME = "MyMutex";

        static void Main(string[] args)
        {
            var handle = false;
            using (var mutex = new Mutex(initiallyOwned: false, name: MUTEX_NAME))
            {
                try
                {
                    // 所有権を取る
                    handle = mutex.WaitOne(timeout: TimeSpan.Zero);
                    if (!handle)
                    {
                        Console.Error.WriteLine("所有権が取れなかった");
                        return;
                    }

                    Console.Error.WriteLine("所有権獲得: 1分待機...");
                    Thread.Sleep(TimeSpan.FromMinutes(1));
                }
                catch (Exception mutexEx)
                {
                    Console.Error.WriteLine(mutexEx);
                }
                finally
                {
                    if (handle)
                    {
                        // 所有権を開放
                        mutex.ReleaseMutex();
                        Console.Error.WriteLine("所有権開放");
                    }

                    mutex.Close();
                }
            }
        }
    }
}

(2) タスクスケジューラで起動するやつ

$ mkdir MutexExampleAnotherSession
$ cd MutexExampleAnotherSession
$ dotnet new console

ソースはこんな感じ。

using System;
using System.IO;
using System.Threading;

namespace MutexExampleAnotherSession
{
    class Program
    {
        const string MUTEX_NAME = "MyMutex";

        static void Main(string[] args)
        {
            using (var logStream = new StreamWriter(path: $"C:/Temp/mymutex.log", append: false))
            {
                logStream.AutoFlush = true;
                Console.SetError(logStream);

                var handle = false;
                using (var mutex = new Mutex(initiallyOwned: false, name: MUTEX_NAME))
                {
                    try
                    {
                        // 所有権を取る
                        handle = mutex.WaitOne(timeout: TimeSpan.Zero);
                        if (!handle)
                        {
                            Console.Error.WriteLine("[別セッション] 所有権が取れなかった");
                            return;
                        }

                        Console.Error.WriteLine("[別セッション] 所有権獲得: 1分待機...");
                        Thread.Sleep(TimeSpan.FromMinutes(1));
                    }
                    catch (Exception mutexEx)
                    {
                        Console.Error.WriteLine(mutexEx);
                    }
                    finally
                    {
                        if (handle)
                        {
                            // 所有権を開放
                            mutex.ReleaseMutex();
                            Console.Error.WriteLine("[別セッション] 所有権開放");
                        }

                        mutex.Close();
                    }
                }
            }
        }
    }
}

タスクマネージャーだと、当然コンソール出せないのでログを出力するようにしておく。

(2) の方をタスクスケジューラに登録、その際に

ユーザがログオンしているかどうかにかかわらず実行する

をチェックしておきます。

んで、先に (1) を起動。以下のようにでます。

$ dotnet run
所有権獲得: 1分待機...

んで、すぐにタスクスケジューラから (2) を起動。 (1) 側でMutexの所有権を持っているので取れないはずが

$ tail -f mymutex.log
[別セッション] 所有権獲得: 1分待機...

取れてる・・・・・ってなります。

名前に Global\ を付ける

うまく取れなかったのを確認したので、上のソースの

const string MUTEX_NAME = "MyMutex";

const string MUTEX_NAME = @"Global\MyMutex";

に変更。

で、もう一回試してみます。

$ dotnet run
所有権獲得: 1分待機...

タスクスケジューラから起動

$ tail -f mymutex.log
[別セッション] 所有権が取れなかった

ちゃんと所有権が取れなかったってログが出ました。


過去の記事については、以下のページからご参照下さい。

  • いろいろ備忘録日記まとめ

devlights.github.io

サンプルコードは、以下の場所で公開しています。

  • いろいろ備忘録日記サンプルソース置き場

github.com

github.com