Click here to Skip to main content
15,748,082 members
Articles / Programming Languages / C#
Tip/Trick
Posted 15 Apr 2023

Stats

9.8K views
9 bookmarked

ThreadGuard - C# Protect Your Variables from Race Conditions and Show Your Variables Thread Intentions

Rate me:
Please Sign up or sign in to vote.
5.00/5 (6 votes)
15 Apr 2023CPOL3 min read
A way to catch / prevent race conditions in multithreaded code
This code/pattern will: make your intentions clear about which synchronization objects protect which variables. Detect improper access of variables without proper synch object acquisition. Clarify the intentions of volatile variable access and ensure use in a safer way.

Introduction

This code is based on an idea recommended for C# feature - this could be done better at the C# and compiler level in some cases so voting might help get this in (GuardedBy feature) dotnet/csharplang#892.

You can find the code at https://github.com/DavesApps/ThreadGuard.

From the book Essential Software Development Career + Technical Guide

Multithreaded code is easy to get wrong. A couple of the main concerns are:

  1. Race conditions due to unprotected variable access are easy to introduce/ hard to detect
  2. Deadlocks due to lock ordering
  3. Livelocks, lock contention, etc.

This code addresses some areas of problem #1. It does this by helping to make multithreading clear/defined about which synchronization objects are protecting which code elements more visible. Currently, when using locks, developers reading others' code need to infer what variables are to be protected and by which synchronization objects by looking at all of the variable accessed in a code block between acquiring and release of a lock. If we can make this more visible in code, it will be easier to tell what variables/objects should be protected and by which synchronization objects. Also this code has the ability to detect when unprotected access to variables/objects is attempted to catch when these problems happen.

Using the Code

Goals

  • Make it easy to visualize protected and unprotected variable access
  • Detect unprotected variable access

This isn't a new idea Java already has something similar.

It's easy to make your multithreaded variable decisions visible and protect your variables from access without the synchronization object being acquired.

