Lesson 2 - Threads in C# .NET - Sleep, Join, and lock
In the previous lesson, Introduction to multi-threaded applications in C#.NET, we created our first multi-threaded application. In today's C# .NET tutorial, we're going to learn how to block and lock threads.
Sleep and Join
We can put the current thread to sleep for a given number of milliseconds
using the static Sleep()
method on the Thread
class.
The thread is blocked until the time runs out, then it wakes up again and
continues its activity.
Let's create a new project with the Writer
class, which will
look similar to the Switcher
from the previous lesson:
class Writer { public void Write0() { for (int i = 0; i < 100; i++) { Console.Write("0"); Thread.Sleep(5); } } public void Write1() { for (int i = 0; i < 150; i++) { Console.Write("1"); Thread.Sleep(5); } } }
The Write0()
method writes 100 zeros to the console and puts its
thread to sleep for 5ms with each output. The Write1()
method
writes 150 ones and so it runs longer (for about 1/4 second) than the
Write0()
method.
Now, in the Main()
method, we'll create a thread for each method
and run them. Finally, we'll write "Done":
Writer writer = new Writer(); Thread thread1 = new Thread(writer.Write0); Thread thread2 = new Thread(writer.Write1); thread1.Start(); thread2.Start(); Console.WriteLine("Done");
The application output is as follows:
"Done" was written first because the main thread didn't wait for the writer
threads. We can wait for a thread to finish using the Join()
method
that blocks the current thread until the method finishes. Let's change our code
into this one:
Writer writer = new Writer(); Thread thread1 = new Thread(writer.Write0); Thread thread2 = new Thread(writer.Write1); thread1.Start(); thread2.Start(); thread1.Join(); thread2.Join(); Console.WriteLine("Done");
The main thread now waits until both threads finish their work. The result is as follows:
If we want to put a thread to sleep for a long time, we can pass a
TimeSpan
instance as a parameter instead of converting hours to
seconds. The TimeSpan
class has static methods like
FromHours()
and so on:
Thread.Sleep(TimeSpan.FromHours(2));
If we want the system to switch a thread, we can let it sleep even for 0 ms.
Calling Thread.Sleep()
will always block the thread. We can achieve
a similar effect using the Thread.Yield()
method.
We can ask for the thread state using the ThreadState
property.
It's a flag having one or more of these values: Running
,
StopRequested
, SuspendRequested
,
Background
, Unstarted
, Stopped
,
WaitSleepJoin
, Suspended
, AbortRequested
,
Aborted
. We use this property mainly for debugging, it's not
appropriate for synchronization.
Sharing data between threads
Of course, we often need to share some data between threads, at least for communication. You shouldn't be surprised that if we run the same method in multiple threads, it'll have its own local variables in each thread. For a simple test, let's use the class from the previous example:
Writer writer = new Writer(); Thread thread1 = new Thread(writer.Write0); thread.Start(); writer.Write0(); Console.ReadKey();
The result:
Since the console is 80 characters wide by default and less than 3 lines were
printed, we can see that both loops ran 100x and that each thread used its
i
variable.
ThreadSafety
The thread method can access instance or static variables. That's the way threads can communicate with each other. As we already know, the catch will be in the synchronization.
Imagine the following class:
class AtmUnsafe { private decimal cash = 100; private void Withdraw100() { if (cash >= 100) { Console.WriteLine("I withdraw 100"); cash -= 100; Console.WriteLine("You still have {0} on your account.", cash); } } public void WithdrawThreads() { Thread thread1 = new Thread(Withdraw100); thread1.Start(); Withdraw100(); if (cash < 0) Console.WriteLine("Cash is negative, we have been robbed."); } }
The class represents an ATM machine that contains some cash. When you create
the ATM, the cash is 100 USD. It also has a simple Withdraw100()
method that withdraws 100 dollars if there's the required balance in the bank
account. We're interested in the WithdrawThreads()
method. It tries
to withdraw $100 using 2 threads (current and newly created). If the cash
balance happens to be negative, it prints a message about that.
We'll add code to the main method, which will make 200 withdrawals from 100 ATMs:
for (int i = 0; i < 100; i++) { AtmUnsafe atm = new AtmUnsafe(); atm.WithdrawThreads(); }
And we'll run the app:
We can see from the output that something is wrong. Where's the problem?
We check whether there's sufficient cash in the account with the condition in
the Withdraw100()
method. Imagine that there is 100 USD in the
account. The condition applies and the system can put the thread to sleep, it
can happen right after evaluating the condition. So this thread is waiting now.
The second thread also checks the condition which still applies and withdraws
$100. Then the first thread wakes up, which is already after the condition, and
also withdraws $100. As a result, we have a negative balance in our account! We
can see that working with threads brings new pitfalls that we haven't
encountered before. We'll solve the situation by locks.
Locking
We certainly agree that the code part checking the balance and changing it must always run uninterrupted, otherwise, we'll get into the above-mentioned situation. We'll fix the problem by locking the part where we're working with the shared variable. We'll modify the code:
class AtmSafe { private decimal cash = 100; private object myLock = new object(); public void WithdrawThreads() { Thread thread1 = new Thread(Withdraw100); thread1.Start(); Withdraw100(); if (cash < 0) Console.WriteLine("Cash is negative, we have been robbed."); } private void Withdraw100() { lock (myLock) { if (cash >= 100) { Console.WriteLine("I withdraw 100"); cash -= 100; Console.WriteLine("you still have {0} on your account.", cash); } } } }
We lock using the lock
construct that takes a lock as a
parameter. The lock can be any object, we create a simple attribute for this
purpose. When the system wants to put the thread to sleep, it has to wait until
it comes out of the critical section (the one inside the lock).
The application now works properly, and we can declare it ThreadSafe.
In the next lesson, Monitors, thread priority, exceptions and more in C# .NET, we'll focus on more thread pitfall, say more about locks and start passing data to threads.