最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

C#串口關(guān)閉時(shí)主界面卡死的原因分析和解決方案

 更新時(shí)間:2025年11月07日 09:02:55   作者:小碼編匠  
最近在使用SerialPort類開發(fā)一個(gè)串口調(diào)試工具時(shí),遇到了一個(gè)經(jīng)典但令人頭疼的問(wèn)題:點(diǎn)擊關(guān)閉串口按鈕后,UI 界面直接卡死(假死),本文將帶你從現(xiàn)象出發(fā),深入.NET源碼,一步步揭開這個(gè)界面卡死背后的真相,并提供一個(gè)優(yōu)雅且根本性的解決方案,需要的朋友可以參考下

問(wèn)題背景

最近在使用 SerialPort 類開發(fā)一個(gè)串口調(diào)試工具時(shí),遇到了一個(gè)經(jīng)典但令人頭疼的問(wèn)題:點(diǎn)擊"關(guān)閉串口"按鈕后,UI 界面直接卡死(假死)

起初以為是操作不當(dāng)或資源未釋放,但反復(fù)檢查代碼邏輯并無(wú)明顯錯(cuò)誤。通過(guò)調(diào)試手段定位后,發(fā)現(xiàn)問(wèn)題出在 SerialPort.Close() 方法上。

本文將帶你從現(xiàn)象出發(fā),深入 .NET 源碼,一步步揭開這個(gè)"界面卡死"背后的真相,并提供一個(gè)優(yōu)雅且根本性的解決方案。

問(wèn)題復(fù)現(xiàn)

以下是典型的串口接收與關(guān)閉邏輯代碼:

private SerialPort comm = new SerialPort();

// 數(shù)據(jù)接收事件
void comm_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
    int n = comm.BytesToRead;
    byte[] buf = new byte[n];
    comm.Read(buf, 0, n);

    // 更新UI,使用Invoke同步主線程
    this.Invoke(new Action(() =>
    {
        // 更新文本框、日志等UI操作
        textBoxLog.AppendText($"Received: {BitConverter.ToString(buf)}\r\n");
    }));
}

// 打開/關(guān)閉按鈕點(diǎn)擊事件
private void buttonOpenClose_Click(object sender, EventArgs e)
{
    if (comm.IsOpen)
    {
        comm.Close(); // ← 卡死就發(fā)生在這里!
    }
    else
    {
        comm.Open();
    }
}

運(yùn)行程序,打開串口并持續(xù)接收數(shù)據(jù),點(diǎn)擊"關(guān)閉"按鈕后,界面瞬間無(wú)響應(yīng)——典型的 UI 卡死

定位問(wèn)題:使用調(diào)試器查看線程堆棧

根據(jù)經(jīng)驗(yàn),UI 卡死通常是由主線程阻塞引起的,尤其是多線程環(huán)境下資源競(jìng)爭(zhēng)導(dǎo)致的死鎖

我們可以通過(guò) Visual Studio 的"調(diào)試 → 全部中斷"功能暫停程序,查看調(diào)用堆棧:

  • UI 線程:停在 SerialPort.Close() 方法內(nèi)部。
  • 輔助線程(SerialPort 內(nèi)部線程):正在執(zhí)行 comm_DataReceived 回調(diào)中的 this.Invoke(...)

初步判斷:UI 線程和串口數(shù)據(jù)接收線程相互等待,形成死鎖。

深入源碼:揭開死鎖真相

為了徹底搞清楚原因,我們查閱了 .NET Framework 的 SerialPortSerialStream 源碼(可通過(guò) Reference Source 查看)。

1、SerialPort.Open() 做了什么?

public void Open()
{
    internalSerialStream = new SerialStream(...);
    internalSerialStream.DataReceived += new SerialDataReceivedEventHandler(CatchReceivedEvents);
}

Open() 方法會(huì)創(chuàng)建一個(gè) SerialStream 實(shí)例,并將 CatchReceivedEvents 綁定到其 DataReceived 事件。

2、CatchReceivedEvents 中的鎖機(jī)制

這是關(guān)鍵所在:

