-
-
Notifications
You must be signed in to change notification settings - Fork 347
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add test to check GUI thread safety #3914
Add test to check GUI thread safety #3914
Conversation
c56f2e4
to
cf52949
Compare
cf52949
to
3e6df15
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, this is freaking awesome. I love tests that allow us to utilise functionality, while saving us from foot guns!
I have one comment, that I might be missing the forest for the trees for. [ForbidGUICalls]
allows us to access properties that are generally GUI thread related, but not make invoke calls? Or do we need to remember to tag all background tasks with this attribute to ensure the checks happen?
It might be a C# nuance that I'm not used to, but the name/description is throwing me. It may just require a little expansion on the description, but it feels like this is done to 'Allow' thread safe property access, rather than prevent non-invoked gui calls (which as noted, some projects do, but has performance impacts).
No;
No, it's not done to allow access. It's as it says, GUI calls are forbidden within a function with this attribute. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok perfect, that makes sense! Too much swapping between different language reviews for me lately!
Background
WinForms is based on some of quite old Windows GUI libraries, from before multithreading was ubiquitous. The .NET implementation inherited a peculiar threading gotcha: All changes to the GUI must be made from the same "GUI thread", or the app will crash or freeze or worse.
Motivation
If you look for ways to deal with this problem, mostly you'll find people recommending
Control.InvokeRequired
/Control.Invoke
, which we already have in the form ofCKAN.GUI.Util.Invoke
. This is fine when you remember to use it correctly, but it's easy to forget, and when that happens, we get crashes or freezes or worse.A few people have created more elaborate systems that wrap the GUI objects in some kind of proxy class that calls
Invoke
as needed. These generally suffer from poor runtime performance if you use them heavily (since switching threads for every property access requires a lot of marshalling between threads) and awkward maintenance (defining aninterface
for every GUI object you create, or creating your GUI objects with a third party facility likeAOPFactory.Create<MyForm>()
). And they still don't solve the problem of what happens if you forget to use them!I have wanted for a long time to add a guardrail to catch thread-unsafe GUI code, but my search for a ready-made one never succeeded.
Changes
Now CKAN is the home of the world's first and only static thread-safety checker for WinForms. If you accidentally try to access a GUI object from a background thread, a new test will emit errors like this:
This will finally provide a guardrail to prevent GUI threading problems. It's a static check that only runs as a unit test, so there is zero run time performance impact. In the future we can consider more aggressive refactoring of our GUI thread handling, confident that the test will catch any errors.
How it works
The new test uses
Mono.Cecil
to load and analyze the compiled GUI assembly instruction-by-instruction. Any function or lambda passed toSystem.Threading.Tasks.Task.Factory.StartNew
orSystem.Threading.Tasks.Task.Run
will be treated as a background thread, and any method or property setter of any class in (or inheriting from anything in) theSystem.Windows.Forms
namespace will be treated as an unsafe GUI method. The test crawls the complete call tree of every background thread looking for calls to the GUI and throws if it finds one. CallingUtil.Invoke
breaks the chain and allows access to GUI within its argument function, as desired.But, you exclaim, sometimes we need to access properties or functions of one of our
Form
s orUserControl
s from a background thread, and it's perfectly OK because those specific properties or functions don't actually do anything GUI-related! Right you are, and for such cases we can add the new[ForbidGUICalls]
attribute to those functions, which does two things:The functions that our
BackgroundWorker
s run in the background are also marked with[ForbidGUICalls]
.In effect, everything the GUI calls in a background thread now has to be explicitly tagged as such, and nothing so tagged can interact directly with anything GUI-related, so we have achieved reliable thread-safety.