Winforms Application Hang Due to Systemevents.Onuserpreferencechanged Event

How to deal with this C# hang invloving SystemEvents.OnUserPreferenceChanged

Controls subscribe this event so that they will redraw themselves when the user changed the theme or system colors. This event also gets fired when you're not close to the machine and Windows automatically locks the workstation. Which explains the morning-after hangover.

The deadlock is caused by a threading problem, the SystemEvents class fires the event on the wrong thread. Which is caused by an initialization problem in your program. The typical trigger is not creating the first window on the main thread, that confuzzles SystemEvents. It tries to fire an event on that same thread again but it isn't around anymore. Or it copied SynchronizationContext.Current before it got initialized by Winforms. Either way, the event will fire on a threadpool thread instead of the main UI thread. That's lethal.

Common when you implement your own splash screen for example. Use the built-in support instead.

UI Freeze caused by WindowsFormsSynchronizationContext and System.Events.UserPreferenceChanged

I put together a solution from older tickets. Thanks very much to those guys!

WinForms application hang due to SystemEvents.OnUserPreferenceChanged event

https://codereview.stackexchange.com/questions/167013/detecting-ui-thread-hanging-and-logging-stacktrace

This solution starts a new thread that continuously tries to detect any threads which are subscribed to the OnUserPreferenceChanged Event and then provide a call stack that should tell you why that is.

public MainForm()
{
InitializeComponent();

new Thread(Observe).Start();
}

private void Observe()
{
new PreferenceChangedObserver().Run();
}

internal sealed class PreferenceChangedObserver
{
private readonly string _logFilePath = $"filePath\\FreezeLog.txt"; //put a better file path here

private BindingFlags _flagsStatic = BindingFlags.NonPublic | BindingFlags.Static;
private BindingFlags _flagsInstance = BindingFlags.NonPublic | BindingFlags.Instance;

public void Run() => CheckSystemEventsHandlersForFreeze();

private void CheckSystemEventsHandlersForFreeze()
{
while (true)
{
try
{
foreach (var info in GetPossiblyBlockingEventHandlers())
{
var msg = $"SystemEvents handler '{info.EventHandlerDelegate.Method.DeclaringType}.{info.EventHandlerDelegate.Method.Name}' could freeze app due to wrong thread. ThreadId: {info.Thread.ManagedThreadId}, IsThreadPoolThread:{info.Thread.IsThreadPoolThread}, IsAlive:{info.Thread.IsAlive}, ThreadName:{info.Thread.Name}{Environment.NewLine}{info.StackTrace}{Environment.NewLine}";
File.AppendAllText(_logFilePath, DateTime.Now.ToString("dd.MM.yyyy HH:mm:ss") + $": {msg}{Environment.NewLine}");
}
}
catch { }
}
}

private IEnumerable<EventHandlerInfo> GetPossiblyBlockingEventHandlers()
{
var handlers = typeof(SystemEvents).GetField("_handlers", _flagsStatic).GetValue(null);

if (!(handlers?.GetType().GetProperty("Values").GetValue(handlers) is IEnumerable handlersValues))
yield break;

foreach(var systemInvokeInfo in handlersValues.Cast<IEnumerable>().SelectMany(x => x.OfType<object>()).ToList())
{
var syncContext = systemInvokeInfo.GetType().GetField("_syncContext", _flagsInstance).GetValue(systemInvokeInfo);

//Make sure its the problematic type
if (!(syncContext is WindowsFormsSynchronizationContext wfsc))
continue;

//Get the thread
var threadRef = (WeakReference)syncContext.GetType().GetField("destinationThreadRef", _flagsInstance).GetValue(syncContext);
if (!threadRef.IsAlive)
continue;

var thread = (Thread)threadRef.Target;
if (thread.ManagedThreadId == 1) //UI thread
continue;

if (thread.ManagedThreadId == Thread.CurrentThread.ManagedThreadId)
continue;

//Get the event delegate
var eventHandlerDelegate = (Delegate)systemInvokeInfo.GetType().GetField("_delegate", _flagsInstance).GetValue(systemInvokeInfo);

//Get the threads call stack
string callStack = string.Empty;
try
{
if (thread.IsAlive)
callStack = GetStackTrace(thread)?.ToString().Trim();
}
catch { }

yield return new EventHandlerInfo
{
Thread = thread,
EventHandlerDelegate = eventHandlerDelegate,
StackTrace = callStack,
};
}
}

private static StackTrace GetStackTrace(Thread targetThread)
{
using (ManualResetEvent fallbackThreadReady = new ManualResetEvent(false), exitedSafely = new ManualResetEvent(false))
{
Thread fallbackThread = new Thread(delegate () {
fallbackThreadReady.Set();
while (!exitedSafely.WaitOne(200))
{
try
{
targetThread.Resume();
}
catch (Exception) {/*Whatever happens, do never stop to resume the target-thread regularly until the main-thread has exited safely.*/}
}
});
fallbackThread.Name = "GetStackFallbackThread";
try
{
fallbackThread.Start();
fallbackThreadReady.WaitOne();
//From here, you have about 200ms to get the stack-trace.
targetThread.Suspend();
StackTrace trace = null;
try
{
trace = new StackTrace(targetThread, true);
}
catch (ThreadStateException) { }
try
{
targetThread.Resume();
}
catch (ThreadStateException) {/*Thread is running again already*/}
return trace;
}
finally
{
//Just signal the backup-thread to stop.
exitedSafely.Set();
//Join the thread to avoid disposing "exited safely" too early. And also make sure that no leftover threads are cluttering iis by accident.
fallbackThread.Join();
}
}
}

private class EventHandlerInfo
{
public Delegate EventHandlerDelegate { get; set; }
public Thread Thread { get; set; }
public string StackTrace { get; set; }
}
}