private void CatchReceivedEvents(object src, SerialDataReceivedEventArgs e)
{
    SerialDataReceivedEventHandler eventHandler = DataReceived;
    SerialStream stream = internalSerialStream;

    if ((eventHandler != null) && (stream != null))
    {
        lock (stream)  // ← 鎖住了 SerialStream 實(shí)例!
        {
            bool raiseEvent = false;
            try {
                raiseEvent = stream.IsOpen && (BytesToRead >= receivedBytesThreshold);
            }
            catch { /* 忽略 */ }
            finally {
                if (raiseEvent)
                    eventHandler(this, e);  // 觸發(fā)用戶定義的 DataReceived 事件
            }
        }
    }
}

可以看到,在觸發(fā)用戶事件(即你的 comm_DataReceived 方法)之前,會(huì)對(duì) SerialStream 實(shí)例加鎖。

這意味著:只要你的事件處理程序在執(zhí)行,這個(gè)鎖就不會(huì)釋放

3、SerialPort.Close() 做了什么?

public void Close()
{
    Dispose();
}

protected override void Dispose(bool disposing)
{
    if (disposing)
    {
        if (IsOpen)
        {
            internalSerialStream.Flush();
            internalSerialStream.Close();  // ← 關(guān)鍵!
            internalSerialStream = null;
        }
    }
    base.Dispose(disposing);
}

繼續(xù)追蹤 SerialStream.Close()

public virtual void Close()
{
    Dispose(true);
    GC.SuppressFinalize(this);
}

protected override void Dispose(bool disposing)
{
    if (_handle != null && !_handle.IsInvalid)
    {
        if (disposing)
        {
            lock (this)  // ← 再次鎖住 this(即 SerialStream 實(shí)例)
            {
                _handle.Close();
                _handle = null;
            }
        }
        base.Dispose(disposing);
    }
}

結(jié)論來(lái)了

  • Close() 方法內(nèi)部也會(huì)對(duì) SerialStream 實(shí)例加鎖。
  • DataReceived 事件處理程序是在 lock(stream) 塊中執(zhí)行的。
  • 如果此時(shí)事件處理程序中調(diào)用了 this.Invoke(...),它會(huì)阻塞等待 UI 線程空閑。
  • 但 UI 線程正在執(zhí)行 Close(),而 Close() 又在等待 lock(stream) 被釋放。
  • 于是形成循環(huán)等待

UI 線程等待 lock(stream) 釋放

輔助線程等待 UI 線程執(zhí)行 Invoke 委托

死鎖發(fā)生!

常見解決方案及其局限性

網(wǎng)上最常見的解決方法是引入兩個(gè)布爾標(biāo)志位:

private bool _isListening = false;
private bool _isClosing = false;

void comm_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
    if (_isClosing) return;

    // ...讀取數(shù)據(jù)
    if (!_isClosing)
    {
        this.Invoke(new Action(() => { /* 更新UI */ }));
    }
}

private void buttonOpenClose_Click(object sender, EventArgs e)
{
    _isClosing = true;
    try
    {
        if (comm.IsOpen) comm.Close();
    }
    finally
    {
        _isClosing = false;
    }
}

這種方法確實(shí)能避免死鎖,但存在以下問(wèn)題:

  • 侵入性強(qiáng):需要在多個(gè)地方判斷狀態(tài)。
  • 不夠優(yōu)雅:靠"提前退出"規(guī)避問(wèn)題,而非解決根本原因。
  • 易出錯(cuò):狀態(tài)管理復(fù)雜,尤其在多線程環(huán)境下。

推薦解決方案

使用 BeginInvoke 破解死鎖

真正的解決之道在于避免阻塞。

我們不需要讓數(shù)據(jù)接收線程等待 UI 更新完成,只需要提交任務(wù)給 UI 線程即可。

因此,將 Invoke 替換為 BeginInvoke

void comm_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
    int n = comm.BytesToRead;
    byte[] buf = new byte[n];
    comm.Read(buf, 0, n);

    // 使用 BeginInvoke 異步提交UI更新任務(wù),不阻塞當(dāng)前線程
    this.BeginInvoke(new Action(() =>
    {
        textBoxLog.AppendText($"Received: {BitConverter.ToString(buf)}\r\n");
    }));
}

