Atomic Operations and Thread Safety
Following with the second part about Threads in C#, today I'm bringing up a very useful topic when it comes to how to build our business logic in a multithreading application ensuring the safety of our operations, I know you might be thinking that this is not a big deal when it comes to Math Operations, well, it turns out that our Business Logic spins around Classes which represents our Business Entities and these can be accessed by any process perhaps at the same time.
Why is this a problem?
Well, it turns out that there is one critical resource that is shared by all threads in a process, Heap memory, in .NET all reference types are allocated on the Heap and therefore multiple threads can, if they have a reference to the same object access the same Heap memory at the same time. This can be very efficient, but it can also be the source of potential bugs.
Let’s start with an example right away. We're going to create a Class called Character, this will contain two methods Hit and Heal, the idea is that this class will be used to represent a character in a game which will start with 100 points of Health and the logic built around it will make this character to be Hit and Healed by two different processes at the same time, same amount of Healing and Damage, this should give us as a result of 100 points of Health at the end.
using?System;
using?System.Collections.Generic;
using?System.Threading;
using?System.Threading.Tasks;
namespace?ThreadSafety
{
class?Character
{
????public?int?Health?{?get;?private?set;?}
????public?void?Hit(int?damage)
????{
????????Health?-=?damage;
????}
????public?void?Heal(int?health)
????{
????????Health?+=?health;
????}
}
class?Program
???{
???????static?void?Main(string[]?args)
???????{
???????????Characterc?=?new?Character();
???????????var?tasks?=?new?List<Task>();
???????????for?(int?i?=?0;?i?<?100;?i++)
???????????{
???????????????Task?t1?=?Task.Factory.StartNew(()?=>
????????????????{
????????????????????for?(int?j?=?0;?j?<?10;?j++)
????????????????????{
????????????????????????c.Hit(10);
????????????????????}
????????????????});
???????????????tasks.Add(t1);
???????????????Task?t2?=?Task.Factory.StartNew(()?=>
???????????????{
???????????????????for?(int?j?=?0;?j?<?10;?j++)
???????????????????{
???????????????????????c.Heal(10);
???????????????????}
???????????????});
???????????????tasks.Add(t2);
???????????}
???????????Task.WaitAll(tasks.ToArray());
???????????Console.WriteLine($"Resulting?Health?is?this={c.Health}");
???????????Console.Read();
???????}
???}
}
As you can see this code is using non-atomic operations to calculate the amount of Damage and Healing the character is receiving go ahead and run the program a few times, we should be seeing 100 points of Health but every time you run it you'll see a different result.
This is because the Summing (+=) and Subtracting (-=) operations are not atomic, it means that when you add a value to another value it happens in a couple of operations on the CPU level.
++ is three operations:
As you can see while one thread is trying to increment or decrement a variable value, since these three operations are occurring behind the scenes another thread can start executing any operation and both threads will end up writing into the variable at the same time and therefore losing one of the operations.
A race condition is an issue in software, where depending on the sequencing of instruction on different threads, incorrect behavior may be observed. So, race conditions are mostly related to timing issues.
领英推荐
What is an Atomic Operation you might wonder? well, let’s see.
An Atomic Operation is one that transitions state from one valid value to another valid value without the state having a temporary invalid value
From a CPU point of view, an operation can be considered atomic if it changes a value executing only one instruction.
Ensuring that data is not corrupted is know as Thread Safety and is often achieved by synchronizing threads to make sure that any updates are atomic.
.NET provides a set of primitives to synchronize between threads, but it is up to developers to use the primitives correctly and thus achieve thread safety.
Let's refactor our previous code to introduce the primitive called Interlocked.
using?System;
using?System.Collections.Generic;
using?System.Threading;
using?System.Threading.Tasks;
namespace?ThreadSafety
{
class?Character
{
????private?int?_health;
????public?int?Health?{?get?=>?_health;?private?set?=>?_health?=?value;?}
????public?void?Hit(int?damage)
????{
????????Interlocked.Add(ref?_health,?-damage);
????}
????public?void?Heal(int?health)
????{
????????Interlocked.Add(ref?_health,?health);
????}
}
?class?Program
????{
????????static?void?Main(string[]?args)
????????{
????????????Characterc?=?new?Character();
????????????var?tasks?=?new?List<Task>();
????????????for?(int?i?=?0;?i?<?100;?i++)
????????????{
????????????????Task?t1?=?Task.Factory.StartNew(()?=>
?????????????????{
?????????????????????for?(int?j?=?0;?j?<?10;?j++)
?????????????????????{
?????????????????????????c.Hit(10);
?????????????????????}
?????????????????});
????????????????tasks.Add(t1);
????????????????Task?t2?=?Task.Factory.StartNew(()?=>
????????????????{
????????????????????for?(int?j?=?0;?j?<?10;?j++)
????????????????????{
????????????????????????c.Heal(10);
????????????????????}
????????????????});
????????????????tasks.Add(t2);
????????????}
????????????Task.WaitAll(tasks.ToArray());
????????????Console.WriteLine($"Resulting?Health?is?this={c.Health}");
????????????Console.Read();
????????}
????}
}
As you can see I have made a few changes to the Character class, instead of ++ or -- now I'm using Interlocked.Add and added a Backing Field to be used along with this Interlocked.Add opeartion.
Let's run again the application and see the magic.
This have been everything for today, next week I'll be posting more practical examples about Threading.
Have fun coding!!!