In the following code, the CheckCounter method is displayed sometimes even the mCounter variable is modified only within lock statement. If I comment the DoAnythingElseWithUI calling, the problem never occurs. It seems that DoAnythingElseWithUI breaks the lock statement and allow the timer1 event continue before the timer2 event release the lock.
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();
}
}
}
Could somebody tell me why?
Is the right solution move the lock into Form1_CompletedEventX like this?
lock (lockObjectForRevisible)
{
ExecuteUIContextAction(() =>
{
ProcessTimer1();
});
}
There is a whole bunch of code with timers and stuff, but If I understand the code correctly, both
ProcessTimer1andProcessTimer2will run on the UI thread after some delay. And that the problem you are experiencing is that both method are somehow running at the same time.An important feature of locks is that a single thread can take the same lock object multiple times:
This is usually not a problem, since locks are meant to protect against multi threaded access, and as long as we are on a single thread all is usually fine.
I would guess that the reason for your issues is that when
SynchronizationContext.Sendis called from the UI thread is probably processes messages on the message queue. The UI thread has a queue of messages to do different things, draw the UI, process mouse/keyboard events, or run some arbitrary piece of code.SynchronizationContext.Sendis one of the ways to add messages to this queue to run some arbitrary code. If I understand the documentation correctly,SynchronizationContext.Sendis supposed to wait for the message to be processed before returning, and when called on the UI thread it needs to process pending messages unless you want a deadlock. So the order of events should be something like this:mUIContext.SendmUIContext.Sendprocesses messages, and calls ProcessTimer1ProcessTimer1observes thatmCounter != 0If you remove the
DoAnythingElseWithUIcall,mUIContext.Sendwill not be called, and ProcessTimer1 cannot start running until ProcessTimer2 returns control back to the message loop.A fix is not easy to suggest, since it is not clear what you are trying to do. But for most applications you should avoid using anything other than the UI thread:
Thread.Sleep, if you really need to delay something,Task.Delaymay be an alternative.If you absolutely need to run some compute heavy code in the background, use
await Task.Run(...)