Attention

1)This is a very ugly hack. It deals with threads in a very invasive way. It should never see a live customer system. I was already nervous deploying it to the customers test system.

2)If you get a logfile it might be very big. Any thread might cause hundreds of entries. Start at the oldest entries, fix it and repeat.(Because of the "tainted thread" scenario from Example 3 it might also contain false positives)

3)I am not sure about the performance impact of this hack. I assumed it would be very big. to my surprise it was almost not noteable. Might be different on other systems though

onuserpreferencechanged hang - dealing with multiple forms and mutlipe ui threads

I'm also confused about the terminology "UI thread".

A UI thread is a thread that pumps a message loop. And operates in a mode that's compatible with user interface objects, it needs to be an STA, a Single Threaded Apartment. That's a COM implementation detail that matters a great deal to common UI operations that are not thread-safe and require an STA, like Drag+Drop, the Clipboard, shell dialogs like OpenFileDialog and ActiveX components.

It is the CLR's job to call CoInitializeEx() and select the apartment type. It does so guided by the [STAThread] attribute on the Main() entrypoint in your program. Present in projects that create UI objects like a Winforms or WPF app. But not a console mode app or service. For a worker thread, in other words a thread that was created by your code instead of Windows, the apartment type is selected by what you passed to Thread.SetApartmentState() method. The default is MTA, the wrong flavor. A threadpool thread is always MTA, that cannot be changed.

The SystemEvents class has the unenviable task of figuring out which thread is the UI thread in your program. Important so it can raise events on the correct thread. It does so by using a heuristic, the first thread that subscribes an event and is an STA thread is considered suitable.

Things go wrong when that guess wasn't accurate. Or certainly in your case where you try to create multiple threads that create UI objects, the guess can only ever be correct for one of them. You probably also forgot to call Thread.SetApartmentState() so it won't be correct for any of them. WPF more strongly asserts this and will generate an exception when the thread isn't STA.

The UserPreferenceChanged event is a trouble-maker, it is subscribed by some of the controls you find on the toolbox. They use it to know that the active visual style theme was changed so they'll repaint themselves, using the new theme colors. A significant flaw in the event handlers in some of these controls is that they assume that the event is raised on the correct thread, the same thread that created the control object.

This will not be the case in your program. The outcome tends to be unpleasant, subtle painting problems are a minor flaw, deadlock is certainly possible. For some reason, locking the work station with Windows+L and unlocking it is particularly prone to causing deadlock. The UserPreferenceChanged event is raised in that case because of the desktop switch from the secure desktop the user's desktop.

The controls that listen to the UserPreferenceChanged event and do not use safe threading practices (using Control.BeginInvoke) are DataGridView, NumericUpDown, DomainUpDown, ToolStrip+MenuStrip and the ToolStripItem derived classes, possibly RichTextBox and ProgressBar (unclear).

The message ought to be clear, you are using unsafe threading practices and they can byte. There in general is never any point to creating UI on a worker thread, the main thread of a Winforms or WPF program is already quite capable of supporting multiple windows. Short from avoiding the dangerous controls, this is what you should strive for to get rid of the problem.

What causes SystemEvents.UserPreferenceChanged to be invoked?

According to this MSDN Link. UserPreferenceChanged Event may occur when one of the Events in the below categories are triggered!. This is for a class of type UserPreferenceChangedEventArgs. I think the Description is self explanatory and clear.

Sample Image

Windows Form application freeze randomly when run overnight

I experienced this exact same issue about a year ago (application hang after some time without user interaction, with OnUserPreferenceChanging() in the call stack).

The most likely cause is that you're using InvokeRequired/Invoke() on a control and not on the main form. This sometimes produces the wrong result if the control's handle hasn't been created yet.

The solution is to always call InvokeRequired/Invoke() on the Main Window (which you can cast as an ISynchronizeInvoke if you don't want to introduce a dependency to your form class).

You can find an excellent, very detailed description of the cause and solution here.



Related Topics



Leave a reply



Submit