Threads in C# - Wrap up
Hello guys, this is the last part about Threads in C#, there are still a bunch of concepts to cover but I don’t want to be boring, I’ll be outlining these concepts at the end of this article.
In our last article, we introduced the concept of Atomic Operations and Thread Safety
We created a fun class about a Character who could be Hit or Healed concurrently by multiple threads (other players), we compared the difference between atomic and non-atomic using Interlocked and simple ++ operations, at the end the result was amazing, how with the introduction of Interlocked we were able to make our code more manageable and predictable.
Let’s talk about another powerful class used in Threads. Monitor, this class provides a mechanism that synchronizes access to objects, this will give you the ability to block a resource, execute operations and release it after finishing using it, in our Character class the methods Hit and Heal will be blocked while we are doing operations like ++ and --, this means that other threads will have to wait until these resources have been released to get to call them again, preventing unpredictable behavior and making sure our expected value is the one printed at the end.
Let’s see how it works.
First of all, you’ll need to call Monitor.Enter at the beginning of the code you want to lock, at the end, you need to call Monitor.Exit to release the lock, the only thing that these methods Enter and Exit require is to pass in an object that will be used to lock in memory. But there is an issue with this approach, what about if the lock is already taken by another thread? This might create a Deadlock that will cause our application to be locked indefinitely, well let me tell you that there is something pretty awesome, it turns out that you can call Monitor.TryEnter instead, this method will receive three parameters, the object used to lock in memory, the timeout that we want our lock to stay trying to lock the resource, and a ref value that will return true in case the lock was successful and false if it’s the opposite.
Let’s see how this looks in the code!!
#First - Let's create an extension method
This extension method will be used to create a wrapper that will handle all the logic regarding Monitor.TryEnter, Monitor.Exit and Timeout along with error handling and disposability.
领英推荐
using?System;
using?System.Threading;
namespace?ThreadSafety
{
????public?static?class?LockExtensions
????{
????????public?static?Lock?Lock(this?object?obj,?TimeSpan?timeout)
????????{
????????????bool?lockTaken?=?false;
????????????try
????????????{
????????????????Monitor.TryEnter(obj,?timeout,?ref?lockTaken);
????????????????if?(lockTaken)
????????????????{
????????????????????return?new?Lock(obj);
????????????????}
????????????????throw?new?TimeoutException("Failed?to?acquire?sync?object.");
????????????}
????????????catch
????????????{
????????????????if?(lockTaken)
????????????????{
????????????????????Monitor.Exit(obj);
????????????????}
????????????????throw;
????????????}
????????}
????}
????public?struct?Lock?:?IDisposable
????{
????????private?readonly?object?_obj;
????????public?Lock(object?obj)
????????{
????????????_obj?=?obj;
????????}
????????public?void?Dispose()
????????{
????????????Monitor.Exit(_obj);
????????}
????}
}
#Second - Let' create our Character class and let's implement out brand new LockExtension class.
using?System;
namespace?ThreadSafety
{
????class?Character
????{
????????private?int?_armor;
????????private?int?_health?=?100;
????????public?int?Health?{?get?=>?_health;?private?set?=>?_health?=?value;?}
????????public?int?Armor?{?get?=>?_armor;?private?set?=>?_armor?=?value;?}
????????private?readonly?object?_token?=?new?object();
????????public?void?Hit(int?damage)
????????{
????????????using?(_token.Lock(TimeSpan.FromSeconds(3)))
????????????{
????????????????Health?-=?damage?-?Armor;
????????????}
????????}
????????public?void?Heal(int?health)
????????{
????????????using?(_token.Lock(TimeSpan.FromSeconds(3)))
????????????{
????????????????Health?+=?health;
????????????}
????????}
????}
}
#Finally, let's call our Character class in a multithreading scenario.
using?System;
using?System.Collections.Generic;
using?System.Threading.Tasks;
namespace?ThreadSafety
{
????class?Program
????{
????????static?void?Main(string[]?args)
????????{
????????????Character c?=?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();
????????}
????}
}
Let's execute the code and enjoy!
In our article about?Atomic Operations and Thread Safety, we got to see that calling operations like ++ and -- inside a multithreading scenario our Character Health had at random value, but after introducing Thread Safety we got to see in each execution that our Character Health was always 100 as expected. The same happened with the code above.
If you want to continue exploring more concepts about Threads I encourage you to take a look at the following Classes.
Well guys, this has been all about Threads. As always, have fun coding!!!
CO-Founder & CTO at Olov Group
2 年Thank you bro ??