Multithreaded applications may have several 'threads of execution', each carrying out their own tasks simultaneously. Unless the computer is capable of multi-processing, the threads will not actually be running simultaneously. However, the operating system will give each thread some processor time by switching between them very quickly, so as far as the user and programmer are concerned the threads are running simultaneously.
The operating system does this switching by keeping track of the 'thread contexts'. A thread context is everything that the processor needs to process a thread, including the position in the thread (the instruction pointer) and the position in stack memory for temporary storage (the stack pointer).
You should write your application as a set of threads if it would otherwise need to switch between tasks itself. This is often relevant to applications which need to monitor inputs while carrying out some other task.
This document will explain the concepts and techniques involved. However, you should refer to the Microsoft reference documentation for exact details of the classes and functions mentioned.
A computer may be running several processes, and each of those processes may own more than one thread, each using the memory allocated to the process. Each process starts execution with its 'primary thread'. Any thread within the process can create additional threads within the process. In Windows this is done by sending the ::CreateThread() API function the address of a function which you wish to run in a new thread.
Each process running on the computer has a priority, so that its threads may get more processor time than others. In Windows the following process priorities exist:
REALTIME_PRIORITY_CLASS
HIGH_PRIORITY_CLASS
NORMAL_PRIORITY_CLASS
IDLE_PRIORITY_CLASSThese process priorities are used to calculate thread priorities according to the following scheme:
THREAD_PRIORITY_TIME_CRITICAL Normal for HIGH, NORMAL, or IDLE. Double for REALTIME. THREAD_PRIORITY_HIGHEST = Process Priority + 2. THREAD_PRIORITY_ABOVE_NORMAL = Process Priority +1. THREAD_PRIORITY_NORMAL = Process Priority. THREAD_PRIORITY_BELOW_NORMAL = Process Priority -1. THREAD_PRIORITY_LOWEST = Process Priority -2. THREAD_PRIORITY_IDLE Normal for REALTIME, 1 for HIGH, NORMAL, or IDLE. So in theory it is possible for a high priority thread in a low priority process to have a higher priority than a low priority thread in a high priority process.
Deriving your own CWinThread object
You may derive an object from CWinThread, then call that object's CreateThread() method to start the thread. You should override the InitInstance() and ExitInstance() methods of the class to do any special initializing or terminating of your thread.
You may have the thread carry out tasks in response to messages which you send it, or you can override the CWinThread::Run() method which is called after the thread is created. Note that if you override ::Run() the thread will no longer be able to receive messages.
Threads may have a window. You may create a window in InitInstance() and assign it to CWinThread's m_pMainWnd member variable. All messages for that window will then be sent to that thread. You will have, in effect, created a window running in its own thread. Note that this technique can often lead to poor performance because sending a windows messages may halt the sender until the ::SendMessage() function returns.
Using a controlling function
Alternatively, you may simply use the MFC AfxBeginThread() function to start a thread, passing it the address of a controlling function which you have defined. The function will run in a new thread and that thread will terminate when the function returns. The function which makes the call to AfxBeginThread() will not wait for the controlling function to return. The controlling function must return a status code indicating success or failure. Note that the controlling function must be a global function or a static member, because it may need to outlive the object which created the new thread.
The AfxBeginThread() function also accepts a pointer to an object of your choice. This object is also a parameter in the declaration of your controlling function.
e.g. UINT ExampleControllingFunction(LPVOID pParam);This object can be used to initiate the calling function with parameters. It may also be used to return information, though you would need to check that the thread had finished before using that object.
You may check whether the controlling function has finished by using the GetExitCodeThread() function. This function accepts the dwRetCode parameter, which will be set to STILL_ACTIVE if the thread is still running, or to the controlling function's return code if the thread has finished.
Alternatively you may use the ::WaitForSingleObject(), which will wait for the requested period of time and return WAIT_OBJECT_0 if the thread is finished.
Posting messages
You can post a message to a thread using the CWinThread::PostThreadMessage() method to post a message to the thread represented by that CWinThread object. 'Posting', unlike 'sending' does not halt the thread until a reply is received.
A message being sent between threads in different processes (For instance, one application talking to another) must be registered with the ::RegisterWindowMessage() API function in both processes.
e.g. UINT nExampleMessage = ::RegisterWindowMessage(_T("Example Message"));As long as both programs register the same text the API will return the same ID number.
Receiving messages
The PostThreadMessage() function uses the message map system, so you can use message map macros to catch messages for the thread. Note that the integration of thread messages into the MFC message map system is recent - since MFC4.2b.
You can use the ON_THREAD_MESSAGE() macro to catch messages sent within a process, or ON_REGISTERED_THREAD_MESSAGE() to catch inter-process messages which have been registered as described above.
Threads should not be allowed to access the same object at one time in order prevent corruption of the object and therefore of the program. Also, because a multi-threaded application generally consists of many threads working on the different parts of one problem, you may need some methods to co-ordinate the threads. The Synchronization Objects described below provide simple and standard ways of dealing with these issues.
Critical Sections
To prevent more than one thread from accessing the same object simultaneously, the object can be given a CCriticalSection member variable. Threads which wish to use the object can call the Lock() method of the objects' CCriticalSection before doing so and call Unlock when they are finished. If any other thread tries to call the Lock() function while the object is being used, then that call will not return until the first thread calls Unlock(), effectively stalling the second thread until the object is free to be used. Notice that this technique only works if all threads comply with the Lock()/Unlock() convention.
Locking() an object before using it is referred to as 'acquiring' the resource. Unlocking is referred to as 'releasing' the resource. A resource which is locked is said to be 'signalled', while a resource which is not locked by any thread is 'unsignalled'. If a thread acquires a resource and then fails to release it before terminating then the resource is 'abandoned'.
You must ensure that one thread does not Lock an object and then, by attempting to use a second object, have to wait for another thread which is itself waiting for the first object. This situation would bring the program to a 'deadlock', with both threads waiting for each other. It can generally be avoided by using the following techniques:
- Unlock the most recently locked objects first.
- Ensure that threads lock shared objects in the same order.
- Ensure that a thread which is causing another to wait does not use ::SendMessage() to send a message to the waiting thread, because ::SendMessage() would never return.
Critical Sections can only be used when sharing objects between threads in the same process, because they depend upon calling Lock() directly.
Mutexes
Mutex is short for 'mutually exclusive'. A CMutex object can be used in a similar way to a CCriticalSection object but can be shared across processes. For cross-process sharing you must give the CMutex a name. All processes which share the object must refer to the mutex by the same name.
You can mark your object's CMutex as locked in your object's constructor, and Unlock() it at the end of your constructor. This ensures that other processes cannot use the object before it is properly setup.
Threads which wish to acquire an object can use the CSingleLock or CMultipleLock classes to simplify things. The CSingleLock constructor takes a pointer to an object's mutex. CMultipleLock's constructor takes an array of mutexes. Calling the Lock() and Unlock() member functions of these classes will lock or unlock their mutexes. Both versions use the ::TryEnterCriticalSection API call internally, so you can give both versions a timeout value which will cause their Lock() functions to return a failure if they cannot acquire the object within that time. CMultipleLock::Lock() can also be asked to return when any one of its objects was acquired.
Semaphores
Semaphores are used when a limited supply of resources are being requested by multiple threads. The collection of resources as a whole receives requests from threads and allocates one of its resources per request. If all of the resources are being used, then the thread must wait until one becomes available. For instance, four threads may each wish to make a connection when only two connections can be open at one time.
The object which receives the request has a CSemaphore member object which has an initial count and a maximum count set in its constructor. The count indicates how many resources are still available. Each time the semaphore's Lock() method is called, its count decreases by 1. If Lock() is called when the count is zero it will not return until the count has increased.
As with the mutex, by providing a name in the CSemaphore constructor, you can make the semaphore available across processes.
Events
CEvent objects can be used by a thread to indicate to other threads that a task upon which they depend is finished. It does so by calling the CEvent::SetEvent() method. Calls to CEvent::Lock() by other threads which have been queuing up in the meantime will then return. Note that a CEvent which is set is said to be signalled, and unlike the other synchronization objects, a call to Lock() will return when the CEvent is signalled.
It is possible to create a CEvent object which only releases one of its waiting threads when SetEvent() is called, then marks itself as unsignalled. This is achieved by setting the bManualReset parameter to true in its constructor. Calling PulseEvent() instead of SetEvent() has the same effect.
Copyright © Murray Cumming. Verbatim copying and distribution of this entire article is permitted in any medium, provided this notice is preserved.