Threads are a means for improving the responsiveness of your application, and for allowing your application to perform more than one operation at the same time. With ACS Threads framework classes, a developer can add threads to their applications with only a modest effort.
The architecture is a simplified version of the Taligent CommonPoint threads framework, which supported Mach threads in its original implementation. The ACS version supports the Mac OS Thread Manager and MP in the initial release, along with pre-release Win32 support.
A summary of Threads features supported in the initial release include:
The following features are planned or under investigation for future releases:
ACS Threads requires the ThreadsLib and Multiprocessing API Library extensions. These extensions are a standard part of Mac OS 7.6 and newer. To use ACS Threads in your application, you must set the qWantsThreadManager flag to 1. If you want to additionally use multiprocessor threads, you must set the qWantsMP flag to 1. Finer-grained control over features are controlled using other flags which are explained in ThreadSwitches_AC.h.
This section describes high-level classes typically used by the developer to implement threads.
CThread |
This is the class the developer will subclass to implement threads, to override the key function called Run(). Other functions include Prepare() which is called after the class is constructed but before the thread is executed. RequestShutdown() and ShutdownRequested() methods (the latter to be called in your override of Run()) and a CAbstractThreadContext accessor (see below) are also provided. A Yield method is provided which can be used to yield the current thread to other threads awaiting execution. The Yield method is very important to call in a cooperative multitasking environment such as the Mac OS. |
CThreadRunner |
The developer typically will create an instance of this class, passing in the thread instance to run. This class provides methods for the client of a thread to query its state, suspend, stop, and join the thread (e.g., wait until the thread has stopped). |
CWhileSynchronized |
This stack-based class provides a convenient C++ mechanism for thread synchronization. The constructor accepts a CMonitorLock, acquires the lock on the thread, and releases the lock on the thread when the class exits scope (e.g., when its destructor is called). |
CMonitorLock |
This class provides a thread with its synchronization so that a function scope which has a lock acquired cannot be entered more than once until the lock has been released. The developer will use one or more instances of this class as a parameter to CWhileSynchronized. This class is also known as a semaphore or mutex. |
CAbstractThreadContext |
This refcounted class implements the lower-level details for implementing and managing a thread. It provides functions for getting and setting the thread's schedule object (see subsystem), for starting, stopping, and yielding, and also implements some of the low level details for running and synchronizing a thread. |
The ACS Threads subsystem is designed to allow variations in threads implementations without affecting developer code significantly.
CAbstractThreadSchedule |
This class will eventually implement scheduling of objects. The initial implementation provides factory functions for creating a thread context and monitor locks. |
CAbstractMonitorLock |
This class provides the interface for monitor locks. Subclasses include CMPMonitorLock, CThreadMgrMonitorLock and CWinMonitorLock. The class CMonitorLock is typedefed to match the build settings (e.g., MP, ThreadMgr, Win32) and utilizes an internal lock to match the threads runtime environment. |
CMonitorCondition |
This class is used by CThreadRunner to manage the starting and stopping of a thread. |
CAbstractThreadContext |
In addition to the developer-accessible features described in the previous section, this class also implements static functions for the current context . It also manages a yield thread to enable preemptive tasks to yield to cooperative tasks and other processes. |
CThreadedIdleEngine |
This is a subclass of the ACS pattern suite class CIdleEngine, which is used by the ACS Network suite to allow threaded idle time during network operations. |
CThreadedReactorFactory |
If you call InitThreads_AC, a threaded reactor factory class will be installed in the reactor pattern implementation. Whenever a client requests the construction of a CReactor, the returned reactor will automatically be threaded. More specifically, a CReactor will be instantiated with a CThreadedIdleEngine instead of a regular CIdleEngine. This is how the ACS Networking suite transparently supports threads without requiring any developer interaction. |
CTime |
This class and its derived classes provide automatic conversion between very small time units. |
To implement a thread, subclass CThread, implement your Run method, and call key methods of the CThreadRunner class from within your subclass or from the main application thread. The MacApp SimpleThreads example illustrates how to implement a thread and how to start and stop the thread. This section explains how to implement a thread; starting and stopping a thread is explained in the next section.
A TSimpleThread subclass thread is implemented whose Run method continuously calls a MacApp view to perform drawing of a spirograph shape, until the thread is told to stop by a call to RequestShutdown from the main application thread.
Here is the complete interface for CSimpleThread:
class TSimpleThread : public CThread_AC { public: TSimpleThread(TSimpleThreadsView* theView = NULL); // Constructor virtual ~TSimpleThread(); // Destructor virtual void Run(); // Represents the entry point for the code to be run in the thread // Overriden to provide the code the thread is to execute. private: TSimpleThreadsView* fView; }; class TSimpleThreadView : public TView { // etc. public: void DrawSpirograph(int& iter); // Draws a portion of a spirograph. };
The key method being implemented by TSimpleThread is the Run method. The constructor and destructor contain no code in this example
void TSimpleThread::Run() { int iter = 0; if (fView != NULL) { while (!ShutdownRequested()) { fView->DrawSpirograph(iter); Yield(); } } }
The Run method repeatedly calls DrawSpirograph on the TView subclass (a method implemented in the subclass to perform direct drawing), calls Yield after each call to DrawSpirograph in order to be a responsive and cooperative multitasking thread, and checks ShutdownRequested. The DrawSpirograph method uses an iter parameter to help that class keep track of which iteration it is on when drawing--'iter' and the code in DrawSpirograph are implementation details.
To create the thread and start it running, the main application thread must create the thread and a thread runner for it. To stop a thread that runs continuously and that calls !ShutdownRequested, a reference to the thread runner must be stored so that its RequestShutdown method can be called to stop the thread. Here are the methods and member data of TView which implement starting and stopping a thread:
class TSimpleThreadView : public TView { // etc. public: void StartThreadExample(); // Creates a thread and runs it, storing its thread runner in fThread. void StopThreadExample(); // Stops fThread by requesting a shutdown and joining the thread (waiting for it to stop). private: CThreadRunner_AC* fThread; // Runner for the thread that is calling DrawSpirograph. };
To create the thread and start it running, the main application thread uses the following from within the StartThreadExample method:
void TSimpleThreadView::StartThreadExample() { if (fThread == NULL) fThread = TH_new CThreadRunner_AC(TH_new TSimpleThread(this)); // etc. }
The simple thread is constructed with a reference to the TSimpleThreadView, and handed off to the constructor of a CThreadRunner. The CThreadRunner will create the thread's scheduler and context, call its Prepare method and then start the thread which in turn will cause its Run method to get called. The CThreadRunner is stored as member data so that the thread can be stopped when the view is destructed, or when the main application thread requests that the thread be stopped:
void TSimpleThreadView::StopThreadExample() { if (fThread != NULL) { delete fThread; fThread = NULL; // etc. } }
In this example, the thread runner is asked to shut down the executing thread by deleting it. The thread runner's destructor will call Join(), which in turn will call the executing thread's RequestShutdown method, informing the thread to shut down (it will exit its Run method's while loop). The Join method will wait until the thread has stopped running. Join in turn calls Yield on the curent thread, so that the thread which has a request to stop gets execution time and is able to exit the while loop of its Run method. Once the thread has exited Run, its CThreadRunner will stop and clean up the thread, and control will return to the main application thread.
Few threads are as straightforward as the one in the SimpleThreads example. The primary issue for threads is managing the access and manipulation of shared data from within multiple threads that may possibly be executing simultaneously as on a multi-processor, or near-simultaneously as in a pre-emptive multitasking environment. The MacApp FracAppMP example illustrates this issue.
The FracAppMP example implements an engine class which creates more than one thread to compute and render a fractal image. Since each thread may access the member data of the engine class, and since the example implements multi-processor threads (explained in more detail below), more than one thread may be executing code that records the updating of the current location in the fractal and the code that checks if the calculation has completed. In order to guarantee that only one thread is executing while these steps are being made, the CMonitorLock and CWhileSynchronized classes are used.
Since the CMonitorLock class can manage the lock state of multiple threads simultaneously, the FracApp engine implements an instance of CMonitorLock as member data in the engine class:
class CFracAppEngine { // etc. public: virtual bool CalcCity(); // Calculates a little bit of the fractal protected: CPoint_AC fCurrentLocation; // holds current positon of the "pen". CRect_AC fCalcRect; // rectangle surrounding the window it was built for CMonitorLock_AC fLock; // Concurrent access control for fields. };
The CalcCity method uses the CMonitorLock when it reaches crticial code for updating the current location:
bool CFracAppEngine::CalcCity() { // etc. // Calculate the fractal as we go. Do next pixel here, based on the state saved // in fCurrentLocation. When done, those variables are updated to go to the next // location to do. bool isFinished = false; // Assume document is not done. CRect_AC tempRect = fCalcRect; { CWhileSynchronized_AC entry(&fLock); tempRect.top = fCurrentLocation.v; ++fCurrentLocation.v; // Up the counter of next row to do. } // etc. return isFinished; }
In this example, there is an access and update of the fCurrentLocation data member. This code above is basically saying "I am now going to work on the current uncalculated part of the fractal, any other thread should work on the next part." If more than one thread is executing this code simultaneously as may happen on a multiprocessor, there is the possibility that more than one thread will think they can work on the same part of the fractal, and part of the fractal may be skipped because the threads will increment the location past an uncomputed part before any thread has had a chance to work on it.
To avoid this situation, the CMonitorLock instance of the CFracAppEngine is handed to a stack-based CWhileSynchronized object, whose constructor will call the Acquire method of the monitor lock, and whose destructor will call the Release method. The thread which first becomes acquired gets to continue running, and any other threads which attempt to become acquired in the same section of code are suspended in a queue until the release method is called on the monitor lock by the thread that is executing. The brackets around CWhileSynchronized, the location access, and the location update are used to ensure that the destructor is called immediately after the update has completed, so that other threads that may have become suspended can continue running with a minimum interruption.
The initial release of ACS Threads can be made multi-processor (MP) aware by setting the qWantsMP switch to 1, but your threads will not be automatically MP-savvy, nor will they be automatically pre-emptive. This behavior is by design, in part. Pre-emptive and MP threads support require additional responsibilities in your threaded code that you may not choose to to accept initially. Your application code may also choose to implement a combination of cooperative and pre-emptive and MP threads.
To implement a pre-emptive or MP thread, override the GetThreadContext method of your CThread subclass and return an CMPThreadContext instance instead of the default, or pass the CThreadManagerSchedule constructor a thread style for pre-emptive.
FracAppMP provides an example for creating different kinds of threads by overriding the GetThreadSchedule method:
class TFracThread : public CThread_AC { // etc. public: virtual CAbstractThreadSchedule_AC* GetThreadSchedule(); // Returns the CThreadSchedule_AC object to be used in the thread. // This is overridden to provide MP threads. private: TFracAppEngine* fEngine; static CAutoPtr_AC<CAbstractThreadSchedule_AC> fSchedule; };
TFracAppEngine stores a preferred thread style data member: representing the type of thread or task scheduling desired:
ThreadStyle fThreadStyle;
Valid values are kCooperativeThread or kPreemptiveThread, as defined in Threads.h. When the preemptive style is selected on PowerPC, MP tasks are used instead of thread manager threads. Pre-emptive thread manager threads on Mac OS are currently supported on 68K only, but this may change in a future Mac OS release.
The GetThreadSchedule method is implemented as follows:
CAbstractThreadSchedule_AC *TFracThread::GetThreadSchedule() { if (fSchedule.IsNULL()) { #if qWantsMP if (fEngine->fThreadStyle == kPreemptiveThread) { fSchedule = TH_new CMPSchedule_AC(); } else { fSchedule = TH_new CThreadManagerSchedule_AC(); } #else fSchedule = TH_new CThreadManagerSchedule_AC(fEngine->fThreadStyle); #endif } return fSchedule; }
In the case where the application is compiled for MP, a CMPSchedule class must be explicitly created; in the case where the application is not, the parameter to CThreadManagerSchedule can be specified as pre-emptive or it may default to cooperative.
Last updated: 07/13/1997