Training
Certifications
Books
Special Offers
Community




 
Designing Enterprise Applications with Microsoft® Visual Basic® .NET
Author Robert Ian Oliver
Pages 500
Disk N/A
Level All Levels
Published 10/16/2002
ISBN 9780735617216
Price $49.99
To see this book's discounted price, select a reseller below.
 

More Information

About the Book
Table of Contents
Sample Chapter
Index
Companion Content
Related Series
Related Books
About the Author

Support: Book & CD

Rate this book
Barnes Noble Amazon Quantum Books

 


Chapter 3: Multithreaded Programming



3  Multithreaded Programming

The ability to perform multiple simultaneous tasks is often a critical feature for more sophisticated applications. The Microsoft .NET Framework's threading libraries finally give developers who use Microsoft Visual Basic the freedom to create fully threaded applications. Entire portions of an application can be executed asynchronously, allowing for more efficient designs and more responsive user interaction.

Most introductory Visual Basic .NET books touch on the subject of .NET threading, but they seldom go beyond the simplest of examples. A large amount of information about threading is never included in most practical texts. I'll attempt to address threading concepts at a deeper level. I should state up front, however, that a single chapter can never serve as a comprehensive treatise on a topic such as this. Our discussion will focus on a number of advanced threading concepts without embroiling the reader in nitty-gritty technical details that would be of little use to the more casual reader. In other words, this chapter will provide an introduction to more advanced topics from a practical perspective and at the same time provide some guidance on when, where, and how to use threads in your applications.

Threads can allow you to create sophisticated applications, but not without a cost. More threads do not always mean better performance, and how the threading is implemented will affect your performance and resource usage (either positively or negatively). In this chapter, I'll outline critical methods for implementing threading and other threading-related features and demonstrate how best to use them in your applications. I'll also highlight some practices that are best avoided.

I'll assume that you have at least a passing familiarity with the fundamentals of the technologies under discussion. You should know what threads are and how to create "simple" threads in Visual Basic .NET. But because of the conceptual complexity of threading issues, we'll start with an overview of basic threading concepts and then progress to concepts such as synchronization constructs. I'll provide examples to demonstrate useful techniques and thread usage, which you can build on in your own projects.

An Overview of Basic Threading Concepts

Visual Basic .NET and the .NET Framework provide a full set of threading capabilities that make creating and running simple threads very easy to do. But before we get into doing just that, let's take a step back and review how processes and threads are related and how this relationship can affect your application designs.

Processes and Threads

Applications run in separate processes on a computer. For each new application launched, a new process is created. A process by definition contains at least one execution thread and has its own address space. This serves to isolate applications from each other because processes cannot share memory directly. Isolation prevents one process from trampling over the memory of another process, ensuring, at least in theory, that one application cannot cause another to crash. This also means that applications cannot directly share memory-resident resources (arrays, classes, or variables). Even sharing non-memory-resident resources, such as disk-resident files, can be tricky.

Threads, on the other hand, are the basic unit to which an operating system allocates processor time. The operating system allocates execution time to threads, not (as you might think) processes. A thread requires an owner process and cannot exist on its own. Processes, on the other hand, can contain multiple threads but must contain at least one thread—the main process thread (usually Sub Main). When the main process thread exits, the entire process is terminated—any other active threads will be prematurely terminated. Threads can share memory with other threads that are within the same process. A thread also has an associated set of resources that includes its own exception handlers, scheduling priority, and a set of structures that allow the operating system to preserve the thread's context as other threads are given execution time.

The .NET Framework takes this model a step further by introducing the concept of application domains. An application domain is a lightweight, managed subprocess that can contain one or more threads. Threads within application domains can create additional application domains.

Execution Time

Threads are granted execution time by the operating system in what are known as time slices. The duration of a time slice is system-dependent and varies between operating systems and processors. In the vast majority of environments, only one thread can be given execution control at a time. The exception is when threads are running on a multiprocessor machine.

Limitations of Threads

Any given system will have an upper limit on the number of threads per process. On systems running Microsoft Windows XP, that limit is approximately 2000 threads per process. As a practical matter, the limit is actually much lower. Each thread constitutes real physical resources on a machine. The memory requirements of preserving the thread context information cause the number of possible threads to be bounded by the available memory.

