在下面的代码中,即使仅在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();
});
}
1条答案
按热度按时间oipij1gg1#
这里有一大堆带有计时器和其他东西的代码,但是如果我正确理解了代码,
ProcessTimer1
和ProcessTimer2
都将在一些延迟后在UI线程上运行。您遇到问题是这两个方法以某种方式同时运行。锁的一个重要特性是,单个线程可以多次使用同一个锁对象:
这通常不是问题,因为锁是用来防止多线程访问的,只要我们在单线程上,通常都没问题。
我猜你的问题的原因是,当从UI线程调用
SynchronizationContext.Send
时,可能是在处理消息队列上的消息。UI线程有一个消息队列,用于执行不同的操作、绘制UI、处理鼠标/键盘事件或运行任意代码。SynchronizationContext.Send
是将消息添加到此队列以运行任意代码的方法之一。如果我对文档的理解正确的话,SynchronizationContext.Send
应该在返回之前等待消息被处理,并且当在UI线程上调用时,它需要处理挂起的消息,除非您希望死锁。所以事件的顺序应该是这样的:1.添加一条消息,要求UI线程调用ProcessTimer 1
mUIContext.Send
mUIContext.Send
处理消息,并调用ProcessTimer 1ProcessTimer1
观察到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(...)