為什么 BeginInvoke 能解決問(wèn)題?

  • BeginInvoke異步調(diào)用,立即返回,不等待 UI 線程執(zhí)行。
  • 數(shù)據(jù)接收線程不會(huì)被阻塞,lock(stream) 能快速釋放。
  • Close() 方法可以順利獲取鎖并關(guān)閉串口。
  • UI 線程會(huì)在空閑時(shí)自動(dòng)處理 BeginInvoke 提交的任務(wù),保證更新安全。

本質(zhì)區(qū)別
Invoke = "你必須現(xiàn)在處理!" → 阻塞等待 → 死鎖風(fēng)險(xiǎn)
BeginInvoke = "有空時(shí)幫我處理一下" → 立即返回 → 安全解耦

完整代碼

public partial class Form1 : Form
{
    private SerialPort _serialPort = new SerialPort();

    public Form1()
    {
        InitializeComponent();
    }

    private void comm_DataReceived(object sender, SerialDataReceivedEventArgs e)
    {
        if (!_serialPort.IsOpen) return;

        int n = _serialPort.BytesToRead;
        byte[] buffer = new byte[n];
        _serialPort.Read(buffer, 0, n);

        // 異步更新UI,避免阻塞串口線程
        this.BeginInvoke(new Action(() =>
        {
            textBoxLog.AppendText($"[RX] {BitConverter.ToString(buffer)}\r\n");
        }));
    }

    private void buttonOpenClose_Click(object sender, EventArgs e)
    {
        if (_serialPort.IsOpen)
        {
            _serialPort.Close(); // 不再卡死!
            buttonOpenClose.Text = "打開串口";
        }
        else
        {
            _serialPort.PortName = "COM3";
            _serialPort.BaudRate = 9600;
            _serialPort.DataReceived += comm_DataReceived;
            _serialPort.Open();
            buttonOpenClose.Text = "關(guān)閉串口";
        }
    }
}

總結(jié)

問(wèn)題原因解決方案
SerialPort.Close() 卡死Invoke 阻塞導(dǎo)致死鎖改用 BeginInvoke 異步更新UI

核心要點(diǎn)

1、SerialPort 內(nèi)部使用鎖保護(hù)資源,DataReceived 事件在鎖內(nèi)觸發(fā)。

2、Close() 方法也需要獲取同一把鎖,存在競(jìng)爭(zhēng)風(fēng)險(xiǎn)。 3、Invoke 會(huì)阻塞輔助線程,是死鎖的導(dǎo)火索。

4、BeginInvoke 是更安全的選擇**,尤其在事件回調(diào)中更新 UI。

開發(fā)啟示

遇到"卡死"問(wèn)題,優(yōu)先考慮死鎖、阻塞、跨線程同步。

學(xué)會(huì)使用調(diào)試器查看線程堆棧,快速定位阻塞點(diǎn)。

閱讀源碼是解決問(wèn)題的終極武器。本文雖未完全讀懂所有細(xì)節(jié),但關(guān)鍵路徑的分析已足夠定位問(wèn)題。

結(jié)果不重要,方法才是關(guān)鍵。掌握"現(xiàn)象 → 定位 → 分析 → 解決"的閉環(huán)能力,遠(yuǎn)比記住一個(gè)技巧更有價(jià)值。

最后