The memory commitment aside, tracking larger numbers of threads creates a significant burden for the operating system, which has to switch between all of the active threads on the system on a regular basis. If there are too many threads, the amount of execution time provided to each thread will not be enough to make significant progress in a reasonable amount of time. This will also affect other applications in the system. Remember that the processor execution time is divided among the active threads. If one application has a single thread and another has three, the first application will get only a quarter of the processing time. As the number of threads in the second application increases, the first application will get increasingly less processor time. This should highlight the need to be a good threading citizen. You can adversely affect other applications on the same machine if you use your threads unwisely.

The bottom line here is that you should create only as many threads as you need and no more. Due to the burden that additional threads impose on the system, you should make careful choices about what tasks belong in separate threads. Here are a couple of additional facts about threads:

  • The amount of time given to a thread is not guaranteed.
  • The order of thread activation is not guaranteed.
  • Events or operations occurring on different threads cannot be assumed to be synchronized; you must perform any synchronization explicitly.

Creating Threads

The core functionality of threading for Visual Basic .NET is contained in the System.Threading.Thread class. This class provides all of the operations that can be performed on managed threads. In the managed world, threads are encapsulated by the Thread class. You use the Thread class to create and manage additional threads within your application. Managed threads, like all other managed objects, are maintained by the .NET runtime environment.

There are three important steps to creating any managed thread in Visual Basic .NET:

  1. Create a method to serve as the thread's starting point.
  2. Create a ThreadStart delegate, using the AddressOf operator with the method you want called when the new thread starts.
  3. Create a Thread object and pass it the ThreadStart delegate you created.

After following these steps, you'll have a thread that can be started and run on its own. The following example demonstrates how to create two separate threads and start them. The example is among the chapter's sample files and is named Basic Threading.

Imports System.Threading
 
Module BasicThreading
   Public Const MAX_COUNT As Integer = 200
 
   Sub Main()
      ' Create the thread objects
      Dim Thread1 As New Thread(New ThreadStart(AddressOf ThreadMethod1))
      ' We can omit the New ThreadStart and just use the AddressOf operator.
      Dim Thread2 As New Thread(AddressOf ThreadMethod2)
 
      ' Start the threads
      Thread1.Start()
      Thread2.Start()
 
      ' Give the user a chance to read the input
      Console.ReadLine()
   End Sub
 
   ' The respective thread methods
   Public Sub ThreadMethod1()
      Dim i As Integer
      For i = 0 To MAX_COUNT
         Console.WriteLine("ThreadMethod1 {0}", i)
      Next
   End Sub
 
   Public Sub ThreadMethod2()
      Dim i As Integer
      For i = 0 To MAX_COUNT
         Console.WriteLine("ThreadMethod2 {0}", i)
      Next
   End Sub
End Module

When we run this application, the output looks like that shown in Figure 3-1. Both threads print out a repeated set of lines to the console. Note that your results (which thread will actually run first and which will finish first) might vary from run to run and will definitely vary from machine to machine. (Remember the three threading facts mentioned earlier.)

Click to view graphic
Click to view graphic

Figure 3-1  Sample output of the Basic Threading sample.

The Basic Threading sample demonstrates the simplest form of threading: the thread class is given the address of a method that it will execute. The method itself has no notion of threads, nor can we pass any parameters to the thread method. One solution to this, given the simplest case, is to declare global variables that are used by your thread methods. This is not very elegant and can be very messy. This does not necessarily present a problem for simple tasks, but it becomes an issue when more threads are involved or if you want to expose that functionality as a library to other developers.

The sample uses a global constant, MAX_COUNT, to pass information to the threads. The problem with this is that the mechanism is imprecise—both of the thread methods use the same constant. But what if we want the threads to execute differently? What if we need to have each thread execute with different initial conditions and we want to create an arbitrary number of threads? Obviously, it gets much trickier using dedicated global variables—we have to create specific variables for each thread, which can lead to a maintenance nightmare.

What we need is a way to pass information to the thread execution methods without creating specific global variables for each thread. What we need is a better way to manage thread creation and execution: encapsulation.

Encapsulating Threads

To get a better handle on creating, managing, and working with threads, a good strategy is to use encapsulation. Creating a class to encapsulate your threads is not the only answer to the control problem, but it is the cleanest. Being able to handle your threads in an object-oriented manner not only simplifies the management of those threads but prevents misuse by other developers. Remember one of the central themes of this book: always explicitly design your code for how it should be used!

