Visual Basic .NET or Visual Basic 2005 offers the ability to use threads in Visual Basic applications for the first time. Threads introduce debugging issues such as race conditions and deadlocks. This article explores these two issues.
A race condition
occurs when two threads access a shared variable at the same time. The first thread reads the variable, and the second thread reads the same value from the variable. Then the first thread and second thread perform their operations on the value, and they race to see which thread can write the value last to the shared variable. The value of the thread that writes its value last is preserved, because the thread is writing over the value that the previous thread wrote.
Details and Example
Each thread is allocated a predefined period of time to execute on a processor. When the time that is allocated for the thread expires, the thread's context is saved until its next turn on the processor, and the processor begins the execution of the next thread.
How can a one-line command cause a race condition? Examine the following example to see how a race condition occurs. There are two threads, and both are updating a shared variable called total
(which is represented as dword ptr ds:[031B49DCh]
in the assembly code).
Visual Basic code:
'Thread 1 Total = Total + val1
'Thread 2 Total = Total - val2
Assembly code (with line numbers) from the compilation of the preceding Visual Basic code:
'Thread 1 1. mov eax,dword ptr ds:[031B49DCh] 2. add eax,edi 3. jno 00000033 4. xor ecx,ecx 5. call 7611097F 6. mov dword ptr ds:[031B49DCh],eax
'Thread 2 1. mov eax,dword ptr ds:[031B49DCh] 2. sub eax,edi 3. jno 00000033 4. xor ecx,ecx 5. call 76110BE7 6. mov dword ptr ds:[031B49DCh],eax
By looking at the assembly code, you can see how many operations the processor is performing at the lower level to execute a simple addition calculation. A thread may be able to execute all or part of its assembly code during its time on the processor. Now look at how a race condition occurs from this code.
Total is 100, val1 is 50, and val2 is 15. Thread 1 gets an opportunity to execute but only completes steps 1 through 3. This means that Thread 1 read the variable and completed the addition. Thread 1 is now just waiting to write out its new value of 150. After Thread 1 is stopped, Thread 2 gets to execute completely. This means that it has written the value that it calculated (85) out to the variable Total. Finally, Thread 1 regains control and finishes execution. It writes out its value (150). Therefore, when Thread 1 is finished, the value of Total is now 150 instead of 85.
You can see how this might be a major problem. If this were a banking program, the customer would have money in their account that should not be present.
This error is random, because it is possible for Thread 1 to complete its execution before its time on the processor expires, and then Thread 2 can begin its execution. If these events occur, the problem does not occur. Thread execution is nondeterministic, therefore you cannot control the time or order of execution. Also note that the threads may execute differently in runtime versus debug mode. Also, you can see that if you execute each thread in series, the error does not occur. This randomness makes these errors much harder to track down and debug.
To prevent the race conditions from occurring, you can lock shared variables, so that only one thread at a time has access to the shared variable. Do this sparingly, because if a variable is locked in Thread 1 and Thread 2 also needs the variable, Thread 2's execution stops while Thread 2 waits for Thread 1 to release the variable. (For more information, see "SyncLock" in the "References" section of this article.)
The most common symptom of a race condition is unpredictable values of variables that are shared between multiple threads. This results from the unpredictability of the order in which the threads execute. Sometime one thread wins, and sometime the other thread wins. At other times, execution works correctly. Also, if each thread is executed separately, the variable value behaves correctly.
occurs when two threads each lock a different variable at the same time and then try to lock the variable that the other thread already locked. As a result, each thread stops executing and waits for the other thread to release the variable. Because each thread is holding the variable that the other thread wants, nothing occurs, and the threads remain deadlocked.
Details and Example
The following code has two objects, LeftVal and RightVal:
'Thread 1SyncLock LeftVal SyncLock RightVal 'Perform operations on LeftVal and RightVal that require read and write. End SyncLockEnd SyncLock
'Thread 2SyncLock RightVal SyncLock LeftVal 'Perform operations on RightVal and LeftVal that require read and write. End SyncLockEnd SyncLock
A deadlock occurs when Thread 1 is permitted to lock LeftVal. The processor stops Thread 1's execution and begins the execution of Thread 2. Thread 2 locks RightVal and then tries to lock LeftVal. Because LeftVal is locked, Thread 2 stops and waits for LeftVal to be released. Because Thread 2 is stopped, Thread 1 is permitted to continue executing. Thread 1 tries to lock RightVal but cannot, because Thread 2 has locked it. As a result, Thread 1 starts to wait until RightVal becomes available. Each thread waits for the other thread, because each thread has locked the variable that the other thread is waiting on, and neither thread is unlocking the variable that it is holding.
A deadlock does not always occur. If Thread 1 executes both locks before the processor stops it, Thread 1 can perform its operations and then unlock the shared variable. After Thread 1 unlocks the variable, Thread 2 can proceed with its execution, as expected.
This error seems obvious when these snippets of code are placed side by side, but in practice, the code may appear in separate modules or areas of your code. This a very hard error to track down because, from this same code, both correct execution and incorrect execution can occur.
A common symptom of deadlock is that the program or group of threads stops responding. This is also known as a hang
. At least two threads are each waiting for a variable that the other thread locked. The threads do not proceed, because neither thread will release its variable until it gets the other variable. The whole program can hang if the program is waiting on one or both of those threads to complete execution.
What Is a Thread?
Processes are used to separate the different applications that are executing at a specified time on a single computer. The operating system does not execute processes, but threads do. A thread is a unit of execution. The operating system allocates processor time to a thread for the execution of the thread's tasks. A single process can contain multiple threads of execution. Each thread maintains its own exception handlers, scheduling priorities, and a set of structures that the operating system uses to save the thread's context if the thread cannot complete its execution during the time that it was assigned to the processor. The context is held until the next time that the thread receives processor time. The context includes all the information that the thread requires to seamlessly continue its execution. This information includes the thread's set of processor registers and the call stack inside the address space of the host process.