Developers Workshop to COM and ATL 3.0

 < Free Open Study > 


As you know, the 32-bit Windows OS manages processes. For each EXE loaded into memory, the operating system creates a separate and isolated memory partition (process) for use during its lifetime. By default, every process has at least one main thread which is the entry point for the program-known as WinMain() in a traditional Windows application or main() in a console application. A thread is a path of execution within a process, and is owned by the same process during its entire lifetime.

Many developers are completely happy to write software containing a single path of execution within the process, and do so quite successfully. This developer's life is generally quite peaceful, as he or she is unconcerned with ensuring the program's data is thread safe given the fact that there is only one thread to worry about at any given time. On the down side, a single-threaded application can be a bit unresponsive to user input requests if that single thread is performing a complex operation (such as printing out a lengthy text file).

Other developers demand more work from a process. Under Win32, it is possible for a developer to create multiple threads within a single process, using a handful of thread API functions such as CreateThread(). Each thread becomes a unique path of execution in the process, and has concurrent access to all data in that process. As you may have guessed, developers typically create additional threads in a process to help improve the program's overall responsiveness.

A thread savvy developer may create a background worker thread to perform a labor-intensive calculation (again, such as printing a large text file). As this secondary thread is churning away, the main thread is still responsive to user interaction, which gives the entire process the potential of delivering greater performance. However, this is only a possibility. Too many threads in a single process can actually degrade performance, as the CPU must switch between the active threads in the process (which takes time).

Multithreading is often a simple illusion provided by the operating system. Machines that host a single CPU do not have the ability to literally handle multiple threads at the same exact time. Rather, a single CPU will execute one thread for a unit of time (called a time slice) based on the thread's priority level. When a thread's time slice is up, the existing thread is suspended to allow the other thread to perform its business. In order for a thread to remember what was happening before it was kicked out of the way, each thread is given the ability to write to Thread Local Storage (TLS) and provided a separate call stack, as illustrated by Figure 7-3:

Figure 7-3: Each thread in a process receives local storage but shares global data.

The Problem of Concurrency and Thread Synchronization

Machines supporting multiple CPUs can enjoy very responsive multithreaded programs, as threads can be assigned to individual CPUs by the operating system, as opposed to time slices on a single CPU. Machines with a single CPU will have threads swapped around by the Windows' thread scheduler. Beyond taking time, the process of switching between threads can cause additional problems. For example, assume a given thread is accessing a point of data, and in the process begins to modify it. Now assume that the first thread is told to wait, to allow another thread to access the same point of data. If the first thread was not finished with its task, the second thread may be modifying data that is in an unstable state.

To protect the application's data from corruption, the developer must make use of any number of Win32 threading primitives such as critical sections, mutexes, or semaphores to synchronize access to shared data. A common primitive (and the default provided by ATL) is the critical section, represented by the CRITICAL_SECTION structure.

When using this approach to make blocks of code thread safe, you must initialize and terminate your CRITICAL_SECTION using Win32 API thread calls. Once the critical section is ready to go, you enter the initialized section, perform any calculations, and exit the critical section. As an example, assume you wish to write a thread-safe function that operates on a private member variable using a CRITICAL_SECTION. You may write code as so:

// This member function adjusts a private integer in a thread-safe // manner using a CRITICAL_SECTION. STDMETHODIMP CCoClass::FooBar() { CRITICAL_SECTION cs; InitializeCriticalSection(&cs); EnterCriticalSection(&cs); m_theInt = m_theInt + 10; // Thread safe! if(m_theInt >= 3000) // Thread safe! m_theInt = 0; // I'm still thread safe! LeaveCriticalSection(&cs); DeleteCriticalSection(&cs); return S_OK; }

Of course, you could declare a private CRITICAL_SECTION data member for use by the entire class, but I'm sure you get the general idea.

So, as we have thread loving and thread avoiding programmers in the world, we end up with single-threaded and multithreaded applications. Single-threaded applications are typically easier to implement, as the process's data is inherently thread safe.

Multithreaded applications are tougher to engineer, as numerous threads can operate on the application's data at the same time. Unless the developer has accounted for this possibility using threading primitives (such as the CRITICAL_SECTION), the program may end up with a good amount of data corruption.


 < Free Open Study > 

Категории