C++ Tutorial
Multi-Threaded Programming II
C++ Thread for Win32 - 2020
Multithreaded programs are most of the cases using Java Threading, the POSIX PThreads library, and the Windows API.
In this tutorial on multithreaded, we'll make C++ Thread class hiding the details of thread creation in Pthreads/Win32.
When two or more threads are programmed to be executed concurrently and to work together to perform some task, we call it concurrent program. The OS manages the usage of resources by the program.
Java provides a Thread class and Win32 and Pthreads provide a set of function calls for creating and manipulating threads.
All concurrent programs exhibit behavior that is unpredictable. This creates plethora of challenges for programmers when they write concurrent programs.
#include <iostream> #include <memory> #include <cassert> #include <windows.h> #include <process.h> class Runnable { public: virtual void* run() = 0; virtual ~Runnable() = 0; }; // Pure virtual destructor: function body required Runnable::~Runnable(){}; class Thread { public: Thread(std::auto_ptr<Runnable> run); Thread(); virtual ~Thread(); void start(); void* join(); private: HANDLE hThread; unsigned wThreadID; // runnable object will be deleted automatically std::auto_ptr<Runnable> runnable; Thread(const Thread&); const Thread& operator=(const Thread&); // called when run() completes void setCompleted(); // stores return value from run() void* result; virtual void* run() {return 0;} static unsigned WINAPI startThreadRunnable(LPVOID pVoid); static unsigned WINAPI startThread(LPVOID pVoid); void printError(LPTSTR lpszFunction, LPSTR fileName, int lineNumber); }; Thread::Thread(std::auto_ptr<Runnable> r) : runnable(r) { if(!runnable.get()) printError("Thread(std::auto_ptr<Runnable> r) failed at ", __FILE__, __LINE__); hThread = (HANDLE)_beginthreadex(NULL,0,Thread::startThreadRunnable, (LPVOID)this, CREATE_SUSPENDED, &wThreadID;); if(!hThread) printError("_beginthreadex failed at ",__FILE__, __LINE__); } Thread::Thread() : runnable(NULL) { hThread = (HANDLE)_beginthreadex(NULL,0,Thread::startThread, (LPVOID)this, CREATE_SUSPENDED, &wThreadID;); if(!hThread) printError("_beginthreadex failed at ",__FILE__, __LINE__); } unsigned WINAPI Thread::startThreadRunnable(LPVOID pVoid) { Thread* runnableThread = static_cast<Thread*>(pVoid); runnableThread->result = runnableThread->runnable->run(); runnableThread->setCompleted(); return reinterpret_cast<unsigned>(runnableThread->result); } unsigned WINAPI Thread::startThread(LPVOID pVoid) { Thread* aThread = static_cast<Thread*>(pVoid); aThread->result = aThread->run(); aThread->setCompleted(); return reinterpret_cast<unsigned>(aThread->result); } Thread::~Thread() { if(wThreadID != GetCurrentThreadId()) { DWORD rc = CloseHandle(hThread); if(!rc) printError ("CloseHandle failed at ",__FILE__, __LINE__); } } void Thread::start() { assert(hThread); DWORD rc = ResumeThread(hThread); // thread created is in suspended state, // so this starts it running if(!rc) printError ("ResumeThread failed at ",__FILE__, __LINE__); } void* Thread::join() { // A thread calling T.join() waits until thread T completes. return result; } void Thread::setCompleted() { // Notify any threads that are waiting in join() } void Thread::printError(LPSTR lpszFunction, LPSTR fileName, int lineNumber) { TCHAR szBuf[256]; LPSTR lpErrorBuf; DWORD errorCode=GetLastError(); FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER|| FORMAT_MESSAGE_FROM_SYSTEM, NULL, errorCode, MAKELANGID(LANG_NEUTRAL,SUBLANG_DEFAULT), (LPTSTR)&lpErrorBuf;, 0, NULL); wsprintf(szBuf,"%s failed at line %d in %s with error %d: %s", lpszFunction, lineNumber, fileName, errorCode, lpErrorBuf); DWORD numWritten; WriteFile(GetStdHandle(STD_ERROR_HANDLE), szBuf, strlen(reinterpret_cast <const char *> (szBuf)), &numWritten;, FALSE); LocalFree(lpErrorBuf); exit(errorCode); } class simpleRunnable: public Runnable { public: simpleRunnable(int ID) : myID(ID) {} virtual void* run() { std::cout << "Thread " << myID << " is running" << std::endl; return reinterpret_cast<void*>(myID); } private: int myID; }; class simpleThread: public Thread { public: simpleThread(int ID) : myID(ID) {} virtual void* run() { std::cout << "Thread " << myID << " is running" << std::endl; return reinterpret_cast<void*>(myID); } private: int myID; }; int main() { // thread1 and thread2 are created on the heap // thread3 is created on the stack // The destructor for thread1 and thread2 will automatically // delete the thread objects. std::auto_ptr<Runnable> r(new simpleRunnable(1)); std::auto_ptr<Thread> thread1(new Thread(r)); thread1->start(); std::auto_ptr<simpleThread> thread2(new simpleThread(2)); thread2->start(); simpleThread thread3(3); thread3.start(); // wait for the threads to finish int result1 = reinterpret_cast<int>(thread1->join()); int result2 = reinterpret_cast<int>(thread2->join()); int result3 = reinterpret_cast<int>(thread3.join()); std::cout << result1 << ' ' << result2 << ' ' << result3 << std::endl; return 0; }
Let's look at the code in detail.
- We have two major C++ classes: Runnable and Thread.
- The run() method is returning a value utilizing the fact that Win32 thread functions can return a value.
- The return value can be retrieved by using a Pthread-style call to method join().
- Class Runnable mimics Java's Runnable interface.
- We created threads in the same way as Java did: we write a C++ class that provides a run() method and inherits from Runnable.
class simpleRunnable: public Runnable { public: ... virtual void* run() { ... };
- We created an instance of this class.
std::auto_ptr<Runnable> r(new simpleRunnable(1));
- Then, pass a pointer to that instance as an argument to the Thread class constructor.
std::auto_ptr<Thread> thread1(new Thread(r));
- We call start() on that Thread object.
thread1->start();
- Class Thread also provides a join() method that simulates the pthread_join() operation.
- A call to T.join() blocks the caller until thread T's run() method completes.
- We use T.join() to ensure that T's run() method is completed before Thread T is destructed and the main thread completes.
- Method join() returns the value that we returned by run().
- Method join() is useful in Java when one threads needs to make sure that other threads have completed before accessing their results.
- But Java's run() method cannot return a value, so results must be obtained some other way.
- The implementation is not simple. When a C++ Thread is created, the corresponding Thread constructor calls function _beginthreadex()
- The _beginthreadex() with the following arguments:
- NULL
This is the default value for security attributes. - 0
This is the default value for stack size. - Thread::startThread() or Thread::startThreadRunnable()
Method startThread() is the startup method for threads created by inheriting from class Thread. Method startThreadRunnable() is the startup method for threads created from Runnable object. - (LPVOID)this
The fourth argument is a pointer to this Thread object, which is passed through to method startThread() or startThreadRunnable(). Thus, all threads execute one of the startup methods, but the startup methods receive a different Thread pointer each time they are executed. - CREATED_SUSPENDED
A Win32 thread is created to execute the startup method, but this thread is created in suspended mode, so the startup method does not begin executing until start() is called on the thread.thread1->start();
- NULL
- The thread is not actually started until method Thread::start() calls Win32 function ResumeThread(), which allows the thread to be scheduled and the startup method to begin execution.
void Thread::start() { assert(hThread); DWORD rc = ResumeThread(hThread); // thread created is in suspended state, // so this starts it running }
- The startup method is either startThread() or startThreadRunnable() depending on which Thread constructor was used to create the Thread object.
- Method startThread() casts its void* pointer to Thread*.
Thread* aThread = static_cast<Thread*>(pVoid);
- Then, it calls the run() method of its Thread* parameter.
aThread->result = aThread->run();
- When the run() method returns, startThread() calls setCompleted() to set the thread's status to completed and to notify any threads waiting in join() that the thread has completed.
aThread->setCompleted();
- The return value of the run() method is saved so that it can be retrieved in method join().
- Static method startThreadRunnable() performs similar steps when thread are created from Runnable objects.
- We use auto_ptr<> objects to manage the destruction of two of the threads and the Runnable object
r. std::auto_ptr<Runnable> r(new simpleRunnable(1)); std::auto_ptr<Thread> thread1(new Thread(r)); ... std::auto_ptr<simpleThread> thread2(new simpleThread(2));
- When auto_ptr<> objects thread1 and thread2 are destroyed automatically at the end of the program, their destructors will invoke
delete automatically on the pointers with which they were initialized. - Passing auto_ptr<Runnable> object to the Thread class constructor passes ownership of the Runnable object from the main thread to the child thread.
- The auto_ptr<Runnable> object in the child thread that receives the auto_ptr<Runnable> object owns the Runnable object that it has a pointer to, and will automatically delete the pointed-to object when the child thread is destroyed.
- When ownership is passed to the thread, the auto_ptr<Runnable> object in main is set automatically to a null state and can no longer be used to refer to the Runnable object. This protects against double deletion by the child thread and the main thread. It also prevents main from deleting the Runnable object before the thread has completed method run() and from accessing the Runnable object while the thread is accessing it.
- The startup functions, startThreadRunnable() and startThread() are static member functions. This is because the _beginthreadex() expects to receive the address of a startup function that has a single (void*) parameter.
unsigned WINAPI Thread::startThread(LPVOID pVoid) { Thread* aThread = static_cast<Thread*>(pVoid); .... }
A nonstatic member function that declares a single parameter actually has two parameters because of this pointer. So, if the startup function is a nonstatic member function, the hidden parameter, this, gets in the way and the call to the startup function fails. But the static member functions do not have this hidden parameter. - The results of the run are quite unpredictable as we predicted.
note: This tutorial on Multithreads for Win32 is largely based on the book "Modern Multithreading" by Carver and Tai. Here, I converted it more reader friendly.
Other Threading Tutorials:
- Multi-Threaded Programming - Terminology - Semaphore, Mutex, Priority Inversion etc.
- Multi-Threaded Programming II - Native Thread for Win32 (A)
- Multi-Threaded Programming II - Native Thread for Win32 (B)
- Multi-Threaded Programming II - Native Thread for Win32 (C)
- Multi-Threaded Programming II - C++ Thread for Win32
- Multi-Threaded Programming III - C/C++ Class Thread for Pthreads
- MultiThreading/Parallel Programming - IPC
- Multi-Threaded Programming with C++11 Part A (start, join(), detach(), and ownership)
- Multi-Threaded Programming with C++11 Part B (Sharing Data - mutex, and race conditions, and deadlock)
- Multithread Debugging
Threading with QT5:
- QThreads - Introduction
- QThreads - Creating Threads
- Creating QThreads using QtConcurrent
- QThreads - Priority
- QThreads - QMutex
- QThreads - GuiThread
- QHttp - Downloading Files
- QNetworkAccessManager and QNetworkRequest - Downloading Files
- QUdpSocket
- QTcpSocket
- QTcpSocket with Signals and Slots
- QTcpServer - Client and Server
- QTcpServer - Loopback Dialog
- QTcpServer - Client and Server using MultiThreading
- QTcpServer - Client and Server using QThreadPool
- Asynchronous QTcpServer - Client and Server using QThreadPool
Ph.D. / Golden Gate Ave, San Francisco / Seoul National Univ / Carnegie Mellon / UC Berkeley / DevOps / Deep Learning / Visualization