问题:如何安全的使用Mutex(防遗弃), AbandonedMutexException:The wait completed due to an abandoned mutex.

之前学到如何关闭Mutex并回收所有资源,写了一个代码。

按照一个很经典的逻辑:获取了Mutex之后执行工作然后再检查是否已经持有Mutex,如果确实持有Mutex,再释放这个Mutex。但是实际运行报错了。思考了三个小时,最后在微软文档和大佬的博客中找到了端倪。

先说结论:Mutex具有多次请求和获取需要相等的特点,如果你获取了两次,就必须要释放两次。如果你使用了WaitOne(0);来判断线程是否已经持有锁,再去释放这个锁,你就出现少释放一次的情况,会让计数器多记一次数,此时计算机认为该线程没有完全释放Mutex,但是此线程会正常结束,导致了一个Mutex被遗弃,从而导致了这个错误的出现。

中间问了Ai很多遍,GPT4O回答了几十个问题,就是没有解释明白。这报错的代码实际上也是它根据我的要求写的,这告诉我,AI真的会写一些导致错误的代码,如果你没有充足的底蕴去发现这个错误,可能会很严重,这里我的错误就是:

1.没有发现Mutex具有多次请求和获取需要相等的特点,如果你获取了两次,就必须要释放两次。

2.不了解异常发生的原因,不理解何为遗弃,以及过度依赖AI。

接下来看为什么导致这个结果:

AbandonedMutexException:The wait completed due to an abandoned mutex.

介绍是: 

当一个线程获取了一个Mutex对象,而另一个线程在退出时没有释放该对象,则会引发该异常。

报错代码: 

using System;
using System.Threading;

class Program
{
    static Mutex mutex = new Mutex();

    static void Main()
    {
        // 启动多个线程来使用 Mutex
        Thread thread1 = new Thread(() => ThreadProc("线程1"));
        Thread thread2 = new Thread(() => ThreadProc("线程2"));

        thread1.Start();
        thread2.Start();

        thread1.Join();
        thread2.Join();

        // 关闭 Mutex 对象
        Console.WriteLine("开始关闭Mutex");
        mutex.Close();
        Console.WriteLine("主线程:Mutex 对象已关闭。");
    }

    static void ThreadProc(string threadName)
    {
        try
        {
            Console.WriteLine($"{threadName} 尝试获取 Mutex...");
            mutex.WaitOne();
            Console.WriteLine($"{threadName} 已获取 Mutex。");

            // 模拟工作
            Console.WriteLine($"{threadName} 正在执行工作...");
            Thread.Sleep(2000);
        }
        catch (AbandonedMutexException ex)
        {
            // 捕获 AbandonedMutexException
            Console.WriteLine($"{threadName} 捕获到 AbandonedMutexException: " + ex.Message);
        }
        catch (ObjectDisposedException ex)
        {
            // 捕获 ObjectDisposedException
            Console.WriteLine($"{threadName} 捕获到 ObjectDisposedException: " + ex.Message);
        }
        finally
        {
            try
            {
                if (mutex.WaitOne(0)) // 仅当当前线程持有 Mutex 时才释放
                {
                    mutex.ReleaseMutex();
                    Console.WriteLine($"{threadName} 释放 Mutex。");
                }
            }
            catch (ObjectDisposedException)
            {
                // 处理在 Mutex 被关闭后尝试释放的情况
                Console.WriteLine($"{threadName} 捕获到 ObjectDisposedException: Mutex 已关闭,无法释放。");
            }
        }
    }
}

输出结果: 

线程2 尝试获取 Mutex...
线程1 尝试获取 Mutex...
线程2 已获取 Mutex。
线程2 正在执行工作...
线程2 释放 Mutex。
线程1 捕获到 AbandonedMutexException: The wait completed due to an abandoned mutex.
线程1 释放 Mutex。
开始关闭Mutex
主线程:Mutex 对象已关闭。

 本来令我很难理解,但是看了一个例子我就理解了一大半:

何为遗弃的线程:

请仔细阅读注释:

using System;
using System.Threading;

public class Example
{
    private static ManualResetEvent _dummy = new ManualResetEvent(false);

    private static Mutex _orphan1 = new Mutex();
    private static Mutex _orphan2 = new Mutex();
    private static Mutex _orphan3 = new Mutex();
    private static Mutex _orphan4 = new Mutex();
    private static Mutex _orphan5 = new Mutex();