Wrapping a Thread with a Class

In the earlier section titled "An Overview of Basic Threading Concepts," all the work to create the threads had to be done, repeatedly, by the main subroutine. From a development standpoint, this is not particularly efficient and can lead to programming errors. (Repetition is rarely desirable.) The solution is to encapsulate the thread's implementation and runtime logic into a separate class, as in the following example:

Imports System.Threading
 
Public Class MyThreadClass
 
   Private m_Thread As Thread
 
   Public Sub New()
      Me.m_Thread = New Thread(AddressOf Me.Run)
   End Sub
 
   Public Sub Start()
      Me.m_Thread.Start()
   End Sub
 
   Private Sub Run()
      ' The thread's main logic goes here.
   End Sub
 
End Class

This is a good start, but it suffers from a major limitation. We cannot set initial conditions for this thread. We need to find a way to give the Run method some information to differentiate one MyThreadClass from another. We can do this most simply by providing a parameterized constructor. This makes the class inherently more useful by allowing it to perform different tasks based on different initial conditions. Using the class-based encapsulation scheme, you can pass parameters to the thread through the class's constructor. The constructor will then set some private variables that the thread can access when it needs to.

If we extend the class we just created to function like the Basic Threading example, we end up with a class that looks something like the following:

Imports System.Threading
 
Public Class MyThreadClass
 
   Private m_Thread As Thread
   Private m_Id As Integer
   Private m_LoopCount As Integer
 
   Public Sub New(ByVal id As Integer, ByVal loopCount As Integer)
      Me.m_Id = id
      Me.m_LoopCount = loopCount
      Me.m_Thread = New Thread(AddressOf Me.Run)
   End Sub
 
   Public Sub Start()
      Me.m_Thread.Start()
   End Sub
 
   ' This is an instance method.
   ' Note that it can use the 'Me' reference pointer.
   Private Sub Run()
      Dim i As Integer
      For i = 0 To Me.m_LoopCount
         Console.WriteLine("Thread{0} {1}", Me.m_Id, i)
      Next
   End Sub
 
End Class

Now we have a class that can provide information to a thread so it can identify itself, and we can customize its behavior. As a result, we can create several different instances of MyThreadClass, all of which will behave according to the supplied initial conditions. We also have made it simple to create new threads, and we've built the requirements for encapsulation into the design itself. With this implementation, it is impossible to create an instance of MyThreadClass without supplying the required initial conditions. This allows us to avoid putting in error-handling code or default settings. Take a look at how much simpler the implementation code looks:

Module Module1
 
    Sub Main()
        Dim t1 As New MyThreadClass(1, 200)
        Dim t2 As New MyThreadClass(2, 300)
 
        t1.Start()
        t2.Start()
 
        ' Again, we wait for the threads to finish.
        Console.ReadLine()
    End Sub
 
End Module

Now you see how you can encapsulate threads to simplify implementation and provide better control over how information is passed to the threads. Also note that you can use this mechanism to return information that results from thread execution. This all leads into the discussion of how to control the execution of your threads.

Controlling Thread Execution

All threads have a life cycle. Each thread is created, executes, and dies at some point. Understanding how to manipulate threads through this cycle is essential. Each state has certain implications for the system and thread. Understanding how these states affect your thread and the application that it executes within can make the difference between success and disaster.

The ThreadState Property and the Life Cycle of a Thread

The Thread class provides an instance property called ThreadState that exposes the current state of a thread. The type of this property is the ThreadState enumeration, which is defined in the System.Threading namespace. Each state listed in the ThreadState enumeration corresponds to a specific point in a thread's life cycle. Table 3-1 describes these states.

Table 3-1  ThreadState Enumeration Values

MemberDescription
AbortedThe thread is in the Stopped state.
AbortRequestedThe Thread.Abort method has been invoked on the thread, but the thread has not yet received the pending System.Threading.ThreadAbortException that will attempt to terminate it.
RunningThe thread has been started. It is not blocked, and no ThreadAbortException is pending.
StoppedThe thread has stopped.
SuspendedThe thread has been suspended.
SuspendRequestedThe thread is being asked to suspend.
UnstartedThe Thread.Start method has not been invoked on the thread.
WaitSleepJoinThe thread is blocked as a result of a call to Wait, Sleep, or Join.

