线程在执行 SerialPort.Close() 时卡住
本文关键字:Close 执行 SerialPort 线程 | 更新日期: 2023-09-27 18:34:06
我遇到了一个问题,当我想关闭gsm终端的串行端口时,应用程序中的线程遇到死锁。这个问题在这里和这里都是众所周知的,但这些线程中的所有建议都没有帮助我。
/// <summary>
/// Closes the COM port, prevents reading and writing
/// </summary>
public void Stop()
{
Debug.WriteLine("stop called");
var block = true;
var bgw = new BackgroundWorker
{
WorkerReportsProgress = false,
WorkerSupportsCancellation = false,
};
bgw.DoWork += (s, e) =>
{
if (!CanAccessPort())
return;
try
{
_serialPort.DataReceived -= Read;
GC.ReRegisterForFinalize(_serialPort.BaseStream);
_serialPort.Close();
_isOpen = false;
}
catch (Exception ex)
{
throw new Exception(PORTERROR, ex);
}
};
bgw.RunWorkerCompleted += (s, e) =>
{
Debug.WriteLine("block is set to false =)");
block = false;
};
bgw.RunWorkerAsync();
while (block)
Thread.Sleep(250);
}
上面的代码在执行_serialPort.Close()
时永远运行。作为推荐的建议,我阅读了有关在单独线程中运行关闭操作的信息。我尝试了BackgroundWorker
和Thread
课程,但没有任何效果。按照另一个线程中的建议使用AutoResetEvent
也不起作用。在关闭端口之前,我正在向其发送一些命令并收到几个结果,但它不会关闭。当我运行一个简单的命令行程序来启动端口、读取数据并尝试关闭它时,一切正常,甚至没有线程。
什么可能导致僵局?我没有做任何与GUI相关的事情,正如几乎所有其他答案中提到的。
此处为数据接收事件处理程序代码:
/// <summary>
/// Reads input from the COM interface and invokes the corresponding event depending on the input
/// </summary>
private void Read(object sender, SerialDataReceivedEventArgs e)
{
var buffer = new char[1024];
var counter = 0;
_keepRunning = true;
if (_timeout == null)
{
// timeout must be at least 3 seconds because when sending a sms to the terminal the receive notification (+CSDI) can be lost with less timeout
_timeout = new Timer(3000);
_timeout.Elapsed += (s, ev) =>
{
_keepRunning = false;
_timeout.Stop();
};
}
_timeout.Start();
// cancel condition: no more new data for 3 seconds or "OK"/"ERROR" found within the result
while (_keepRunning)
{
var toRead = _serialPort.BytesToRead;
if (toRead == 0)
{
Thread.Sleep(100);
continue;
}
_timeout.Stop();
_timeout.Start();
counter += _serialPort.Read(buffer, counter, toRead);
// ok or error found in result string
var tmp = new string(buffer).Replace("'0", "").Trim();
if (tmp.EndsWith("OK") || tmp.EndsWith("ERROR"))
{
_timeout.Stop();
_keepRunning = false;
}
}
// remove empty array slots from the back
var nullTerminalCounter = 0;
for (var i = buffer.Length - 1; i != 0; i--)
{
if (buffer[i] == ''0')
{
nullTerminalCounter++;
continue;
}
break;
}
Array.Resize(ref buffer, buffer.Length - nullTerminalCounter);
var str = new String(buffer).Trim();
// result must be something different than incoming messages (+CMTI: '"MT'", 25)
if (!((str.StartsWith("+CMTI") || str.StartsWith("+CSDI")) && str.Length < 20))
{
// when an incoming message is received, it does not belong to the command issued, so result has not yet arrived, hence port is still blocked!
_isBlocked = false;
Debug.WriteLine("port is unblocked");
}
var args = new CommandReturnValueReceivedEventArgs
{
ResultString = str
};
OnCommandReturnValueReceived(this, args);
}
if (toRead == 0)
{
Thread.Sleep(100);
continue;
}
此代码是死锁的基本来源。 SerialPort.Close(( 的规则是,它只能在 SerialPort 的事件处理程序均未处于活动状态时关闭串行端口。 问题是,您的 DataReceived 事件处理程序几乎总是处于活动状态,等待数据。 这不是该事件的预期用途。 您应该从串行端口读取任何可用内容,通常将字节附加到缓冲区并取出。 当有更多字节可用时,该事件将再次触发。
while (_keepRunning)
看起来您发现了此问题并尝试使用计时器修复它。 这也行不通,以一种非常难以调试的非常糟糕的方式。 布尔变量不是正确的同步原语,如 ManualResetEvent。 当您以 x86 为目标并运行程序的发布版本时,while(( 循环不会看到_keepRunning变量变为 false。 这使得抖动优化器成为可能,它倾向于将变量存储在CPU寄存器中。 声明变量易失性是抑制该优化所必需的。
我怀疑,但不能保证,使用易失性可以解决您的问题。 您可能希望在调用 Close(( 之前将_keepRunning设置为 false,这样您就不会获得超时延迟。
然而,指出了更结构性的修复。 重写 DataReceived 事件处理程序,使其永远不会循环等待数据。 鉴于这似乎是与调制解调器通信的代码,因此只需要一个简单的_serialPort.ReadLine()
调用。
如果我确保Open
和Close
永远不会被任何线程同时调用,我的问题就消失了。我通过使用锁来做到这一点。
本质如下。
lock(_port)
_port.Open(.....);
...
lock(_port)
_port.Close(.....);