    [MTAThread]
    public static void Main()
    {
        // 启动一个线程,该线程获取所有五个 Mutex,然后
        // 在不释放它们的情况下结束。
        //
        Thread t = new Thread(new ThreadStart(AbandonMutex));
        t.Start();
        // 确保线程已完成。
        t.Join();

        // 等待一个被遗弃的 Mutex。WaitOne 立即返回,
        // 因为它的等待条件被遗弃的 Mutex 满足,但返回时会抛出
        // AbandonedMutexException。
        try
        {
            _orphan1.WaitOne();
            Console.WriteLine("WaitOne succeeded.");
        }
        catch (AbandonedMutexException ex)
        {
            Console.WriteLine("Exception on return from WaitOne." +
                "\r\n\tMessage: {0}", ex.Message);
        }
        finally
        {
            // 无论是否抛出异常,当前
            // 线程都拥有 Mutex,必须释放它。
            //
            _orphan1.ReleaseMutex();
        }

        // 创建一个等待句柄数组,包含一个
        // ManualResetEvent 和两个 Mutex,使用另外两个
        // 被遗弃的 Mutex。
        WaitHandle[] waitFor = { _dummy, _orphan2, _orphan3 };

        // WaitAny 在数组中的任何一个等待句柄被信号触发时返回,
        // 因此两个被遗弃的 Mutex 中的任何一个都满足其等待条件。
        // 返回时,WaitAny 抛出 AbandonedMutexException。
        // MutexIndex 属性返回两个被遗弃的 Mutex 中较低的索引值。
        // 注意 Try 块和 Catch 块以不同的方式获取索引。
        //  
        try
        {
            int index = WaitHandle.WaitAny(waitFor);
            Console.WriteLine("WaitAny succeeded.");

            // 当前线程拥有 Mutex,必须释放它。
            Mutex m = waitFor[index] as Mutex;
            if (m != null) m.ReleaseMutex();
        }
        catch (AbandonedMutexException ex)
        {
            Console.WriteLine("Exception on return from WaitAny at index {0}." +
                "\r\n\tMessage: {1}", ex.MutexIndex, ex.Message);

            // 无论是否抛出异常,当前
            // 线程都拥有 Mutex,必须释放它。
            //
            if (ex.Mutex != null) ex.Mutex.ReleaseMutex();
        }

        // 使用另外两个被遗弃的 Mutex 进行 WaitAll 调用。
        // WaitAll 直到所有等待句柄都被信号触发才返回,
        // 因此必须通过调用 Set() 来触发 ManualResetEvent。
        _dummy.Set();
        waitFor[1] = _orphan4;
        waitFor[2] = _orphan5;

        // 被触发的事件和两个被遗弃的 Mutex 满足
        // WaitAll 的等待条件,但返回时会抛出
        // AbandonedMutexException。对于 WaitAll,MutexIndex
        // 属性始终为 -1,Mutex 属性始终为 null。
        //  
        try
        {
            WaitHandle.WaitAll(waitFor);
            Console.WriteLine("WaitAll succeeded.");
        }
        catch (AbandonedMutexException ex)
        {
            Console.WriteLine("Exception on return from WaitAll. MutexIndex = {0}." +
                "\r\n\tMessage: {1}", ex.MutexIndex, ex.Message);
        }
        finally
        {
            // 无论是否抛出异常,当前
            // 线程都拥有 Mutex,必须释放它们。
            //
            _orphan4.ReleaseMutex();
            _orphan5.ReleaseMutex();
        }
    }

    [MTAThread]
    public static void AbandonMutex()
    {
        _orphan1.WaitOne();
        _orphan2.WaitOne();
        _orphan3.WaitOne();
        _orphan4.WaitOne();
        _orphan5.WaitOne();
        // 通过在不释放它们的情况下退出来遗弃 Mutex。
        Console.WriteLine("Thread exits without releasing the mutexes.");
    }
}

/* 输出会是:

Thread exits without releasing the mutexes.
Exception on return from WaitOne.
        Message: The wait completed due to an abandoned mutex.
Exception on return from WaitAny at index 1.
        Message: The wait completed due to an abandoned mutex.
Exception on return from WaitAll. MutexIndex = -1.
        Message: The wait completed due to an abandoned mutex.
 */

遗弃:

说人话就是:一个线程获取了Mutex然后结束了,没有释放就导致了Mutex的遗弃,所以后面的线程使用了被遗弃的Mutex,也就是说,Wait方法错误的结束了等待。

