之前学到如何关闭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 已关闭,无法释放。");
}
}
}
}
}