To put this discussion in context, Figure 3-2 shows the thread states in action. You can see how the thread goes from its initial "unstarted" state through running, suspension, and eventual death (Aborted or Stopped).

Click to view graphic
Click to view graphic

Figure 3-2  The life cycle of a managed thread.

Now let's delve a little deeper into what the individual states really mean and what implications they have for threads and for the system.

Unstarted

This is the initial state of all managed threads. When a thread is in the Unstarted state, it has never been run. At this point, the thread is not considered active. Although it consumes memory, which happens as soon as the thread object is constructed, it does not consume any additional processing resources until it is started. Once a thread has been started, it can never return to this state.

You can have a virtually indefinite number of thread objects in your application that are in the Unstarted state. In other words, the operating system places no limitation on the number of thread objects as long as the threads have not been started. The number of Unstarted threads is limited by the total amount of memory available. Each Unstarted Thread object consumes a small amount of memory, which merely contains the type information, the ThreadStart delegate for the thread's main method, and some other minor details.

Once you start the thread, things get more complicated. Your thread not only starts to consume processor time, but more memory is allocated to store the thread's state and other relevant information. Ultimately, there is a limit on the number of active threads available to a Windows system. The magic here number is 2000. If you attempt to start any additional active threads once you've reached this limit, you'll run into problems. Thankfully, this is a fairly rare problem because most well-designed applications don't create hundreds, let alone thousands, of active threads.

Running

The Running state indicates that the thread is active and is executing its main routine. If a thread looks at its own ThreadState, it will almost always return Running. This should be fairly obvious: the thread has to execute the statement, so it must be running. In more rare cases, ThreadState might return AbortRequested, which indicates that another thread has tried to abort the current thread. For the most part, though, a thread is rarely interested in its own state; it is more for the information of other threads.

If a thread evaluates another thread's state, it might return Running. On a single-processor machine, this result might seem counterintuitive. After all, only a single thread can be executing at any given time. But ultimately, Running does not mean "executing." A thread that is in the Running state might or might not be executing. What Running means, more that anything else, is that the thread is in an active state—it has been given execution time by the processor, but it might not be the currently executing thread.

Suspended

The Suspended state indicates that the thread has been started but is not currently active. This thread will not execute, under any circumstances, until it is told to resume by another thread.

WaitSleepJoin

The WaitSleepJoin state is a kind of catch-all thread state. It represents three distinct mechanisms but a single result. The thread is blocked, pending a specific event. The three possibilities are as follows:

  • The thread is waiting for one or more objects (Wait).
  • The thread is sleeping for a specific duration (Sleep).
  • The thread is waiting on another thread's completion (Join).

You can make a thread wait for one or more objects by using the synchronization constructs described in the section titled "Thread Synchronization" later in this chapter. To make a thread sleep for a specific duration or wait for another thread's completion, you can use the Sleep and Join methods of the Thread class. The bottom line is that a thread cannot cause another thread to enter this state. A thread can only enter this state at its own behest.

Stopped

A thread's ThreadState is the ThreadState.Stopped value when the method pointed to by the thread's ThreadStart delegate returns. From the thread's perspective, when that routine returns, everything it was created for has been completed. The Stopped state, like the Aborted state, indicates that the thread has finished executing. Once a thread is in the Stopped state, it can not be restarted. It is, for all intents and purposes, dead. The Stopped state indicates that the thread completed executing in a fairly mundane fashion—it was simply done.

Aborted

An aborted thread is a thread that has terminated in an abnormal fashion. The Aborted state, like the Stopped state, indicates that the thread has finished executing. Once a thread is in the Aborted state, it cannot be restarted. It is, for all intents and purposes, dead. If a thread is in the Aborted state, its method pointed to by the thread's ThreadStart delegate is exiting in an abnormal fashion. This is not necessarily a bad thing, but it does mean that the thread was terminated due to a cause other than simply returning at the end of a task. Once the thread has been completely terminated, the thread will be in the Stopped state.

Before we get into how to manipulate the threads using these states, we need a little information on how to access a thread reference.

Referencing the Current Thread

It is often necessary for a thread to obtain a reference to itself. Thanks to the .NET Framework, instead of having to pass around a reference to a thread variable, you can use a Shared property of the Thread class called Thread.CurrentThread:

Public Shared ReadOnly Property CurrentThread As Thread

