|
Chapter 25 "Multi-Threaded Programming"In this chapter I will touch upon the difficult topic of multi-threaded programming including concurrency, locks and threads. I will present an approach to coding safe multi-thread code using inheritance. In a nutshell, I suggest a division of labor in which all "critical sections" are encapsulated into an abstract base class. This base class encapsulates or hides the complexity of thread safe access and operations on shared variables. IntroductionProcesses and ThreadsA thread is an "identifiable flow of control within a process with its own stack" where a process is a "container of threads within a protected address space." The most simple of programs run in a single thread of execution within a single process. Alternatively "A process is a collection of virtual memory space, code, data, and system resources. A thread is code that is to be serially executed within a process. A processor executes threads, not processes, so each 32-bit application has at least one process, and a process always has at least one thread of execution, known as the primary thread. A process can have multiple threads in addition to the primary thread. Prior to the introduction of multiple threads of execution, applications were all designed to run on a single thread of execution." MSDN Process, Threads and Apartments Single threaded programming is tempting in its simplicity, but can result an unresponsive user interface. Launching a long running task in an asynchronous separate thread can allow the user to continue to interact with the user interface or perform other tasks while the asynchronous thread is working. This "magic" can be accomplished by "slicing" or dividing cpu time between different threads or by providing multiple cpus within a single computer. Races and ConcurrencyOne of the downsides of multi-threaded program is the unexpected side effects of multiple threads operating on shared data leading to a race condition. As defined on MDSN: "A race condition is a bug that occurs when the outcome of a program depends on which of two or more threads reaches a particular block of code first. Running the program many times produces different results, and the result of any given run cannot be predicted." The study of concurrency "is concerned with the sharing of common resources between computations which executed overlapped in time. For instance, the simple act of incrementing a shared data value by one is problematic as it is possible for two threads to read the value of a shared data member almost "simultaneously" such that both threads write the same "incremented" value. Thread SafetyThe degree of thread safety depends on the demands of the environment such that a data member may be thread safe only in:
Thread safety in a multi-threaded environment can be obtained by techniques such as:
Single Threaded EnvironmentAn example of an environment with thread affinity is Windows Forms programming. If you launch a separate thread in a Windows Form program, you must marshal the call back to the Win Forms thread when the asynchronous thread returns. According to MSDN: "Windows Forms uses the single-threaded apartment (STA) model because Windows Forms is based on native Win32 windows that are inherently apartment-threaded. The STA model implies that a window can be created on any thread, but it cannot switch threads once created, and all function calls to it must occur on its creation thread. Outside Windows Forms, classes in the .NET Framework use the free threading model." Here is an example of code that marshals a call back to the WinForm thread: public void
Form1_OnThreadedProcessEvent(object
worker, ThreadedProcessEventArgs e) Inter Process MarshallingAn example of a thread safe environment that marshals thread access is a call to an out of process COM dll. Although threads within a COM process may be multi-threaded or single threaded, COM hides this complexity by providing an environment in which calls to a COM process are adapted to the caller's thread safety. Processes, Threads, and Apartments "The threading models in COM provide the mechanism for clients and servers that use different threading architectures to work together. Calls among objects with different threading models in different processes are naturally supported. From the perspective of the calling object, all calls to objects outside a process behave identically, no matter how the object being called is threaded. Likewise, from the perspective of the object being called, arriving calls behave identically, regardless of the threading model of the caller." "Interaction between a client and an out-of-process object is straightforward, even when they use different threading models because the client and object are in different processes. COM, interposed between the client and the server, can provide the code for the threading models to interoperate, using standard marshaling and RPC. For example, if a single-threaded object is called simultaneously by multiple free-threaded clients, the calls will be synchronized by COM by placing corresponding window messages in the server's message queue. The object's apartment will receive one call each time it retrieves and dispatches messages. However, some care must be taken to ensure that in-process servers interact properly with their clients." Multi Threaded EnvironmentAn example of code that is safe in a multi-threaded environment is the .NET framework (outside of Windows Forms) which is free threaded. In a multi-threaded environment, thread safety can be achieved using thread local storage, immutable objects/variables, atomicity and explicit locking. Thread Safety Using Local Variables or Immutable Objects and VariablesOne way to write thread safe code in a multi-threaded environment is to use only local variables. Calls to methods that access only local variables are thread safe since each call to the method gets its own stack frame and set of local variables. Another way to write multi-thread safe code is to use read only data or immutable objects. Since the values of immutable objects cannot be changed, they can be passed to or acted upon by multiple threads without concern for concurrency conflicts. Thread Safety Using Thread Local Variables The CLR supports the concept of variables that are local to a thread and stored in memory owned by the thread as in: [ThreadStatic] static int myThreadLocalInt; You should not initialize the value since it is set only once, not once per thread. For dynamic thread local storage see Thread.AllocateNamedDataSlot. Atomic Safety Using Interlocked and VolatileAn alternative way to achieve multi-thread safety is by providing atomic actions. According to http://en.wikipedia.org/wiki/Atomicity An atomic operation is one that is indivisible: either it completes fully, or has no lasting effect... An "atomic" operation has the additional property that none of its effects are visible until after it completes. That is, that there are no intermediate states visible to other threads. An example of atomic actions are the Interlocked methods in C# such as Interlocked.Increment(Int32). Another example of atomicity is the use of volatile variables. "Using the volatile modifier ensures that one thread retrieves the most up-to-date value written by another thread." Unfortunately, in actual practice, operations on volatile variables may not be thread safe in a multi-processor environment. Thread Safety Using Explicit LocksThe third method to achieve multi-thread safety is through the use of explicit locking. Locking forces threads to queue up to the locked segment so that only one thread is allowed to enter the critical section at a time. For instance, here is some sample code that protects the bool flag isStarted:
which expands to:
A Twisted Proposal Using InheritanceWith all this complexity, it is no wonder that developers shy away from multi-threaded programming. In attempt to simplify this task I have applied inheritance to the task of writing a thread safe class. In this approach, all calls to critical values are encapsulated into an abstract base class. All of the thread safety issues are hidden within the abstract base class. Business logic is added in the concrete class that extends from the abstract thread safe base class. Here is some sample code in which all critical sections are encapsulated by the abstract base class AccountBase. This class implements a read many, one writer logic. abstract public class AccountBase { // the concrete class has _no_ direct access to this private // data member // ASSERT balance >=0; private float balance=0; private readonly object syncLock= new Object(); // provide thread safe data manipulation methods and properties here // ASSERT increment >=0 protected void _Deposit(float increment) { if (increment <0) {throw new ArgumentOutOfRangeException();} lock(syncLock) { balance += increment; } } // ASSERT decrement =>0 // caller cannot know if decrement > balance // at runtime so we return false if dec > bal // return true if successful, re: balance >= 0 // return false if fails due to decrement > balance protected bool _Withdraw(float decrement) { if (decrement <0) {throw new ArgumentOutOfRangeException();} lock(syncLock) { if (decrement > balance) {return false;} else { balance -= decrement; return true; } } } // get only property // no lock here // multiple read, single write behavior public float Balance { get {return balance;} } } Business logic can be implemented in the concrete class SavingsAccount which extends from AccountBase. // concrete class // business rules go here public class SavingsAccount : AccountBase { /// <summary> /// The main entry point for the application. /// </summary> [STAThread] static void Main(string[] args) { // // TODO: Add code to start application here // new SavingsAccount(); System.Console.ReadLine(); } public bool Deposit(float deposit) { if (deposit<0) { System.Console.WriteLine("Invalid entry."); return false; } else { _Deposit(deposit); return true; } } public bool Withdraw(float withdrawal) { if (withdrawal<0) { System.Console.WriteLine("Invalid entry."); return false; } else { if (_Withdraw(withdrawal)) { return true; } else { System.Console.WriteLine("Inadequate funds."); return false; } } } The writer of the concrete class is freed from the complexity of the code that implements thread safe shared data access and operations. Note: The _Withdraw and _Deposit methods throw an exception if an explicitly stated preconditions is violated, but do _not_ throw an exception if an invalid withdrawal is attempted. Due to the dynamics of a multi-threaded environment, it is not possible for the caller to validate that the account has adequate funds _outside_ of the critical section of code. If at runtime adequate funds are not available, the withdrawal is aborted and the method returns false. protected bool _Withdraw(float decrement) { if (decrement <0) {throw new ArgumentOutOfRangeException();} lock(syncLock) { if (decrement > balance) {return false;} else { balance -= decrement; return true; } } } Separating out the logic for thread safe data access and operations into an abstract base class compartmentalizes or encapsulates this logic into a distinct "code module." Design and implementation of the thread safe abstract class can be assigned to a skilled programmer with an interest in multi-threaded programming and explicit locking. The writer of the concrete class can avoid concurrency concerns by avoiding read/write class variables, using only read only class variables, using only local variables and by passing only immutable objects between function calls. DeadlocksAlthough locking critical sections can prevent concurrency conflicts, it can also cause unintended consequences such as deadlocks. A deadlock may occur when two threads reach a state where neither thread can continue until the other thread completes its task or releases a lock. Learn MoreLearn more about multi threaded programming at Skeet. Here is a sample multi-threaded Win Forms application. Have fun! |
Send mail to
jeff_louie@yahoo.com with questions or
comments about this web site.
Copyright © 2001, 2002, 2003, 2004, 2005, 2006, 2007 ©
|