Chapter 10: Threads, AppDomains, and Processes


[I]f you want your application to benefit from the continued exponential throughput advances in new processors, it will need to be a well-written concurrent (usually multi-threaded) application.
— Herb Sutter

Overview

Threading, or more generally concurrency and parallelism, offers a way to execute multiple units of code simultaneously. The Windows platform has evolved over time from offering only sequential execution (e.g., in MS-DOS with quasi-multitasking in the form of terminate and stay resident programs [TSRs]), to cooperative multitasking in 16-bit Windows, to real multi-threading in 32-bit Windows. Threads are OS primitives that enable you to partition logically independent tasks which are then scheduled for execution by the OS in a manner that maximizes utilization of physical resources. On real parallel machines — e.g., multiprocessor, multi-core, and/or hyper-threaded (HT) architectures — this can result in code executing genuinely simultaneously. On other machines, the OS scheduling algorithm simulates parallel execution by constantly switching between runnable threads after permitting each to run for a finite time-slice. At the most fundamental level, this enables multiple processes to be running at once; this is because each process uses at least one thread. Drilling deeper, individual processes are able to partition work into multiple threads of execution.

There are two general categories of situations in which this behavior is desirable: for responsiveness and for performance. A program will frequently block execution so that it may perform I/O, for example reading the disk, communicating with a network endpoint, and the like. But UIs work by processing messages enqueued onto a per-UI-thread message queue. Several types of blocking cause the UI's message pump to run, but others do not. This can cause messages (e.g., WM_CLOSE, WM_PAINT, etc.) to get clogged in the queue until the I/O operation completes. For lengthy operations, this can lead to an unresponsive UI. (If you've ever seen your favorite program's title bar change to " (Not Responding)" you know what I mean.) The second reason, for performance, can be used to take advantage of real hardware. Certain algorithms are conducive to execution in parallel. Splitting such operations into multiple threads can enable a speedup when run in parallel when compared to sequential execution.

To make such scenarios possible, among others, the .NET Framework offers a variety of asynchronous programming primitives and techniques. There are three basic levels of isolation and concurrent execution that we will focus on. From coarser-grained to finer-grained units, they are processes, application domains (referred to as AppDomains subsequently), and threads. Remember, threads are the only real units of concurrency; the others are forms of isolation and can be used to group logically related threads. Figure 10-1 illustrates the relationship between these three units of execution.

image from book
Figure 10-1: Isolation between processes, AppDomains, and threads.

Processes in the .NET Framework correspond one to one with a process in Windows. A process's primary purpose is to manage per-program resources; this includes a shared virtual address space among all threads running in the process, a HANDLE table, a shared set of loaded DLLs (mapped into the same address space), and a variety of other process-wide data stored in the Process Environment Block (PEB). Problems with one process will not normally affect other processes because of this isolation. However, because of interprocess communication and machine-wide shared resources — such as files, memory mapped I/O, and named kernel objects — it is not uncommon for one process to interfere with another.

A single managed process (one which has loaded the CLR) can contain any number of AppDomains. The simplest only contain one. AppDomains are a finer-grained level of isolation between logical components inside the same process, for both reliability and security purposes. They are a good alternative to process-level isolation because of the relative inexpensiveness of creating, managing, and switching execution and due to the level of resource sharing among AppDomains in a process. AppDomains within a process are not entirely isolated. While they do generally load their own assemblies and have their own copies of static variables, for example, resource leaks from one AppDomain can affect another and HANDLE-thieving security holes are possible because AppDomains share a single per-process handletable. AppDomains can be shut down individually while still keeping the enclosing process alive.

As noted previously, a thread is the real unit of execution on Windows. It too has a set of its own state. Perhaps most importantly, each thread has its own stack (generally 1MB worth), which is used for creating activation frames and executing methods. Threads also have their own Thread Environment Block (TEB) in which thread-local state may be stored, such as Thread Local Storage (TLS) for example. Each thread is scheduled based on a combination of the process priority class and the thread's priority. A thread's execution can be preempted if its time-slice expires while executing or if a higher-priority thread becomes runnable; when a thread is preempted the OS performs a context switch, which saves register state for the outgoing thread and restores such state for the incoming thread.

A managed thread is simply one that has executed managed code at least once, in which case it will have been permanently stamped with a CLR data structure in TLS. A thread can only belong to a single process. But if a thread has stack in multiple AppDomains — meaning its current stack has traversed more than one AppDomain — a thread can belong to more than one AppDomain simultaneously. It's important also to realize that a managed thread does not necessarily map to a physical OS thread; using the hosting IHostTaskManager and IHostSyncManager APIs, for instance, a host can instead choose to map threads down to another primitive, such as Windows fibers. We'll see some of the implications of this later in this chapter.

A powerful set of APIs offer fine-grained control over each of these units of isolation, permitting construction, starting, monitoring, and stopping of processes, AppDomains, and threads explicitly. All of this happens implicitly for simple cases. With the power to control such things comes responsibility to organize and enforce consistent data-sharing practices between threads, using synchronization and locking primitives such as monitors, mutexes, and semaphores. Without these constructs, unexpected (and undesirable) behavior will result, one example of which is a race condition. The introduction of locking can surface additional bugs, too, such as deadlocks. Both are detailed later in this chapter and can be among the nastiest type of bug to fix. Each can result in corrupt program state, hangs, or undefined behavior. We will see later on how to avoid these situations.




Professional. NET Framework 2.0
Professional .NET Framework 2.0 (Programmer to Programmer)
ISBN: 0764571354
EAN: 2147483647
Year: N/A
Pages: 116
Authors: Joe Duffy

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net