以上就是C#串口關(guān)閉時(shí)主界面卡死的原因分析和解決方案的詳細(xì)內(nèi)容,更多關(guān)于C#串口關(guān)閉時(shí)主界面卡死的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • C#遍歷文件夾及子目錄下所有圖片

    C#遍歷文件夾及子目錄下所有圖片

    這篇文章主要為大家詳細(xì)介紹了C# 遍歷文件夾及子目錄下所有圖片的相關(guān)資料,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2017-01-01
  • C#中實(shí)現(xiàn)網(wǎng)段掃描的代碼

    C#中實(shí)現(xiàn)網(wǎng)段掃描的代碼

    C#中實(shí)現(xiàn)網(wǎng)段掃描的代碼...
    2007-04-04
  • C#讀寫EXCEL單元格的問(wèn)題實(shí)現(xiàn)

    C#讀寫EXCEL單元格的問(wèn)題實(shí)現(xiàn)

    這篇文章主要介紹了C#讀寫EXCEL單元格的問(wèn)題實(shí)現(xiàn),小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧
    2019-04-04
  • C#批量插入數(shù)據(jù)到Sqlserver中的三種方式

    C#批量插入數(shù)據(jù)到Sqlserver中的三種方式

    這篇文章主要為大家詳細(xì)介紹了C#批量插入數(shù)據(jù)到Sqlserver中的三種方式,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2016-12-12
  • 如何用C#實(shí)現(xiàn)壓縮文件

    如何用C#實(shí)現(xiàn)壓縮文件

    話說(shuō)當(dāng)今壓縮市場(chǎng)三足鼎立,能叫上名號(hào)的有zip、rar、7z。其中zip是壓縮界的鼻祖,在各大平臺(tái)上的流行度最廣,rar是商業(yè)軟件,壓縮率和效率都是很高的。7z是開源的,屬于后起之秀。這篇文章介紹了如何使用ICSharpCode組件,在windows平臺(tái)下zip的壓縮與解壓的方法
    2021-06-06
  • C#編寫COM組件的方法分析

    C#編寫COM組件的方法分析

    這篇文章主要介紹了C#編寫COM組件的方法,結(jié)合實(shí)例形式分析了C#編寫COM組件的具體步驟與相關(guān)實(shí)現(xiàn)技巧,需要的朋友可以參考下
    2017-06-06
  • C#連接mysql數(shù)據(jù)庫(kù)完整實(shí)例

    C#連接mysql數(shù)據(jù)庫(kù)完整實(shí)例

    這篇文章主要介紹了C#連接mysql數(shù)據(jù)庫(kù)的方法,以一個(gè)完整實(shí)例形式分析了C#操作mysql數(shù)據(jù)庫(kù)連接的基本技巧,非常具有實(shí)用價(jià)值,需要的朋友可以參考下
    2015-05-05
  • 使用aspose.word 第三方的插件實(shí)現(xiàn)導(dǎo)出word

    使用aspose.word 第三方的插件實(shí)現(xiàn)導(dǎo)出word

    本文給大家分享的是一個(gè)使用使用aspose.word 第三方的插件實(shí)現(xiàn)導(dǎo)出word的實(shí)例,十分的實(shí)用,有需要的小伙伴可以參考下。
    2015-06-06
  • C#實(shí)現(xiàn)字符串格式化的示例代碼

    C#實(shí)現(xiàn)字符串格式化的示例代碼

    在日常編程中,經(jīng)常需要對(duì)字符串進(jìn)行格式化操作,以便呈現(xiàn)為不同的格式,本文主要為大家詳細(xì)介紹了C#實(shí)現(xiàn)字符串格式化的相關(guān)知識(shí),希望對(duì)大家有所幫助
    2023-12-12
  • C#使用Newtonsoft.Json中的JObject對(duì)象

    C#使用Newtonsoft.Json中的JObject對(duì)象

    本文詳細(xì)講解了C#使用Newtonsoft.Json中JObject對(duì)象的方法,文中通過(guò)示例代碼介紹的非常詳細(xì)。對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2022-07-07

最新評(píng)論

贵溪市| 洱源县| 平利县| 繁昌县| 商丘市| 禹城市| 瑞安市| 鄱阳县| 临邑县| 满洲里市| 潍坊市| 盐城市| 根河市| 图木舒克市| 西充县| 德阳市| 金山区| 格尔木市| 田阳县| 兰溪市| 伊宁县| 桓台县| 通辽市| 贺州市| 衡水市| 蓬溪县| 土默特右旗| 壶关县| 遂平县| 两当县| 西乌珠穆沁旗| 宿州市| 盐亭县| 新疆| 班玛县| 新野县| 习水县| 临潭县| 高碑店市| 凤凰县| 南充市|