Deep C# - Threading,Tasks and Locking |
Written by Mike James | ||||||||
Wednesday, 22 October 2025 | ||||||||
Page 2 of 7
Starting A ThreadThere is more than one way that you can enter the world of multi-threaded programs. Some of them are so simple that you might not even notice the transition. For example, when you employ an asynchronous call using BeginInvoke you use a separate thread to run the method without having to do anything extra. However, no matter how you create a new thread, the same considerations apply. There are two ways of explicitly using a thread in your program. The first is to use one of the existing threads in the thread pool. This has the advantage that you don’t have the overhead of actually creating a new thread, but the number of threads available for use in the thread pool is limited. In general, you should only use a thread-pool thread for tasks that are short and release the thread back to the thread pool as soon as possible. The same concerns apply to using a thread-pool thread as one you create yourself. As long as the task that you want the thread to do is sufficiently long-lived you are advised to create a thread for it. To do this you need to use the Thread class in System.Threading. The Thread constructor accepts either a ThreadStart or a ParameterizedThreadStart delegate which wraps the method that will be executed by the new thread. For example, suppose we have a method like: public void CountUp() { for (int i = 0; i < 9999; i++) MessageBox.Show("Thread1 " + i.ToString()); } then this can be run as a new thread using: Thread T1=new Thread(new ThreadStart(CountUp)); This creates the new thread object but doesn’t actually start the thread running. To do this we have to use the Start method: T1.Start(); You will now see the message box appear and as long as you keep clicking the OK button the thread will continue. There are a number of thread methods that can be used to stop and pause threads and what these do and how they are used is fairly obvious. Here we will concentrate on more difficult aspects of threading. Avoiding Race ConditionsWe often use the term “thread safe” without really bothering to define what it actually means, as if using it often enough would make its meaning obvious. Basically a block of code, or an object, is thread safe if it works correctly and as desired if multiple threads make use of it at the same time. Most of the code you will encounter isn’t thread safe and this includes most of the objects in the .NET class library and, of course, your own code. The important thing to realize is that in general code isn’t thread safe unless you take steps to make it so. What exactly is the problem? There is the obvious confusion caused by threads sharing the same data. For example, if a method has a counter then another thread starting the same method might well zero that counter and leave it in a state that is not as the first thread left it. For example, consider the following code: public int count = 0; public void A() { for (int i = 0; i < 10000000; i++) { count++; } } public void B() { for (int i = 0; i < 10000000; i++) { count++; } } private void button1_Click(object sender, EventArgs e) { Thread T1 = new Thread(new ThreadStart(A)); Thread T2 = new Thread(new ThreadStart(B)); T1.Start(); T2.Start(); T1.Join(); T2.Join(); textBox1.Text = count.ToString(); } Function A adds one to count in a loop and function B does the same. These are run as two independent threads and the main thread waits until they have completed before using the Join method. If you run this program then you will discover that the final value of count isn’t predictable because it all depends on when the two threads get access to count. This is not thread-safe coding because the result changes each time you run it – it is subject to race conditions where both tasks attempt to update the variable at the same time and the result depends on which arrived at it first. In particular consider what happened if function A is updating the counter and reads 42 but is then interrupted by function B which also reads 42. They both add one to get 43 and store the result back in count which now holds 43 when it should hold 44 as there have been two updates. In a deeper sense it isn’t thread safe because access to the global resource, i.e. count, isn’t controlled. In this case the increment is such a fast operation that the chance of being interrupted is low, but given enough updates the effect can be seen. This is the sort of problem that thread-safe code is designed to avoid. Notice that, as this sort of threading error can have a low probability of occurring, it’s possible for it to go unnoticed for many, many runs of the program and look to all intents and purposes like some sort of random hardware failure. This is what makes multi-threaded programs very difficult to debug. |
||||||||
Last Updated ( Wednesday, 22 October 2025 ) |