Example of creating a Monitor (standard C# lock) and using it to protect a bool variable. You can protect as many variables as you want.

C#
MonitorGuard _mgc = new MonitorGuard();
GuardedVar<bool> _tcv = new GuardedVar<bool>(_mgc); // this variable is guarded
                                                    // by the _mgc monitor/lock

Example of using the variable after acquiring the lock:

C#
using (mgc.Acquire())
{
            _tcv.Value = true;           
}

If you use the guarded variable without acquiring the lock first, it will throw a configurable exception by default that is GuardFailureException.

With that code, it's easy to tell which of your variables need multithreading protection and it will throw an exception when used unprotected. These are very lightweight checks, so performance should not be impacted.

ThreadGuard also lets you encode your intentions for a lock free variable accessing via Volatile.Read and Volatile.Write methods. Normally, when you do this, in many cases, you can only have one thread writing to the variable and other threads can read. Sometimes, you only use variables like this to tell a thread to exit - typically the only safe usage is for example defaulting a bool to false like IsDone and setting to true (ideally from one thread- though other variants exist).

You can encode your intentions as follows:

C#
GuardedVolatileVarBool _tgvvIsDone = new GuardedVolatileVarBool(true);
_tgvvIsDone.Value = false;

       Thread thread1 = new Thread(() =>
       {
           while (!_tgvvIsDone.Value)
           {  //do something here
           }
       });
       thread1.Start();
       //do something
       _tgvvIsDone.Value = true; Will cause thread to exit
       thread1.Join();

So with this example, it's easy to see variable _tgvvIsDone is protected and if it's used inappropriately like writing to the value from multiple threads, it will throw an exception. It is also best practice (in some cases required) to use volatile for variable access which it does in that class. It will even prevent changing the value to something different if you desire (only allowing one write to the value - i.e., one shot flag is often the safest way to use).

Summary

So in the above code, you can:

  • Make your intentions clear about the protected variables
  • It's clear which synchronization objects are protecting which variables
  • Detect improper access of variables without proper synch object acquisition
  • Clarify the intentions of volatile variable access and ensure use in a safer way

So please vote for the idea to build this into C# (dotnet/csharplang#892) if you like it (as in the compiler could use keywords instead of needing these classes. It may also be able to detect some improper use at compile time for some cases), and in the meantime, consider using something like this or these ideas to make your multithreaded code safer.

History

  • 15th April, 2023: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
United States United States
Languages/frameworks: C#, .NET, ASP.NET, C/C++, WPF
Experienced in Web development, UI development, frameworks and multi-threading.


Author of the book Essential Software Development Career + Technical Guide.
https://www.amazon.com/dp/B0BXHYWMDP/

Comments and Discussions

 
QuestionInteresting Pin
Keith Rule17-Apr-23 9:23
professionalKeith Rule17-Apr-23 9:23 
QuestionHow much would we actually gain? Pin
Paulo Zemek16-Apr-23 15:44
mvaPaulo Zemek16-Apr-23 15:44 
QuestionRe: How much would we actually gain? Pin
wmjordan16-Apr-23 23:16
professionalwmjordan16-Apr-23 23:16 
AnswerRe: How much would we actually gain? Pin
DavesApps17-Apr-23 2:39
DavesApps17-Apr-23 2:39 
AnswerRe: How much would we actually gain? Pin
DavesApps18-Apr-23 7:03
DavesApps18-Apr-23 7:03 
GeneralRe: How much would we actually gain? Pin
Paulo Zemek18-Apr-23 9:00
mvaPaulo Zemek18-Apr-23 9:00 
GeneralRe: How much would we actually gain? Pin
DavesApps18-Apr-23 9:34
DavesApps18-Apr-23 9:34 
GeneralRe: How much would we actually gain? Pin
Paulo Zemek18-Apr-23 9:43
mvaPaulo Zemek18-Apr-23 9:43 
GeneralRe: How much would we actually gain? Pin
Paulo Zemek18-Apr-23 9:50
mvaPaulo Zemek18-Apr-23 9:50 
GeneralRe: How much would we actually gain? Pin
DavesApps18-Apr-23 10:06
DavesApps18-Apr-23 10:06 
GeneralRe: How much would we actually gain? Pin
Paulo Zemek18-Apr-23 15:17
mvaPaulo Zemek18-Apr-23 15:17 
Having variables separate from their locks fits existing patterns?
True, but...
Are the existing patterns good, or are they the source of bugs?

As I see, having a guardedby keyword means we want to be able to have compile-time validations. So, if we forget to lock, the compiler can tell us that we forgot that lock (I would not talk about implicit locking as that can lead to excessive locking/unlocking).
The problem comes when we acquire a lock, then call a method that will use those objects protected by the lock. Especially when I was dealing with locks in C++, and those locks were not reentrant, I would, in a more simple context, either assume a method was called when a lock was already obtained, or that a method would need to acquire the lock.

The first pattern was to pass an extra parameter, like Locked<x> that actually didn't contain anything. It was just the signal that: To call this method, you must have that particular lock. Similar to what happens in C# that to pass an argument by ref, you need to add the ref keyword... in C++ that's not needed and might be a source of errors.

Anyway, moving forward, why should we have 2 (or maybe 10 or more) fields saying "I am guarded by X"... when we can make those 2 or more fields (or even just a single field) be "owned" by the lock?
When you don't acquire the lock, you simply don't have access to those fields. When you acquire the lock, you receive another object (a "locked" object) that has access to all those fields. So, aside from copying that locked object and doing other clearly wrong stuff, you just don't have access to the fields by accident. You get compile-time errors and the like, and we don't need new fancy language features to do things that the language is already capable of doing.

Does it change how we write code? For sure... but adding guardedby to a lot of fields is also changing... and to me the change is less drastic, especially with many fields requiring the lock, when we have a single lock and many fields declared inside such a lock.

I even wrote a very simple MutexLock class. The entire code is at GDB online Debugger | Code, Compile, Run, Debug online C, C++

The code isn't fully tested, but it is a working proof of concept that doesn't require new compiler features, neither having heap allocated objects to do the locks.
GeneralRe: How much would we actually gain? Pin
DavesApps19-Apr-23 2:39
DavesApps19-Apr-23 2:39 
GeneralRe: How much would we actually gain? Pin
Paulo Zemek19-Apr-23 17:22
mvaPaulo Zemek19-Apr-23 17:22 
GeneralMy vote of 5 Pin
LightTempler15-Apr-23 12:33
LightTempler15-Apr-23 12:33 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.