检错过程

首先是观察输出:

Mutex正常释放了。

捕获报错异常类型:

trycatch发现是AbandonedMutexException:The wait completed due to an abandoned mutex.

其次是观察异常产生位置:

异常由后面运行的线程获取Mutex时产生。

了解异常原因:

异常是因为上一个使用此Mutex的线程没有释放就结束或者崩溃了。

推理:

首先是确定线程1遗弃了Mutex,线程2才会异常。

然后为什么线程1会遗弃Mutex,原因是没有释放,但是我们确信释放成功了。

删改代码确定出错位置:

删除或者注释掉和Mutex相关的代码,找到引发的原因。

最后确定问题位置:

WaitOne(0);因为它会返回当前线程是否拥有Mutex,所以我拿来判断是否持有Mutex,然后去释放Mutex。但是发现不进行此判断,程序就会正常运行。

查询WaitOne:

问题就在这,AI根本不会通过我使用了这个导致报错就告诉我是因为多次请求Mutex就要多次释放。

然后去了解遗弃的可能:

什么会导致遗弃,释放了还会遗弃的可能性是什么

最后找到:

ReleaseMutex():释放当前 Mutex 一次。注意,这里强调了一次,因为拥有互斥体的线程可以在重复的调用Wait系列函数而不会阻止其执行;这个跟Monitor的Enter()/Exit()可以在获取对象锁后可以被重复调用一样。Mutex被调用的次数由公共语言运行库(CLR)保存,每WaitOne()一次计数+1,每ReleaseMutex()一次计数-1,只要这个计数不为0,其它Mutex的等待者就会认为这个Mutex没有被释放,也就没有办法获得该Mutex。 另外,跟Monitor.Exit()一样,只有Mutex的拥有者才能RleaseMutex(),否则会引发异常。

报错代码中导致遗弃的原因

WaitOne();

if (mutex.WaitOne(0)) // 仅当当前线程持有 Mutex 时才释放
{
    mutex.ReleaseMutex();
    Console.WriteLine($"{threadName} 释放 Mutex。");
}

看了结论很容易理解这里为什么少释放一次,因为前面还获取了一次Mutex,这里少释放一次就会导致程序结束却没有释放Mutex,遗弃的Mutex被线程2捡到,然后就出大问题了。

解决办法:

 很简单,不使用WaitOne(0);检查就可以了,而是使用bool值记录当前线程是否已经获得Mutex。

using System;
using System.Threading;

class Program
{
    static Mutex mutex = new Mutex();

    static void Main()
    {
        // 启动多个线程来使用 Mutex
        Thread thread1 = new Thread(() => ThreadProc("线程1"));
        Thread thread2 = new Thread(() => ThreadProc("线程2"));

        thread1.Start();
        thread2.Start();

        thread1.Join();
        thread2.Join();

        // 关闭 Mutex 对象
        Console.WriteLine("开始关闭Mutex");
        mutex.Close();
        Console.WriteLine("主线程:Mutex 对象已关闭。");
    }

    static void ThreadProc(string threadName)
    {
        bool hasMutex = false;
        try
        {
            Console.WriteLine($"{threadName} 尝试获取 Mutex...");
            mutex.WaitOne();
            hasMutex = true;
            Console.WriteLine($"{threadName} 已获取 Mutex。");

            // 模拟工作
            Console.WriteLine($"{threadName} 正在执行工作...");
            Thread.Sleep(2000);
        }
        catch (AbandonedMutexException ex)
        {
            // 捕获 AbandonedMutexException
            Console.WriteLine($"{threadName} 捕获到 AbandonedMutexException: " + ex.Message);
            hasMutex = true; // 即使抛出异常,当前线程仍然持有 Mutex
        }
        catch (ObjectDisposedException ex)
        {
            // 捕获 ObjectDisposedException
            Console.WriteLine($"{threadName} 捕获到 ObjectDisposedException: " + ex.Message);
        }
        finally
        {
            if (hasMutex)
            {
                try
                {
                    mutex.ReleaseMutex();
                    Console.WriteLine($"{threadName} 释放 Mutex。");
                }
                catch (ObjectDisposedException)
                {
                    // 处理在 Mutex 被关闭后尝试释放的情况
                    Console.WriteLine($"{threadName} 捕获到 ObjectDisposedException: Mutex 已关闭,无法释放。");
                }
            }
        }
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值