This property always returns a reference to the current thread. This allows you to write generic methods that can always have access to the thread they're running on. As you read about the thread control methods in the following section, keep in mind that you can always gain access to and control over the current thread in any part of your code. Accessing different threads is another matter, but accessing the current thread for the sake of control is about as simple as it could be.

Thread Control Methods

Now that I've talked about the states of a thread and how any thread can get a reference to itself, we need to look at how to coax a thread through its life cycle. The methods provided by the Thread class give you complete control over the life cycle of threads. Table 3-2 provides an overview of these methods.

Table 3-2 Thread Control Methods

TypeMethodDescription
InstanceStartCauses a thread to start running.
SharedSleepPauses the calling thread for a specified time.
InstanceSuspendPauses a thread when it reaches a safe point.
InstanceAbortStops a thread when it reaches a safe point.
InstanceResumeRestarts a suspended thread.
InstanceJoinCauses the calling code to wait for a thread to finish. If this method is used with a timeout value, it will return True if the thread finishes in the allotted time.
InstanceInterruptInterrupts a thread that is in the WaitSleepJoin state.
SharedSpinWaitCauses a thread to wait the number of times defined by the iterations parameter. This is equivalent to inserting a simple counter loop in your code.

The purpose of some of these methods is almost painfully obvious, but the in-depth discussion that follows highlights how these methods can and should be used, which is not always so obvious. An excellent example is the concept of safe points. As described by Robert Burns on MSDN, safe points are places in code where it is safe for the common language runtime (CLR) to perform automatic garbage collection. When the Abort or Suspend methods of a thread are called, the CLR analyzes the thread's code and determines an appropriate location for the thread to stop running.

Thread.Start

Public Sub Start()

Before a thread can do anything, its Start method must be called by another thread. This will cause the thread's ThreadState to change to the Running state. Repeated calls to Start will cause a ThreadStateException. Start can be called only once per thread and is allowed only when the thread is in the Unstarted state.

A thread can never call its own Start method because calling Start on a thread that's not in the Unstarted state will cause a ThreadStateException to be thrown. By definition, a thread can reference itself only if it has already been started, so it should never call Start on itself (unless, of course, you like catching exceptions for the heck of it).

Thread.Sleep

Overloads Public Shared Sub Sleep(Integer)
Overloads Public Shared Sub Sleep(TimeSpan)

When a thread is in the Running state, it can call the Sleep method of the Thread class. The Sleep method has two distinct uses. First, you can use it to put your thread into the WaitSleepJoin state for a specified period of time. By specifying an amount of time in milliseconds or specifying a TimeSpan, you cause the operating system to put the thread on an inactive list. This is the most efficient way to introduce delays because it consumes the least amount of resources and the operating system will take care of resuming your thread when the specified time period has elapsed.

The second use of the Sleep method is to surrender the thread's execution time without actually having it go into the WaitSleepJoin state. By specifying 0 milliseconds or TimeSpan.Zero, you tell the underlying operating system that you want to give other threads a chance at the processor. There are advantages to doing this, especially for long-running or processor-intensive tasks. The following example demonstrates both uses of Thread.Sleep:

' Surrenders execution of the thread.
' Does not cause the current thread to go into the WaitSleepJoin state.
Thread.Sleep(0)
Thread.Sleep(TimeSpan.Zero)
 
' Surrenders execution of the thread.
' Causes the current thread to go into the WaitSleepJoin state.
Thread.Sleep(100)
Thread.Sleep(New TimeSpan(0, 0, 0, 0, 100))

Thread.SpinWait

Public Shared Sub SpinWait(ByVal iterations As Integer)

SpinWait is almost the polar opposite of the Sleep method. It delays the execution of your thread for a specific number of iterations, but it does so by spinning. In other words, your thread remains executing rather than marking the thread inactive for a specific period of time. It's the same as creating your own loop that runs continuously until the counter runs out.

This does not mean that your thread will not surrender its execution time on the processor. The operating system will continue to switch between threads, but your thread will spend x amount of its execution time spinning and doing nothing productive.

Thread.Suspend

Public Sub Suspend()

You can call Suspend on a thread that's in the Running state. This will force the thread into the Suspended state. You can call Suspend on a thread even if that thread is already in the Suspended state. The CLR will not cause an exception. It will figure that you're getting what you want anyway, so why bother you? Calling Suspend when the thread is in any other state than Running or Suspended will cause a ThreadStateException to be generated.

