C# 串口程序在關閉串口時候會死鎖

爲什麼會死鎖呢,併發衝突。

我們要了解一下SerialPort的實現和串口通訊機制,在你打開串口的時候,SerialPort會創建一個監聽線程ListenThread,在這個線程中,等待註冊的串口中斷,當收到中斷後,會調用DataReceived事件。調用完成後,繼續進入循環等待,直到串口被關閉退出線程。

我們的UI主線程如何做的呢,首先創建一個窗體,然後執行了Application.Run(窗體實例)。是這樣把,這裏的Application.Run就是創建了一個消息循環,循環的處理相關的消息。

這裏我們就有了2個線程,UI主線程、串口監聽線程。那麼你在DataReceived處理數據的時候,就需要線程同步,避免併發衝突,什麼是併發衝突?併發衝突就是2個或多個並行(至少看上去像)的線程運行的時候,多個線程共同的操作某一線程的資源,在時序上同時或沒有按我們的預計順序操作,這樣就可能導致數據混亂無序或是彼此等待完成死鎖軟件。

而串口程序大多是後者。爲什麼呢,看看我們的例子中DataReceived做了什麼?首先讀取數據,然後就是調用this.Invoke方法更新UI了。這裏Invoke的時候,監聽線程將等待UI線程的標誌,等到後,開始操作UI的資源,當操作完成之前,監聽線程也就停在DataReceived方法的調用這裏,如果這個時候。併發了關閉串口的操作會如何呢?SerialPort的Close方法,會首先嚐試等待和監聽線程一樣的一個互斥體、臨界區、或是事件(不確定.net用的哪種)。那這個同步對象什麼時候釋放呢?每次循環結束就釋放,哦。循環爲什麼不結束呢?因爲這一次的循環操作執行到DataReceived之後,執行了Invoke去更新界面了,那Invoke怎麼又沒有執行完成呢?看上去很簡單的幾行代碼。雖然我沒仔細研讀過.net的Invoke原理,但我猜測是通過消息的方式來同步的,這也是爲什麼這麼多的類,只有控件(窗體也是控件的一種,.net在概念上,顛覆了微軟自己的概念,傳統的win32編程,是說所有的控件都是個window,只是父窗體不同,表現形式不同,但都是基於系統消息隊列的,.net出於更高的抽象,正好反過來了。呵呵)纔有Invoke方法了。(委託自己的Invoke和這個不同)

我猜測控件/窗體的Invoke是SendMessage方式實現的,那麼發送消息後就會等待消息循環來處理消息了。如果你直接去關閉串口了。你點擊按鈕本身也會被轉換成消息WM_CLICK,消息循環在處理按鈕的WM_CLICK時候,調用你按鈕的OnClick方法,進而觸發調用你的ButtonClose_Click事件,這都是同步調用的,你的主線程,處理消息的過程,停在了這個Click事件,而你的Click事件又去調用了SerialPort的Close方法,Close方法又因爲和串口監聽線程的同步信號量關聯在一起需要等待一次的while結束,而這個while循環中調用了DataReceived方法,這個方法中調用了Invoke,也就是發送了消息到消息隊列等待結果,但消息循環正在處理你的關閉按鈕事件等待退出。

實在太複雜了,這個情況下,你想要真的關閉串口成功,就需要while中的DataReceived方法調用結束釋放同步信號,就需要執行完Invoke,就需要執行消息循環,幸運的是,我們真的有辦法執行消息循環來打破僵局。Application.DoEvents()。還好,不幸中的萬幸。可是問題又來了,你能讓Invoke結束,但你無法確定是否在你調用消息循環後,你的某一時刻不會再次併發,可能由於單cpu的串行操作模擬並行中,又把時間片先分給了優先級高的串口監聽線程呢?是有可能的。所以,我們就需要一點方法來避免再次invoke窗體。優化後不會司機的例子如下,我們修改DataReceived方法,關閉方法,並定義2個標記Listening和Closing。

 

namespace SerialportSample
{
    public partial class SerialportSampleForm : Form
    {
        private SerialPort comm = new SerialPort();
        private StringBuilder builder = new StringBuilder();
        //避免在事件處理方法中反覆的創建,定義到外面。
        private long received_count = 0;
       //接收計數
        private long send_count = 0;
        //發送計數
        private bool Listening = false;
         //是否沒有執行完invoke相關操作
        private bool Closing = false;
       //是否正在關閉串口,執行Application.DoEvents,
      並阻止再次invoke

        public SerialportSampleForm()
        {
            InitializeComponent();
        }

        //窗體初始化
        private void Form1_Load(object sender, EventArgs e)
        {
            //初始化下拉串口名稱列表框
            string[] ports = SerialPort.GetPortNames();
            Array.Sort(ports);
            comboPortName.Items.AddRange(ports);
            comboPortName.SelectedIndex =
            comboPortName.Items.Count > 0 ? 0 : -1;
            comboBaudrate.SelectedIndex =
            comboBaudrate.Items.IndexOf("9600");

            //初始化SerialPort對象
            comm.NewLine = "\r\n";
    

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章