Basic Concepts of Threading
BASIC CONCEPT: A thread, or LightWeight Process (LWP: LightWeight Process), is the smallest unit of a program's execution flow. A standard thread consists of a thread ID, a current instruction pointer (PC), a collection of registers, and a stack. A thread is an entity within a process, a basic unit that is independently scheduled and dispatched by the system. Threads do not own system resources and nearly own a small number of resources necessary for operation.
The basic state of a thread
Basic states: three basic states: ready, blocked and running.
Ready state: means that the thread has all the conditions to run, is logically ready to run, and is waiting for a handler;
Running state: means that the thread possession handler is running;
Blocking state: a thread is waiting for an event (e.g., a semaphore) and is logically not executable.
The relationship between processes and threads
In short, a program has at least one process, and a process has at least one thread.
The main difference between processes and threads is that they are different ways of managing operating system resources. Processes have a separate address space, and after a process crashes, it will not affect other processes in protected mode, while threads are just different execution paths in a process. Threads have their own stack and local variables, but there is no separate address space between threads, a thread dies is the same as the whole process dies, so multi-process programs are much more complex than themulti-threadedThe program is robust, but when switching between processes, it consumes more resources and is less efficient. However, for concurrent operations that require simultaneous operation and sharing of certain variables, only threads can be used, not processes.
Why use multithreading
- Avoiding Blocking As you know, there is only one main thread for a single process, and when the main thread blocks, the whole process blocks, and you can't do any other functions.
- Avoid CPU idling Applications often involve RPC, database access, disk IO, etc. These operations are much slower than the CPU, and while waiting for these responses, the CPU cannot process new requests, resulting in poor performance for such single-threaded applications. cpu > memory > disk
- Improved efficiency A process has 4GB of virtual address space on its own, whereas multiple threads can share the same address space, and thread switching is much faster than process switching.
Creating Threads
CreateThread
Need to add <windows.h>
HANDLE CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes,//SD
SIZE_T dwStackSize,//initialstacksize
LPTHREAD_START_ROUTINE lpStartAddress,//threadfunction
LPVOID lpParameter,//threadargument
DWORD dwCreationFlags,//creationoption
LPDWORD lpThreadId//threadidentifier
)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
1. The first parameter, lpThreadAttributes, represents the safe attributes of the thread kernel object, and is usually passed in as NULL to use the default settings.
2. The second parameter, dwStackSize, indicates the size of the thread stack space. Passing 0 means the default size (1MB) is used.
3. The third parameter, lpStartAddress, indicates the address of the thread function to be executed by the new thread, and multiple threads can use the same function address.
4. The fourth parameter, lpParameter, is the parameter passed to the thread function.
5. The fifth parameter, dwCreationFlags, specifies additional flags to control the creation of the thread. A value of 0 indicates that the thread can be scheduled immediately after creation, while a value of CREATE_SUSPENDED indicates that the thread is suspended after creation so that it cannot be scheduled until ResumeThread() is called.
6. The sixth parameter, lpThreadId, returns the ID number of the thread. Passing NULL means that the thread ID number does not need to be returned.
Example:
#include <iostream>
#include <>
DWORD WINAPI LaoWang(void* arg)
{
int count = *(int*)arg;
for (int i = 0; i < count; i++)
{
Sleep(4000);
puts("The old king sings.");
}
return 0;
}
DWORD WINAPI XiaoHong(void* arg)
{
int count = *(int*)arg;
for (int i = 0; i < count; i++)
{
Sleep(3000);
puts("Little red push-ups.");
}
return 0;
}
DWORD WINAPI XiaoMing(void* arg)
{
int count = *(int*)arg;
for (int i = 0; i < count; i++)
{
Sleep(2000);
puts("Ming shakes his hair.");
}
return 0;
}
int main()
{
HANDLE threadArry[3];
int laowang = 30, xiaohong = 30, xiaoming = 30;
threadArry[0] = CreateThread(NULL, 0, LaoWang, (void*)&laowang, 0, NULL);
threadArry[1] = CreateThread(NULL, 0, XiaoHong, (void*)&xiaohong, 0, NULL);
threadArry[2] = CreateThread(NULL, 0, XiaoMing, (void*)&xiaoming, 0, NULL);
WaitForMultipleObjects(3,threadArry,TRUE, INFINITE);
for (int i = 0; i < 3; i++)
{
CloseHandle(threadArry[i]);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
_beginthreadex
Need to add <> library
Need to add <<> library
unsigned long _beginthreadex(
void *security, // security attribute, NULL for default security
unsigned stack_size, // Thread's stack size, usually defaults to 0.
unsigned(_stdcall *start_address)(void *), // Threaded functions
void *argilist, // Arguments to threaded functions
unsigned initflag, // Initial state of the new thread, 0 means execute immediately, // CREATE_SUSPENDED means hang after creation.
unsigned *threaddr // Used to receive the thread ID
);
// Return value: // Success returns a new thread handle, failure returns 0.
// __stdcall means that 1. arguments are pressed onto the stack from right to left 2. the function is called by the caller to modify the stack
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
Example:
#include <iostream>
#include <>
#include <>
unsigned WINAPI LaoWang(void* arg)
{
int count = *(int*)arg;
for (int i = 0; i < count; i++)
{
Sleep(4000);
puts("The old king sings.");
}
}
unsigned WINAPI XiaoHong(void* arg)
{
int count = *(int*)arg;
for (int i = 0; i < count; i++)
{
Sleep(3000);
puts("Little red push-ups.");
}
return 0;
}
unsigned WINAPI XiaoMing(void* arg)
{
int count = *(int*)arg;
for (int i = 0; i < count; i++)
{
Sleep(2000);
puts("Ming shakes his hair.");
}
return 0;
}
int main()
{
uintptr_t threadArry[3];
hMutex = CreateMutex(NULL,false,NULL);
int laowang = 30, xiaohong = 30, xiaoming = 30;
threadArry[0] = _beginthreadex(NULL,0,LaoWang,(void*)&laowang,0,NULL);
threadArry[1] = _beginthreadex(NULL, 0, XiaoHong, (void*)&xiaohong, 0, NULL);
threadArry[2] = _beginthreadex(NULL, 0, XiaoMing, (void*)&xiaoming, 0, NULL);
WaitForMultipleObjects(3,(HANDLE*)threadArry,TRUE, INFINITE);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
Thread Synchronization - Mutually Exclusive Objects
What does mutually exclusive object mean?
Mutually exclusive objects are used in multithreading for accessing shared variables. In multithreading, it is sometimes possible for multiple threads to share a shared variable, but this is problematic: if more than one thread modifies a variable at the same time, the programmed value will be incorrect. A mutex object is a variable that allows threads to access it mutually exclusive. This way only one thread will be using the variable at a uniform moment. The general procedure for using mutexes is as follows: enter the mutex (and wait if another thread is using it) and release the mutex by manipulating the variable.
CreateMutex
HANDLE WINAPI CreateMutexW(
_In_opt_ LPSECURITY_ATTRIBUTES lpMutexAttributes, // Points to the security attribute
_In_ BOOL bInitialOwner, // Initialize the owner of the mutex object TRUE Immediate possession of the mutex body
_In_opt_ LPCWSTR lpName // Pointer to mutex object name L "Bingo"
);
- 1
- 2
- 3
- 4
- 5
Example:
#include <iostream>
#include <>
#include <>
HANDLE hMutex;
int num;
DWORD WINAPI LaoWang(void* arg)
{
WaitForSingleObject(hMutex, INFINITE);
int count = *(int*)arg;
for (int i = 0; i < count; i++)
{
Sleep(4000);
num += 1;
puts("The old king sings.");
ReleaseMutex(hMutex);
}
return 0;
}
DWORD WINAPI XiaoHong(void* arg)
{
WaitForSingleObject(hMutex, INFINITE);
int count = *(int*)arg;
for (int i = 0; i < count; i++)
{
Sleep(3000);
num += 1;
puts("Little red push-ups.");
ReleaseMutex(hMutex);
}
return 0;
}
DWORD WINAPI XiaoMing(void* arg)
{
WaitForSingleObject(hMutex, INFINITE);
int count = *(int*)arg;
for (int i = 0; i < count; i++)
{
Sleep(2000);
num += 1;
puts("Ming shakes his hair.");
ReleaseMutex(hMutex);
}
return 0;
}
int main()
{
HANDLE threadArry[3];
hMutex = CreateMutex(NULL,false,NULL);
int laowang = 30, xiaohong = 30, xiaoming = 30;
threadArry[0] = CreateThread(NULL, 0, LaoWang, (void*)&laowang, 0, NULL);
threadArry[1] = CreateThread(NULL, 0, XiaoHong, (void*)&xiaohong, 0, NULL);
threadArry[2] = CreateThread(NULL, 0, XiaoMing, (void*)&xiaoming, 0, NULL);
WaitForMultipleObjects(3,threadArry,TRUE, INFINITE);
for (int i = 0; i < 3; i++)
{
CloseHandle(threadArry[i]);
}
std::cout << "Num=" << num << std::endl;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
Event Objects for Thread Synchronization
The event object also belongs to the kernel object, which contains the following three members:
● Use of counts;
● A boolean value used to indicate whether the event is an automatically reset event or a manually reset event;
● Boolean value indicating whether the event is in the notified or unnotified state.
There are two types of event objects:
There is a difference between a manually reset event object and an automatically reset event object. The difference between these two types of event objects is that when a manually reset event object is notified, all threads waiting for the event object become schedulable, whereas when an automatically reset event object is notified, only one of the threads waiting for the event object becomes schedulable.
- Creating Event Objects
Call the CreateEvent function to create or open a named or anonymous event object. - Setting the event object state
Call the SetEvent function to set the specified event object to the signaled state. - Reset event object state
Calls the ResetEvent function to set the specified event object to an unsignaled state. - Request Event Object
The thread requests the event object by calling the WaitForSingleObject function.
CreateEvent
HANDLE CreateEvent(
LPSECURITY_ATTRIBUTES lpEventAttributes, // Security Attributes
BOOL bManualReset, // Reset method TRUE Must be manually reset with ResetEvent FALSE Automatically reset to no signal state
BOOL bInitialState, // Initial state TRUE Initial state is signaled FALSE Unsignaled state
LPCTSTR lpName //Object name NULL Unnamed event object
);
- 1
- 2
- 3
- 4
- 5
- 6
Event Object Example:
#include <iostream>
#include <>
#include <>
char Str[100] = { 0 };
HANDLE hEvent;
unsigned WINAPI HasA(void* arg)
{
int count = 0;
WaitForSingleObject(hEvent, INFINITE);
for (int i=0;Str[i]!=0;i++)
{
if (Str[i] == 'A')
{
count++;
}
}
printf("String containing %d letters of A.", count);
return 0;
}
unsigned WINAPI HasOther(void* arg)
{
int count = 0;
for (int i = 0; Str[i] != 0; i++)
{
if (Str[i] != 'A')
{
count++;
}
}
printf("Strings are %d without the letter A.", count - 1);
SetEvent(hEvent);
return 0;
}
int main()
{
fputs("Please enter capital letters:", stdout);
fgets(Str, 100, stdin);
HANDLE hThread1, hThread2;
hEvent = CreateEvent(NULL, true, false, L"A");
hThread1 = (HANDLE)_beginthreadex(NULL, 0, HasA, NULL, 0, NULL);
hThread2 = (HANDLE)_beginthreadex(NULL, 0, HasOther, NULL, 0, NULL);
WaitForSingleObject(hThread1, INFINITE);
WaitForSingleObject(hThread2, INFINITE);
CloseHandle(hEvent);
return 0;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
windows kernel objects and handles
1. Kernel Objects
Every kernel object in Windows is just a block of memory that is allocated by the operating system kernel and can only be accessed by the operating system kernel; the application program can't locate these data structures in memory and change their contents directly. The block is a data structure whose members maintain information about the object. A few members (security descriptors and usage counts) are common to all kernel objects, but most are specific to different types of objects.
2. Kernel object usage counts and lifetimes
Thread synchronization with semaphores
Components of a signal quantity
① Counter:
The number of times this kernel object has been used
② Maximum number of resources:
Identifies the maximum number of resources a semaphore can control (signed 32 bits)
(iii) The current number of resources:
Identifies the number of currently available resources (signed 32 bits). That is, it indicates the number of currently open resources (not the number of remaining resources), and only open resources can be claimed by threads. Only open resources can be requested by threads. However, these open resources are not necessarily occupied by threads. For example, if 5 resources are currently open and only 3 threads apply for them, then there are still 2 resources that can be applied for, but if there are a total of 7 threads that want to use the signaling capacity, then it is obvious that the 5 open resources are not enough. But if there are a total of 7 threads using the signal volume, obviously the 5 open resources are not enough. At this point, you can open 2 more until the maximum number of resources is reached.
The rules for semaphores are as follows:
(1) If the current resource count is greater than 0, then the semaphore is in the triggered state (signaled state), indicating that there are available resources.
(2) If the current resource count is equal to 0, then the semaphore is in the untriggered state (unsignaled state), indicating that there are no available resources.
(3) The system will never let the current resource count become negative (4) The current resource count will never be greater than the maximum resource count
The approximate flow of information volume usage:
A semaphore differs from a mutex in that it allows multiple threads to access the same resource at the same time, but limits the maximum number of threads that can access the resource at the same time. The semaphore object synchronizes threads in a different way than the previous methods, as signals allow multiple threads to use a shared resource at the same time.
CreateSemaphore
Function: Create a semaphore
HANDLE CreateSemaphore(
LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,// Null security properties
LONG lInitialCount,// How many resources are available at the time of initialization. 0: Untriggered // state (no signal), indicating no available resources
LONG lMaximumCount,//Maximum number of resources that can be handled
LPCTSTR lpName//NULL Name of the semaphore
);
- 1
- 2
- 3
- 4
- 5
- 6
ReleaseSemaphore
Function function: increase the signal quantity, but not exceed the maximum signal quantity
`WINAPI ReleaseSemaphore(
_In_ HANDLE hSemaphore, // Handle of the signal quantity
_In_ LONG lReleaseCount, // Add the lReleaseCount value to the top of the semaphore's current resource count 0-> 1
_Out_opt_ LPLONG lpPreviousCount // the original value of the current resource count );`
- 1
- 2
- 3
- 4
Example:
#include <>
#include <>
#include <>
unsigned WINAPI Read(void* arg);
unsigned WINAPI Accu(void* arg);
static HANDLE semOne;
static HANDLE semTwo;
static int num;
int main(int argc, char* argv[]) {
HANDLE hThread1, hThread2;
semOne = CreateSemaphore(NULL, 0, 1, NULL); //semOne No resources available Binary semaphore that can only represent 0 or 1 No signaling
semTwo = CreateSemaphore(NULL, 1, 1, NULL); //semTwo Available resources, signaling status Signaling available
hThread1 = (HANDLE)_beginthreadex(NULL, 0, Read, NULL, 0, NULL);
hThread2 = (HANDLE)_beginthreadex(NULL, 0, Accu, NULL, 0, NULL);
WaitForSingleObject(hThread1, INFINITE);
WaitForSingleObject(hThread2, INFINITE);
CloseHandle(semOne);
CloseHandle(semTwo);
system("pause");
return 0;
}
unsigned WINAPI Read(void* arg) {
int i; for (i = 0; i < 5; i++) {
fputs("Input num: ", stdout); // 1 5 11
printf("begin read\n"); // 3 6 12 // Wait for a signal from the kernel object semTwo; if there is a signal, continue execution; if there is no signal, wait
WaitForSingleObject(semTwo, INFINITE); printf("beginning read\n"); //4 10 16
scanf("%d", &num);
ReleaseSemaphore(semOne, 1, NULL);
}
return 0;
}
unsigned WINAPI Accu(void* arg) {
int sum = 0, i; for (i = 0; i < 5; i++) {
printf("begin Accu\n"); //2 9 15 //Wait for a signal from the kernel object semOne, if there is a signal, continue execution; if there is no signal, wait for the
WaitForSingleObject(semOne, INFINITE);
printf("beginning Accu\n"); //7 13
sum += num; printf("sum = %d \n", sum); // 8 14
ReleaseSemaphore(semTwo, 1, NULL);
}
printf("Result: %d \n", sum);
return 0;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
Key Code Snippets for Thread Synchronization
A critical code segment, also known as a critical area, operates in the user mode. It is a small piece of code that must have exclusive access to some resource before the code can be executed. The part of the code that accesses the same resource in multiple threads is usually regarded as the critical code segment.
VOID InitializeCriticalSection(PCRITICAL_SECTION pcs)
functionality: Initialize the critical area
parameters: psc points to the address of the CRITICAL_SECTION structure
This function has only one pointer to the CRITICAL_SECTION structure. Before calling the InitializeCriticalSection function, you first need to construct an object of type CRITICAL_SCTION structure and then pass the address of that object to the InitializeCriticalSection function
VOID DeleteCriticalSection(PCRITICAL_SECTION pcs)
functionality: Resets the member variable in the structure (instead of deleting it). Note: If you delete it, other threads are still using a key segment, which can have unpredictable results.
parameters: psc points to the address of the CRITICAL_SECTION structure
When the critical section is no longer needed, it can be released by calling the DeleteCriticalSection function, which releases all the resources of a critical section object that is not owned by any thread.
VOID EnterCriticalSection(PCRITICAL_SECTION pcs)
functionality: EnterCriticalSection checks the member variables in the structure that indicate whether a thread is making an access. and which thread is accessing. If no thread is accessing, it gets access. If a thread is accessing, it waits.
take note of: The internal implementation of this API is really just a few columns of tests on this structure, and its value lies in the fact that it can perform all the tests atomically.
parameters: psc points to the address of the CRITICAL_SECTION structure
The EnterCriticalSection function is called to take ownership of the specified critical section object. The function waits for ownership of the specified critical section object and returns if that ownership is given to the calling thread; otherwise the function waits, causing the thread to wait.
BOOL TryEnterCriticalSection(PCRITICAL_SECTION pcs)
functionality: It is the same as EnterCriticalSection, except that this API does not enter a wait state because another thread is using the structure pcs, it determines if it is accessible based on the return value. Then continue with the following code
parameters: psc points to the address of the CRITICAL_SECTION structure
VOID LeaveCriticalSection(PCRITICAL_SECTION pcs)
functionality: Update the member function to indicate that no threads are accessing the protected resource, and also check that no other threads are in a waiting state as a result of the call to EnterCriticalSection. If so, the member function is updated to cut a waiting thread back to the scheduling state
parameters: psc points to the address of the CRITICAL_SECTION structure
When a thread has finished using a resource protected by a critical section, it calls the LeaveCriticalSection function to release ownership of the specified critical section object. After that, other threads that want to take ownership of the critical section object can do so and access the protected resource by entering the critical code segment.
Example:
#include <iostream>
#include <>
#include <>
int tickets = 10000;
CRITICAL_SECTION c_arg;
unsigned WINAPI Thread1(void* arg)
{
while (true)
{
EnterCriticalSection(&c_arg);
if (tickets<=0)
{
LeaveCriticalSection(&c_arg);
break;
}
else
{
tickets--;
printf("Buy your ticket at window 1, %d sheet left \n", tickets);
LeaveCriticalSection(&c_arg);
}
}
return 0;
}
unsigned WINAPI Thread2(void* arg)
{
while (true)
{
EnterCriticalSection(&c_arg);
if (tickets <= 0)
{
LeaveCriticalSection(&c_arg);
break;
}
else
{
tickets--;
printf("Buy your ticket at window 2, %d sheet left \n", tickets);
LeaveCriticalSection(&c_arg);
}
}
return 0;
}
int main()
{
HANDLE hThread1, hThread2;
InitializeCriticalSection(&c_arg);
hThread1 = (HANDLE)_beginthreadex(NULL, 0, Thread1, NULL, 0, NULL);
hThread2 = (HANDLE)_beginthreadex(NULL, 0, Thread2, NULL, 0, NULL);
WaitForSingleObject(hThread1, INFINITE);
WaitForSingleObject(hThread2, INFINITE);
CloseHandle(hThread1);
CloseHandle(hThread2);
DeleteCriticalSection(&c_arg);
system("pause");
return 0;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60