winforms SynchronizationContext.Send breaks lock语句

mum43rcc  于 2023-05-29  发布在  其他
关注(0)|答案(1)|浏览(152)

在下面的代码中,即使仅在lock语句中修改mCounter变量,有时也会显示CheckCounter方法。如果我注解DoAnythingElseWithUI调用,则永远不会出现问题。DoAnythingElseWithUI似乎中断了lock语句,并允许timer1事件在timer2事件释放锁之前继续。

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Timers;
using System.Windows.Forms;

namespace LockThreadTest
{
    public partial class Form1 : Form
    {
        private SynchronizationContext mUIContext;
        private System.Timers.Timer mTimer1 = new System.Timers.Timer();
        private System.Timers.Timer mTimer2 = new System.Timers.Timer();

        public delegate void CompletedEventHandler(object sender);
        public event CompletedEventHandler CompletedEvent1;
        public event CompletedEventHandler CompletedEvent2;

        private object lockObject = new object();

        private static int mCounter = 0;

        public Form1()
        {
            mUIContext = WindowsFormsSynchronizationContext.Current;

            InitializeComponent();

            mTimer1.Interval = 1000;
            mTimer2.Interval = 1000;
            mTimer1.Elapsed += new System.Timers.ElapsedEventHandler(Timer1_Elapsed);
            mTimer2.Elapsed += new System.Timers.ElapsedEventHandler(Timer2_Elapsed);

            this.CompletedEvent1 += Form1_CompletedEvent1;
            this.CompletedEvent2 += Form1_CompletedEvent2;
        }

        public virtual void OnCompletedEvent1()
        {
            if (CompletedEvent1 != null)
                CompletedEvent1(this);
        }

        public virtual void OnCompletedEvent2()
        {
            if (CompletedEvent2 != null)
                CompletedEvent2(this);
        }

        private void Timer1_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
        {
            OnCompletedEvent1();
        }

        private void Timer2_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
        {
            OnCompletedEvent2();
        }

        private void ProcessTimer1()
        {
            lock (lockObject)
            {
                CheckCounter();

                mCounter++;
                WriteLog($"ProcessTimer1 started {mCounter}");

                Random random = new Random();
                Thread.Sleep(random.Next(50));

                mCounter--;
                WriteLog($"ProcessTimer1 finished {mCounter}");
            }
        }

        private void ProcessTimer2()
        {
            lock (lockObject)
            {
                CheckCounter();

                mCounter++;
                WriteLog($"ProcessTimer2 started {mCounter}");

                Random random = new Random();
                Thread.Sleep(random.Next(300));
                DoAnythingElseWithUI(); //after comment this line, the problem is gone

                mCounter--;
                WriteLog($"ProcessTimer2 finished {mCounter}");
            }
        }

        private void DoAnythingElseWithUI()
        {
            ExecuteUIContextAction(() =>
            {
                WriteLog("Anything else");
            });
        }

        private void CheckCounter()
        {
            if (mCounter != 0)
            {
                MessageBox.Show($"Alert! {mCounter}");
            }
        }

        private void WriteLog(string message)
        {
            richTextBox1.AppendText($"{DateTime.Now.ToString("HH:mm:ss:fffff")} {message}{Environment.NewLine}");
        }

        private void Form1_CompletedEvent1(object sender)
        {
            ExecuteUIContextAction(() =>
            {
                ProcessTimer1();
            });
        }

        private void Form1_CompletedEvent2(object sender)
        {
            ExecuteUIContextAction(() =>
            {
                ProcessTimer2();
            });
        }

        private void btnStart_Click(object sender, EventArgs e)
        {
            ThreadPool.QueueUserWorkItem(state =>
            {
                mTimer1.Start();
                mTimer2.Start();
            });
        }

        public void ExecuteUIContextAction(Action action)
        {
            if (mUIContext == null)
            {
                if (WindowsFormsSynchronizationContext.Current == null)
                {
                    WindowsFormsSynchronizationContext.SetSynchronizationContext(new SynchronizationContext());
                    mUIContext = WindowsFormsSynchronizationContext.Current;
                }
            }
            mUIContext.Send(new SendOrPostCallback(delegate (object state)
            {
                action();
            }), null);
        }

        public SynchronizationContext UIContext
        {
            get { return mUIContext; }
        }

        private void btnStop_Click(object sender, EventArgs e)
        {
            mTimer1.Stop();
            mTimer2.Stop();
        }
    }
}

有人能告诉我为什么吗?
正确的解决方案是像这样将锁移动到Form1_CompletedEventX中吗?

lock (lockObjectForRevisible)
{
   ExecuteUIContextAction(() =>
   {
      ProcessTimer1();
   });
}
oipij1gg

oipij1gg1#

这里有一大堆带有计时器和其他东西的代码,但是如果我正确理解了代码,ProcessTimer1ProcessTimer2都将在一些延迟后在UI线程上运行。您遇到问题是这两个方法以某种方式同时运行。
锁的一个重要特性是,单个线程可以多次使用同一个锁对象:

var obj = new object();
lock(obj){
   lock(obj){
      // works fine!
   }
}

这通常不是问题,因为锁是用来防止多线程访问的,只要我们在单线程上,通常都没问题。
我猜你的问题的原因是,当从UI线程调用SynchronizationContext.Send时,可能是在处理消息队列上的消息。UI线程有一个消息队列,用于执行不同的操作、绘制UI、处理鼠标/键盘事件或运行任意代码。SynchronizationContext.Send是将消息添加到此队列以运行任意代码的方法之一。如果我对文档的理解正确的话,SynchronizationContext.Send应该在返回之前等待消息被处理,并且当在UI线程上调用时,它需要处理挂起的消息,除非您希望死锁。所以事件的顺序应该是这样的:

  1. ProcessTimer 2被调用
  2. ProcessTimer 2休眠,从而阻塞UI线程。
    1.添加一条消息,要求UI线程调用ProcessTimer 1
  3. ProcessTimer 2唤醒
  4. ProcessTimer 2调用mUIContext.Send
  5. mUIContext.Send处理消息,并调用ProcessTimer 1
  6. ProcessTimer1观察到mCounter != 0
    如果删除DoAnythingElseWithUI调用,则不会调用mUIContext.Send,并且ProcessTimer 1无法开始运行,直到ProcessTimer 2将控制权返回给消息循环。
    修复并不容易建议,因为不清楚您正在尝试做什么。但是对于大多数应用程序,你应该避免使用UI线程以外的任何东西:
    1.使用调用UI线程上事件的windows forms timer
    1.摆脱Thread.Sleep,如果你真的需要延迟一些事情,Task.Delay可能是一个替代方案。
    1.摆脱锁,因为您应该只在UI线程上运行代码
    1.摆脱同步上下文,因为您应该只在UI线程上运行代码。
    如果绝对需要在后台运行一些计算量很大的代码,请使用await Task.Run(...)

相关问题