If you're calling Suspend on another thread, the CLR will attempt to suspend that thread in an appropriate place. This can lead to unexpected behavior because the CLR decides where to suspend the thread and it might not happen where you expect.

If you call Suspend on the current thread, the CLR will immediately put your thread into the Suspended state. This actually provides an argument in favor of calling Suspend only on the owner thread and not on other thread instances. By doing this, you can ensure that a thread will always suspend at a predictable location instead of potentially anywhere in its code.

Thread.Resume

Public Sub Resume()

When a thread is in the Suspended state, another thread can call its Resume method. This will cause the thread to move from the Suspended state to the Running state. If a thread is in the Suspended state, it can be resumed only by another, active thread. If no other thread has a reference to the suspended thread, the thread can never be reactivated.

Thread.Abort

Overloads Public Sub Abort()
Overloads Public Sub Abort(Object)

The Abort method has some interesting behaviors. First, calling Abort causes a ThreadAbortException to be generated on the destination thread. It is up to that thread to handle the consequences of that exception, especially clean-up. To handle the ThreadAbortException, you should include exception-handling code in your thread's main Run method. This should also highlight the importance of appropriate exception handling, or at least the use of the Try.Finally syntax to ensure that file handles and database connections are cleaned up reliably. To that end, your thread's Run method might look like this example:

Private Sub Run
   Try
      ' Do your stuff here
   Catch ex As ThreadAbortException
      ' Deal with the exception
   Finally
      ' Clean up your resources here
   End Try
End Sub

Thread.Join

Overloads Public Sub Join()
Overloads Public Function Join(Integer) As Boolean
Overloads Public Function Join(TimeSpan) As Boolean

Join is the simplest synchronization construct available to developers. When you call the Join method on a thread, the calling thread will enter the WaitSleepJoin state and will stay there until the called thread has completed (is in the Stopped or Aborted state). This allows threads to block execution pending the completion of other threads in the system. The following example shows how this might be used:

Sub Main()
   Dim t1 As New Thread(AddressOf Thread1Method)
   Dim t2 As New Thread(AddressOf Thread2Method)
 
   ' Start both threads
   t1.Start()
   t2.Start()
 
   ' Wait for both threads to complete
   t1.Join()
   t2.Join()
 
   ' You are guaranteed at this point that both threads have completed
End Sub

If a thread has already completed, calling Join will have no effect and your application will proceed as normal.

Thread.Interrupt

You can call the Interrupt method on another thread to cause it to exit the WaitSleepJoin state. If you call this method while the thread is running, the next time the referenced thread enters the WaitSleepJoin state it will be immediately set back to Running. I don't recommend this as a common practice—it is appropriate only in the rarest of cases.

Tying It All Together

Now you've seen how to manage a thread through its life cycle. We should spend a little time talking about thread execution and how to tailor your threads to make them run more efficiently and play well with others. Consider the following method:

Public Done As Boolean = False
 
Public Sub MyLongRunningMethod()
   While Not Done
   End While
   
   Console.WriteLine("Done")
End Sub

This is an example of a completely horrendous programming practice. This code will cause the thread to loop interminably while checking a variable that might take minutes, seconds, or even hours to change. This will impose a heavy processing load on a system without achieving anything. Obviously, there should be a better way, and there is. You can use the Thread.Sleep method to tell the system to put your thread out of the active thread list until a certain time period has elapsed. Alternatively, you can use the Thread.Sleep method to give other threads a chance to execute (still keeping your thread active, of course). This lends itself to two scenarios. The first, like our example above, allows you to use processing resources more efficiently by telling the system how long you can wait before you need to continue. We can recast our previous example as follows:

Public Sub MyLongRunningMethod()
   While Not Done
      Thread.Sleep(100) 'Sleep for ~100 milliseconds
   End While
   
   Console.WriteLine("Done")
End Sub

Instead of running this thread continuously and constantly consuming processing resources, we can efficiently poll the status of the done variable 10 times a second.

Now that you know how to manage threads by encapsulating them in classes, let's look at how to make threads work together. What if they need access to a shared resource? What if there are dependencies? How can this all be managed? This is where thread synchronization comes into play.


Next



Last Updated: October 2, 2002
Top of Page