C++ Multithreading
Category Programming Technology
Creating Threads
On the Windows platform, the Windows API provides support for multithreading. As mentioned in the concepts of processes and threads, a program must have at least one thread, which is called the main thread. If we do not explicitly create a thread, our program will only have the main thread.
Below, let's look at the operations and methods related to threads in Windows:
CreateThread and CloseHandle
CreateThread is used to create a thread, with the following function prototype:
HANDLE WINAPI CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes, // Security attributes related to the thread, often set to NULL
SIZE_T dwStackSize, // Initial stack size for the new thread, can be set to 0
LPTHREAD_START_ROUTINE lpStartAddress, // Callback function to be executed by the thread, also known as the thread function
LPVOID lpParameter, // Parameter passed to the thread function, NULL if no parameter is needed
DWORD dwCreationFlags, // Flags controlling the thread creation
LPDWORD lpThreadId // Output parameter, used to get the thread ID, NULL if the thread ID is not needed
);
Explanation:
- lpThreadAttributes: A pointer to a SECURITY_ATTRIBUTES structure, determines if the returned handle can be inherited by child processes. If NULL, the handle cannot be inherited.
- dwStackSize: Initial stack size for the thread in bytes. The system allocates this value.
lpStartAddress: A pointer to a function that the thread will call. This function is known as the thread function (ThreadProc) and is the starting address for the thread. The thread function is a callback function called by the operating system within the thread. The prototype is:
DWORD WINAPI ThreadProc(LPVOID lpParameter); // lpParameter is the input parameter, a void pointer
lpParameter: Parameter passed to the thread function (ThreadProc), NULL if no parameter is needed.
- dwCreationFlags: Flags controlling the thread creation. Three types: 0: The thread starts executing immediately; CREATE_SUSPENDED: The thread is created in a suspended state and only starts executing when resumed; STACK_SIZE_PARAM_IS_A_RESERVATION: dwStackSize specifies the initial stack size for the thread. If this flag is not specified, dwStackSize is set to a system-reserved value.
- Return value: If the thread is created successfully, it returns the handle of the new thread; otherwise, it returns NULL. If the thread creation fails, you can get the error information using the GetLastError function.
BOOL WINAPI CloseHandle(HANDLE hObject); // Closes an open object handle
[Demo1]: Creating a Simple Thread
Example
#include "stdafx.h"
#include <windows.h>
#include <iostream>
using namespace std;
// Thread function
DWORD WINAPI ThreadProc(LPVOID lpParameter)
{
for (int i = 0; i < 5; ++i)
{
cout << "Child Thread: i = " << i << endl;
Sleep(100);
}
return 0L;
}
int main()
{
// Create a thread
HANDLE thread = CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL);
// Close the thread
CloseHandle(thread);
// Main thread execution path
for (int i = 0; i < 5; ++i)
{
cout << "Main Thread: i = " << i << endl;
Sleep(100);
}
return 0;
}
Result:
Main Thread: i = 0
Child Thread: i = 0
Main Thread: i = 1
Child Thread: i = 1
Child Thread: i = 2
Main Thread: i = 2
Child Thread: i = 3
Main Thread: i = 3
Child Thread: i = 4
Main Thread: i = 4
[Demo2]: Passing Parameters in the Thread Function
Example
#include "stdafx.h"
#include <windows.h>
#include <iostream>
using namespace std;
#define NAME_LINE 40
// Define the structure for thread function parameters
typedef struct __THREAD_DATA
{
int nMaxNum;
char strThreadName[NAME_LINE];
__THREAD_DATA() : nMaxNum(0)
{
memset(strThreadName, 0, NAME_LINE * sizeof(char));
}
} THREAD_DATA;
// Thread function
DWORD WINAPI ThreadProc(LPVOID lpParameter)
{
THREAD_DATA* pThreadData = (THREAD_DATA*)lpParameter;
for (int i = 0; i < pThreadData->nMaxNum; ++i)
{
cout << pThreadData->strThreadName << " --- " << i << endl;
Sleep(100);
}
return 0L;
}
int main()
{
// Initialize thread data
THREAD_DATA threadData1, threadData2;
threadData1.nMaxNum = 5;
strcpy(threadData1.strThreadName, "Thread 1");
threadData2.nMaxNum = 10;
strcpy(threadData2.strThreadName, "Thread 2");
// Create the first child thread
HANDLE hThread1 = CreateThread(NULL, 0, ThreadProc, &threadData1, 0, NULL);
// Create the second child thread
HANDLE hThread2 = CreateThread(NULL, 0, ThreadProc, &threadData2, 0, NULL);
// Close the threads
CloseHandle(hThread1);
CloseHandle(hThread2);
// Main thread execution path
for (int i = 0; i < 5; ++i)
{
cout << "Main Thread === " << i << endl;
Sleep(100);
}
system("pause");
return 0;
}
Result:
Main Thread === Thread 1 — 0
0
Thread 2 — 0
Thread 1 — 1
Main Thread === 1
Thread 2 — 1
Main Thread === 2
Thread 1 — 2
Thread 2 — 2
Main Thread === 3
Thread 2 — 3
Thread 1 — 3
Main Thread === 4
Thread 2 — 4
Thread 1 — 4
Thread 2 — 5
Press any key to continue… Thread 2 — 6
Thread 2 — 7
Thread 2 — 8
Thread 2 — 9
CreateMutex, WaitForSingleObject, ReleaseMutex
From [Demo2], we can see that although the created child threads are running normally, the output is not what we expected. We expected each statement to be output followed by a newline, but the result is not always like this. This is because no synchronization is done when the threads are executed, for example, the first line of output. The main thread outputs "Main Thread ===" and then the time slice is used up. Then it's the child thread 1's turn to output, and after outputting "Thread 1 —", the time slice is also used up. Then it's the main thread's turn to execute and output "0", and then it's the child thread 1's turn to output "0". Thus, the result "Main Thread === Thread 1 — 0 0" appears.
Main thread: cout << "Main Thread === " << i << endl;
To avoid this situation, we do some simple synchronization for the threads, using a Mutex here.
A Mutex is similar to a binary semaphore, allowing only one thread to access the resource. Unlike a binary semaphore, a semaphore can be acquired and released by any thread in the system, meaning the same semaphore can be acquired by one thread and released by another. A Mutex, however, requires the thread that acquires the Mutex lock to be the one that releases it; other threads releasing the Mutex are ineffective.
The following functions are used when using a Mutex for thread synchronization:
HANDLE WINAPI CreateMutex(
LPSECURITY_ATTRIBUTES lpMutexAttributes, // Security attributes related to the thread, often set to NULL
BOOL bInitialOwner, // Whether the creating thread owns the Mutex initially
LPCTSTR lpName // Name of the Mutex
);
Explanation: lpMutexAttributes also indicates security, similar to lpThreadAttributes in CreateThread, determining whether the returned handle can be inherited by child processes. If NULL, the handle cannot be inherited. bInitialOwner indicates whether the creating thread owns the Mutex initially. If TRUE, the creating thread is designated as the owner of the Mutex object, and other threads need to release the Mutex first before accessing it. lpName is the name of the Mutex.
DWORD WINAPI WaitForSingleObject(
HANDLE hHandle, // Handle of the lock to acquire
DWORD dwMilliseconds // Timeout interval
);
Explanation: WaitForSingleObject waits for a specified object (such as a Mutex object) until that object is no longer occupied (such as the Mutex being released) or the timeout interval is exceeded. Additionally, there is a similar function, WaitForMultipleObjects, which waits for one or all specified objects until all are no longer occupied or the timeout interval is exceeded.
hHandle: Handle of the object to wait for. dwMilliseconds: Timeout interval in milliseconds; if dwMilliseconds is non-zero, it waits until the timeout interval is exceeded or the object is no longer occupied. If dwMilliseconds is INFINITE, it waits indefinitely until the object is no longer occupied.
BOOL WINAPI ReleaseMutex(HANDLE hMutex);
Explanation: Release the mutex lock object that you possess, where hMutex
is the handle of the mutex to be released.
[Demo3]: Thread Synchronization
Example
#include "stdafx.h"
#include <windows.h>
#include <iostream>
#define NAME_LINE 40
// Define the structure for thread function parameters
typedef struct __THREAD_DATA
{
int nMaxNum;
char strThreadName[NAME_LINE];
__THREAD_DATA() : nMaxNum(0)
{
memset(strThreadName, 0, NAME_LINE * sizeof(char));
}
} THREAD_DATA;
HANDLE g_hMutex = NULL; // Mutex
// Thread function
DWORD WINAPI ThreadProc(LPVOID lpParameter)
{
THREAD_DATA* pThreadData = (THREAD_DATA*)lpParameter;
for (int i = 0; i < pThreadData->nMaxNum; ++i)
{
// Request a mutex lock
WaitForSingleObject(g_hMutex, INFINITE);
cout << pThreadData->strThreadName << " --- " << i << endl;
Sleep(100);
// Release the mutex lock
ReleaseMutex(g_hMutex);
}
return 0L;
}
int main()
{
// Create a mutex
g_hMutex = CreateMutex(NULL, FALSE, NULL);
// Initialize thread data
THREAD_DATA threadData1, threadData2;
threadData1.nMaxNum = 5;
strcpy(threadData1.strThreadName, "Thread1");
threadData2.nMaxNum = 10;
strcpy(threadData2.strThreadName, "Thread2");
// Create the first child thread
HANDLE hThread1 = CreateThread(NULL, 0, ThreadProc, &threadData1, 0, NULL);
// Create the second child thread
HANDLE hThread2 = CreateThread(NULL, 0, ThreadProc, &threadData2, 0, NULL);
// Close the threads
CloseHandle(hThread1);
CloseHandle(hThread2);
// Main thread execution path
for (int i = 0; i < 5; ++i)
{
// Request a mutex lock
WaitForSingleObject(g_hMutex, INFINITE);
cout << "Main Thread === " << i << endl;
Sleep(100);
// Release the mutex lock
ReleaseMutex(g_hMutex);
}
system("pause");
return 0;
}
Result:
Main Thread === 0
Thread1 — 0
Thread2 — 0
Main Thread === 1
Thread1 — 1
Thread2 — 1
Main Thread === 2
Thread1 — 2
Thread2 — 2
Main Thread === 3
Thread1 — 3
Thread2 — 3
Main Thread === 4
Thread1 — 4
Press any key to continue… Thread2 — 4
Thread2 — 5
Thread2 — 6
Thread2 — 7
Thread2 — 8
Thread2 — 9
To further understand the importance of thread synchronization and the use of mutexes, let's look at another example.
Buying train tickets is a major concern for people returning home during the Spring Festival. We will simulate a simple train ticket sales system (for simplicity, we will extract the simplest model for simulation): There are 500 train tickets from Beijing to Ganzhou, sold simultaneously at 8 windows, ensuring system stability and data atomicity.
[Demo4]: Simulating Train Ticket Sales System
SaleTickets.h
#include "stdafx.h"
#include <windows.h>
#include <iostream>
#include <strstream>
#include <string>
using namespace std;
#define NAME_LINE 40
// Define the structure for thread function parameters
typedef struct __TICKET
{
int nCount;
char strTicketName[NAME_LINE];
__TICKET() : nCount(0)
{
memset(strTicketName, 0, NAME_LINE * sizeof(char));
}
} TICKET;
typedef struct __THD_DATA
{
TICKET* pTicket;
char strThreadName[NAME_LINE];
__THD_DATA() : pTicket(NULL)
{
memset(strThreadName, 0, NAME_LINE * sizeof(char));
}
} THD_DATA;
// Convert basic type data to string
template<class T>
string convertToString(const T val)
{
string s;
std::strstream ss;
ss << val;
ss >> s;
return s;
}
// Ticket selling program
DWORD WINAPI SaleTicket(LPVOID lpParameter);
SaleTickets.cpp
#include "stdafx.h"
#include <windows.h>
#include <iostream>
#include "SaleTickets.h"
using namespace std;
extern HANDLE g_hMutex;
// Ticket selling program
DWORD WINAPI SaleTicket(LPVOID lpParameter)
{
THD_DATA* pThreadData = (THD_DATA*)lpParameter;
TICKET* pSaleData = pThreadData->pTicket;
while (pSaleData->nCount > 0)
{
// Request a mutex lock
WaitForSingleObject(g_hMutex, INFINITE);
if (pSaleData->nCount > 0)
{
cout << pThreadData->strThreadName << " sold ticket " << pSaleData->nCount-- << ", ";
if (pSaleData->nCount >= 0) {
cout << "Ticket sold successfully! Remaining " << pSaleData->nCount << " tickets." << endl;
} else {
cout << "Ticket sale failed! Tickets sold out." << endl;
}
}
Sleep(10);
// Release the mutex lock
ReleaseMutex(g_hMutex);
}
return 0L;
}
Test program:
// Ticket selling system
void Test2()
{
// Create a mutex
g_hMutex = CreateMutex(NULL, FALSE, NULL);
// Initialize train tickets
TICKET ticket;
ticket.nCount = 100;
strcpy(ticket.strTicketName, "Beijing-->Ganzhou");
const int THREAD_NUMM = 8;
THD_DATA threadSale[THREAD_NUMM];
HANDLE hThread[THREAD_NUMM];
for (int i = 0; i < THREAD_NUMM; ++i)
{
threadSale[i].pTicket = &ticket;
string strThreadName = convertToString(i);
strThreadName = "Window" + strThreadName;
strcpy(threadSale[i].strThreadName, strThreadName.c_str());
// Create thread
hThread[i] = CreateThread(NULL, NULL, SaleTicket, &threadSale[i], 0, NULL);
// Request a mutex lock
WaitForSingleObject(g_hMutex, INFINITE);
cout << threadSale[i].strThreadName << " started selling " << threadSale[i].pTicket->strTicketName << " tickets..." << endl;
// Release the mutex lock
ReleaseMutex(g_hMutex);
// Close the thread
CloseHandle(hThread[i]);
}
system("pause");
}
Result:
Window0 started selling Beijing–>Ganzhou tickets…
Window0 sold ticket 100, Ticket sold successfully! Remaining 99 tickets.
Window1 started selling Beijing–>Ganzhou tickets…
Window1 sold ticket 99, Ticket sold successfully! Remaining 98 tickets.
Window0 sold ticket 98, Ticket sold successfully! Remaining 97 tickets.
Window2 started selling Beijing–>Ganzhou tickets…
Window2 sold ticket 97, Ticket sold successfully! Remaining 96 tickets.
Window1 sold ticket 96, Ticket sold successfully! Remaining 95 tickets.
Window0 sold ticket 95, Ticket sold successfully! Remaining 94 tickets.
Window3 started selling Beijing–>Ganzhou tickets…
Window3 sold ticket 94, Ticket sold successfully! Remaining 93 tickets.
Window2 sold ticket 93, Ticket sold successfully! Remaining 92 tickets.
Window1 sold ticket 92, Ticket sold successfully! Remaining 91 tickets.
Window0 sold ticket 91, Ticket sold successfully! Remaining 90 tickets.
Window4 started selling Beijing–>Ganzhou tickets…
Window4 sold ticket 90, Ticket sold successfully! Remaining 89 tickets.
Window3 sold ticket 89, Ticket sold successfully! Remaining 88 tickets.
Window2 sold ticket 88, Ticket sold successfully! Remaining 87 tickets.
Window1 sold ticket 87, Ticket sold successfully! Remaining 86 tickets.
Window0 sold ticket 86, Ticket sold successfully! Remaining 85 tickets.
Window5 started selling Beijing–>Ganzhou tickets…
Window5 sold ticket 85, Ticket sold successfully! Remaining 84 tickets.
Window4 sold ticket 84, Ticket sold successfully! Remaining 83 tickets.
Window3 sold ticket 83, Ticket sold successfully! Remaining 82 tickets.
Window2 sold ticket 82, Ticket sold successfully! Remaining 81 tickets.
Source: http://blog.csdn.net/luoweifu/article/details/46835437