A code is called thread safe if it is being called from multiple threads concurrently without the breaking of functionalities. Learn what thread safety is and how to achieve it.
- [Instructor] When we write multithreaded code, we have to strive for thread safety. This is one concept that needs to be understood, implemented correctly and fully tested before we assume the code is ready for production. So what is thread safety? Code is supposed to be thread safe if and only if shared data structures are modified in a manner that ensures that all threads behave properly, fulfill their design specifications, and not have any unintended interactions.
It should not lead to deadlocks or raise conditions. Let's jump into the code and see how thread safety works in practice. We are going to create a dictionary of items. It'll be a simple static dictionary of type int and string. Call it items and initialize it right here. Using tasks, we can start a new task, and let's call it add item.
I'd like to add an item to the dictionary. Currently the dictionary has no items at all. Resolve the method. Now, typically what we would like to do is simply come here and say if items.contains, you know, let's say a certain item, then do something else, add that item.
And again, this is just pseudocode to understand what we're trying to do here. We're not going to use this code. So as you notice, it's items dot contains key, and once I check for that particular key, I could now add the item and, since it's string, we can just call it anything for now, Hello World. So this is fine as long as we make sure that only one thread will always access this particular piece of code.
Otherwise, there are potential problems with this code if you write it like this. One of those is, what if one thread is doing this, which is items dot add, while we are here and we're looking for the contains key in that particular situation. So, a lot of times what happens is developers end up locking this aspect, which is the write aspect, and then they forget to lock the read aspect, even though both of them are dependent on each other.
So thread safety, as I mentioned earlier, is very important, and we need to make sure that it's implemented correctly and is fully tested. Just to recap the definition, code is thread safe is and only if shared data structures are modified in a manner that ensures all threads behave properly. And we also need to make sure that they fulfill their design specifications and do not have any unintended interactions.
So for example, if we had no items whatsoever here, and we were able to pass through this while someone actually added an item, we may be looking at a completely different result. So in order to fix that, we will get rid of this code and do it the right way using locks. In the previous course, we saw different ways of doing locks using lock monitor dot enter and exit as well as Mutex, so if you want more details into understanding locking, I would highly recommend watching that course.
And right here, once we've locked the items collection, which is a dictionary in this case, I also want to know which actual task or process acquired this lock. So it's very simple. I could say task dot current ID. Once I have that, I would know it was task ID number X that is accessing this code. Once we've locked the items, it's a good idea to now modify the collection.
And we can give it any value, so for example, Cazton, name of my company, plus items dot count. Again, this is just test data, so feel free to use any kind of collection you'd like to use. Then I'd like to have a completely different collection, just so I could take the items, copy them into more like a read only collection for now, and be able to display those items.
In order to do that, I need to go back and make sure that items are locked again so I could do this. 'Cause if items is not locked, and it's changing or is being changed by any other thread, I will have a different collection than what I expected. Remember, code is all about predictability, and the last thing we need is surprises in data just because of code that was not written with thread safety in mind.
And I'd also like to do line number 28. I'm doing the same exact thing that I did in line number 21 so I know which particular task is accessing this line of code. Now we can run this. We can have any amount of tasks running, and then right here we can say task dot wait all and give it the IDs. So in this case I can say var task one all the way to five and then wait for all of them.
Give it the right IDs. The order really doesn't matter in this case. Again, because in a real life scenario, we will never know what the actual order of these tasks will be. This is just a simple simulation. Now we can do control shift B, so we build it correctly, and then say control F5.
So as you can see, lock one was acquired by ID one, lock two was acquired by one, and same way they all got into the lock. You can see the IDs are one, two, three, four and five. Now we would like to print these items to see what's the actual result we got finally. In order to print those, we might have to do a four each or a four loop, whatever you like. Easier way is to create a four each and hit tab and then enter your collection.
In this case, we're calling it dictionary. I'll leave that as item. And while we're doing this, I want to be able to say, the item actually is a key value pair so you can also called it kvp, just so you remember that it's not simply an item, it's more like a key value pair. And then we can move this line of code here and say kvp dot key and then maybe add a space in between, and then say kvp dot value.
Once we're done with this, control shift B to build it and then control F5. So as you can see, that was fairly quick, and this is the first item added by task ID one, and then now we have two items. So they're playing with the same dictionary. But now we have three items because it's task number three, and then all the way to task number four, we were able to add five items. And why is that? Because as you noticed, task five fired before task number four, which is very good.
Even though task three was added first, it really doesn't matter because they all are running concurrently, and anyone can take precedence over the other one. Going back to the example that I displayed earlier, if you notice, this piece of code simply checks for a key to be present, and if they key isn't present, the dictionary will add the key with the value Cazton to it. Now, if key is thread safe, where would you add the lock? Is it going to be at dictionary dot add key comma Cazton? Or is it going to be the entire if statement? The answer is, it's still going to be the entire if statement, because of the reasons we talked about earlier, which majorly means some other thread could have manipulated the dictionary.
So we want to make sure that the dictionary itself was thread safe. In dot net framework, which methods are thread safe? The static methods or the instance methods? The answer is the static methods. Now this is important to understand. Just because dot net methods, which is the dot net framework static methods, are thread safe does not mean that any static method we end up creating is going to be thread safe.
This is a big distinction, and a very important one. Why do you think the static members in dot net framework are thread safe? What was the reason that the team might have decided to do that? Let's take a simple example of any static class or method we use, something like timespan or date time or anything like that. So we have date time dot now, that's a good example of a static method. What if it was not thread safe? What if it was being written to by many other threads while you were actually reading it? That could be a big problem.
So for that reason, they made a conscious decision of creating all static members in a thread safe fashion. Now this is one thing to keep in mind, so when you are creating your own static methods, make sure that they are thread safe.
- Thread safety and affinity
- Task Parallel Library (TPL) basics
- PLINQ introduction
- Task-based Asynchronous Pattern (TAP)