当前位置: 首页 > 工具软件 > uiLock > 使用案例 >

UI卡死——有趣的死锁问题

裴存
2023-12-01

一、假设有这样一个通信服务器ComServer,包含接收和发送数据方法ReceiveData和SendData,它们实现的结构是这样的。

//子线程A中调用
public void ReceiveData()
{
    lock(this)
    {
        ......
        this.Invoke(new Action(()=>
        {
            //UI主线程执行
            ......
        }));
    }
}

 

//UI主线程中调用
public void SenData()
{
    lock(this)
    {
        ......
    }
}

那么问题来了,UI发生卡死是为什么呢?

简单分析一下,当我们在子线程A中调用 ReceiveData的同时,在UI主线程中调用SendData,会有哪些情况产生?

1、ReceiveData顺利执行完Invoke,将锁释放掉,然后SendData被UI线程调用,顺利得到锁并执行。

2、ReceiveData还未执行到Invoke,此时SendData被UI线程调用,由于得不到锁,只得将UI线程挂起,等待ReceiveData释放锁,而ReceiveData继续执行到Invoke,并需要进入UI线程执行Invoke内部代码,不幸的是UI线程被SendData挂起,于是乎UI线程就一直被挂起,结果你懂的!

那怎么解决呢?

1、将Invoke改为BeginInvoke,ReceiveData将不再等待BeginInvoke中的内容执行,直接继续向下执行,及时将锁释放。

2、不加锁,也就不存在死锁问题喽~

二、线程挂起导致的死锁问题

当子线程在占用锁的情况下被Suspend挂起,将导致锁不能被释放,此时UI由于无法获得锁而被挂起,界面处于无响应状态,也无法通过界面交互再令子线程Resume继续以释放锁,就会出问题,这是Suspend和Resume被弃用的主要原因,容易在线程包含锁的情况下发生死锁问题。

public partial class Form1 : Form
    {
        private readonly static object locker = new object();
        private Thread MyThread = null;
        public Form1()
        {
            InitializeComponent();
            MyThread = new Thread(PlayCallBack);
            MyThread.Start();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            try
            {
                MyThread.Suspend();
            }
            catch { }
        }
        private void button2_Click(object sender, EventArgs e)
        {
            try
            {
                MyThread.Resume();
            }
            catch { }
        }
        protected override void OnPaint(PaintEventArgs e)
        {
            lock (locker)
            {
                //UI刷新
            }
            base.OnPaint(e);
        }
        public void PlayCallBack()
        {
            while (true)
            {
                lock (locker)
                {
                    Thread.Sleep(10);
                }
            }
        }

那解决方案是什么呢?就是在挂起线程时要避免线程正在占用锁,或者说,当子线程在占用锁的时候避免线程被挂起,因此,解决方案就是

1、判断子线程是否占用锁。

2、给挂起线程的逻辑块也加上同样的锁,当子线程释放掉锁以后,再将子线程挂起。

但是,问题是对于一个死循环逻辑,要等子线程释放锁是有些困难的,所以,判断子线程是否占用锁或单纯的为挂起线程的逻辑加锁解决不了问题。

所以,应当尽量减小锁的作用范围,最好缩减到一条语句,这样就能尽可能及时的完成锁的释放,避免在线程挂起时出现占用锁的情况,类似于下面的效果。

        public void PlayCallBack()
        {
            while (true)
            {
                Thread.Sleep(10);
                lock (locker)
                {
                    isLocking = true;
                }
                isLocking = false;
            }
        }

那么,有没有当线程挂起时自动释放锁的操作呢?

C#中可以采用ManualResetEvent来控制线程的挂起Reset和继续Set,相比于Suspend和Resume,它也不能直接释放锁,但是可以通过WaitOne来控制线程挂起的位置,避免线程在占用锁的情况下挂起,从而避免死锁问题的产生。

 public partial class Form2 : Form
    {
        private readonly static object locker = new object();
        private Thread MyThread = null;
        private ManualResetEvent are = new ManualResetEvent(true);
        public Form2()
        {
            InitializeComponent();
            MyThread = new Thread(PlayCallBack);
            MyThread.Start();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            try
            {
                are.Set();
            }
            catch { }
        }
        private void button2_Click_1(object sender, EventArgs e)
        {
            try
            {
                are.Reset();
            }
            catch { }
        }
        protected override void OnPaint(PaintEventArgs e)
        {
            lock (locker)
            {
                //UI刷新
            }
            base.OnPaint(e);
        }
        public void PlayCallBack()
        {
            while (true)
            {
                are.WaitOne();
                lock (locker)
                {
                    Thread.Sleep(10);
                }
            }
        }
    }

ResetEvent构造中有一个bool参数,如果这个参数是true,那么第一次尝试继续就不会被阻塞。如果这个参数是false,那么第一次尝试继续就会被堵塞。

AutoResetEvent和ManualResetEvent的区别:

对于AutoResetEvent来说,Set之后,所有线程最先执行到的WaitOne顺利继续,再次遇到WaitOne时就会阻塞,每Set一次,只能释放一个WaitOne,相当于执行一次WaitOne就会Reset一次,也就是AutoReset,而ManualResetEvent在Set之后,后续所有的WaitOne均能继续,直到手动Reset,接着后续所有的WaitOne都会阻塞。

 类似资料: