同步(Synchronization)
原创文章,未经允许,禁止转载😤
若有说错,欢迎指正,多谢😄
资源可以被多个线程或者多个进程进行访问,而一个线程或者一个进程对资源的修改会导致另外一个线程或者另外一个进程在对其读取或者修改的时候出现数据不同步的情况,会导致不正常的程序行为,所以需要一些保护机制来保证在同一时刻只有一个线程或者一个进程对资源进行操作。
不正常的行为举例如下,以线程为例:
假设存在数据成员int i = 3;
,该成员被两个线程A和B同时进行访问,在A线程的代码中,如下:
// 前面有业务逻辑代码
{
i = 4;
if(i==4){
cout<<"i==4 \n";
}else{
cout<<"i!=4 \n";
}
}
// 后面有业务逻辑代码
在B线程的代码中,如下:
// 前面有业务逻辑代码
{
i = 5;
if(i==5){
cout<<"i==5 \n";
}else{
cout<<"i!=5 \n";
}
}
// 后面有业务逻辑代码
假设在某一时间点,线程A先执行到i = 4
这行代码,然后线程A失去CPU的时间片,此时线程B得到调度执行,执行了i = 5
这行代码,然后线程B也立即失去了CPU的时间片,A得到调度执行,由线程A的判断分支,很明显就输出了i!=4的这个结果。对于线程A来说,我想要的肯定是i==4这个结果,由于B线程对于变量i的修改而导致A线程的程序出现的不可预知的错误。所以,需要一定的机制来保证A线程中大括号内的代码执行保证原子性,以下就引出几种机制。
Mutex
-
基本说明:
保护一个共享资源被多个线程或者多个进程的同时访问不会出现以上介绍出现的异常程序行为。
-
操作对象:线程和进程。
-
举例说明:
如果多个线程共享对数据库的访问,那么多个线程可以共享同一个mutex对象来保证在一个连续的时间段内只能有一个线程来对数据库进行写操作(注意:两个线程或者进程只有读写、写写才会出现数据不同步的问题,读读不会)。
在微软提供的api中,举例如下:
#include <windows.h> #include <stdio.h> #define THREADCOUNT 2 HANDLE ghMutex; DWORD WINAPI WriteToDatabase( LPVOID ); int main( void ) { HANDLE aThread[THREADCOUNT]; DWORD ThreadID; int i; // Create a mutex with no initial owner ghMutex = CreateMutex( NULL, // default security attributes FALSE, // initially not owned NULL); // unnamed mutex if (ghMutex == NULL) { printf("CreateMutex error: %d\n", GetLastError()); return 1; } // Create worker threads for( i=0; i < THREADCOUNT; i++ ) { aThread[i] = CreateThread( NULL, // default security attributes 0, // default stack size (LPTHREAD_START_ROUTINE) WriteToDatabase, NULL, // no thread function arguments 0, // default creation flags &ThreadID); // receive thread identifier if( aThread[i] == NULL ) { printf("CreateThread error: %d\n", GetLastError()); return 1; } } // Wait for all threads to terminate WaitForMultipleObjects(THREADCOUNT, aThread, TRUE, INFINITE); // Close thread and mutex handles for( i=0; i < THREADCOUNT; i++ ) CloseHandle(aThread[i]); CloseHandle(ghMutex); return 0; } DWORD WINAPI WriteToDatabase( LPVOID lpParam ) { // lpParam not used in this example UNREFERENCED_PARAMETER(lpParam); DWORD dwCount=0, dwWaitResult; // Request ownership of mutex. while( dwCount < 20 ) { dwWaitResult = WaitForSingleObject( ghMutex, // handle to mutex INFINITE); // no time-out interval switch (dwWaitResult) { // The thread got ownership of the mutex case WAIT_OBJECT_0: __try { // TODO: Write to the database printf("Thread %d writing to database...\n", GetCurrentThreadId()); dwCount++; } __finally { // Release ownership of the mutex object if (! ReleaseMutex(ghMutex)) { // Handle error. } } break; // The thread got ownership of an abandoned mutex // The database is in an indeterminate state case WAIT_ABANDONED: return FALSE; } } return TRUE; }
分析如下:
- 首先创建了存放两个线程句柄的
aThread
数组 - 然后调用
CreateThread
函数创建了两个线程,这两个线程的作用都是进行写数据库操作(因为是以WriteToDatabase
为内容创建的线程),并将两个线程的句柄存在aThread
中 - 在
WriteToDatabase
函数体内,线程在执行写操作时( // TODO: Write to the database 所在的括号内的代码)都会先调用WaitForSingleObject
函数,该函数的作用就是当前线程(比如aThread[0]
)需要请求对mutex的所有权,一旦请求到了,才允许进行数据库的写操作,其中INFINITE参数表示无限,用在这里的意思是,当前线程会一直请求对mutex的所有权,请求不到,一直等在WaitForSingleObject
这行代码处,直到获取到所有权之后才往后执行。 - 在main函数中(是主线程中的main函数)调用
WaitForMultipleObjects
来等待两个线程对象aThread[0],aThread[1]
变得可用,才执行后面的关闭两线程对象的操作 - 最终需要关闭mutex对象(
CloseHandle(ghMutex);
)
因为Windows对于资源的分配是有限度的,不可能无限制的分配线程资源、句柄资源,所以程序使用完毕后需要将资源还给Windows操作系统。
以上就是通过mutex对象来保证多个线程对同一个数据库进行写操作保护的具体案例。这个写操作和"同步(Synchronization)"一节中举例的大括号中的代码没有任何区别。
- 首先创建了存放两个线程句柄的
Event
案例前景:
主线程正在对一块共享内存进行写操作,其他线程需要对这块共享内容进行读操作,在这个主线程进行写过程结束之前,不应该有其他线程对这块共享内存进行 访问操作,除了上面说到的mutex对象外,也可以用Event对象。还是以微软提供的代码进行讲解:
#include <windows.h>
#include <stdio.h>
#define THREADCOUNT 4
HANDLE ghWriteEvent;
HANDLE ghThreads[THREADCOUNT];
DWORD WINAPI ThreadProc(LPVOID);
void CreateEventsAndThreads(void)
{
int i;
DWORD dwThreadID;
// Create a manual-reset event object. The write thread sets this
// object to the signaled state when it finishes writing to a
// shared buffer.
ghWriteEvent = CreateEvent(
NULL, // default security attributes
TRUE, // manual-reset event
FALSE, // initial state is nonsignaled
TEXT("WriteEvent") // object name
);
if (ghWriteEvent == NULL)
{
printf("CreateEvent failed (%d)\n", GetLastError());
return;
}
// Create multiple threads to read from the buffer.
for(i = 0; i < THREADCOUNT; i++)
{
// TODO: More complex scenarios may require use of a parameter
// to the thread procedure, such as an event per thread to
// be used for synchronization.
ghThreads[i] = CreateThread(
NULL, // default security
0, // default stack size
ThreadProc, // name of the thread function
NULL, // no thread parameters
0, // default startup flags
&dwThreadID);
if (ghThreads[i] == NULL)
{
printf("CreateThread failed (%d)\n", GetLastError());
return;
}
}
}
void WriteToBuffer(VOID)
{
// TODO: Write to the shared buffer.
printf("Main thread writing to the shared buffer...\n");
// Set ghWriteEvent to signaled
if (! SetEvent(ghWriteEvent) )
{
printf("SetEvent failed (%d)\n", GetLastError());
return;
}
}
void CloseEvents()
{
// Close all event handles (currently, only one global handle).
CloseHandle(ghWriteEvent);
}
int main( void )
{
DWORD dwWaitResult;
// TODO: Create the shared buffer
// Create events and THREADCOUNT threads to read from the buffer
CreateEventsAndThreads();
// At this point, the reader threads have started and are most
// likely waiting for the global event to be signaled. However,
// it is safe to write to the buffer because the event is a
// manual-reset event.
WriteToBuffer();
printf("Main thread waiting for threads to exit...\n");
// The handle for each thread is signaled when the thread is
// terminated.
dwWaitResult = WaitForMultipleObjects(
THREADCOUNT, // number of handles in array
ghThreads, // array of thread handles
TRUE, // wait until all are signaled
INFINITE);
switch (dwWaitResult)
{
// All thread objects were signaled
case WAIT_OBJECT_0:
printf("All threads ended, cleaning up for application exit...\n");
break;
// An error occurred
default:
printf("WaitForMultipleObjects failed (%d)\n", GetLastError());
return 1;
}
// Close the events to clean up
CloseEvents();
return 0;
}
DWORD WINAPI ThreadProc(LPVOID lpParam)
{
// lpParam not used in this example.
UNREFERENCED_PARAMETER(lpParam);
DWORD dwWaitResult;
printf("Thread %d waiting for write event...\n", GetCurrentThreadId());
dwWaitResult = WaitForSingleObject(
ghWriteEvent, // event handle
INFINITE); // indefinite wait
switch (dwWaitResult)
{
// Event object was signaled
case WAIT_OBJECT_0:
//
// TODO: Read from the shared buffer
//
printf("Thread %d reading from buffer\n",
GetCurrentThreadId());
break;
// An error occurred
default:
printf("Wait error (%d)\n", GetLastError());
return 0;
}
// Now that we are done reading the buffer, we could use another
// event to signal that this thread is no longer reading. This
// example simply uses the thread handle for synchronization (the
// handle is signaled when the thread terminates.)
printf("Thread %d exiting\n", GetCurrentThreadId());
return 1;
}
分析如下(看主线程的main中代码,这个才是真正的程序逻辑):
- 首先通过
CreateEvent
函数创建了一个多个线程可以共享的ghWriteEvent
事件对象 - 然后通过
CreateThread
创建了THREADCOUNT(4)个线程对象,并将线程的句柄保存在ghThreads[i]
数组中 - 然后主线程调用
WriteToBuffer
对共享内存进行写操作,写操作结束需要将事件对象ghWriteEvent
设置为可用状态,其他线程想要访问共享内存,必须查询ghWriteEvent
是否处于可用状态,只有在ghWriteEvent
处于可用状态,我们创建的四个线程才被允许访问共享资源,这个过程查询的过程就体现在ThreadProc
函数的WaitForSingleObject
调用上。之后便是访问共享资源的操作,体现在WAIT_OBJECT_0
case语句中 - 主线程通过
WaitForMultipleObjects
等待四个线程都变成可用状态,在此之前,主线程处于阻塞状态。 - 主线程等到之后,就可以释放线程资源和
ghWriteEvent
事件对象资源了,然后主线程结束,程序退出。
Semaphore
信号量对象。用来限制只有一定数量的线程来执行某一特定的任务。
还是以微软提供的代码进行讲解:
#include <windows.h>
#include <stdio.h>
#define MAX_SEM_COUNT 10
#define THREADCOUNT 12
HANDLE ghSemaphore;
DWORD WINAPI ThreadProc( LPVOID );
int main( void )
{
HANDLE aThread[THREADCOUNT];
DWORD ThreadID;
int i;
// Create a semaphore with initial and max counts of MAX_SEM_COUNT
ghSemaphore = CreateSemaphore(
NULL, // default security attributes
MAX_SEM_COUNT, // initial count
MAX_SEM_COUNT, // maximum count
NULL); // unnamed semaphore
if (ghSemaphore == NULL)
{
printf("CreateSemaphore error: %d\n", GetLastError());
return 1;
}
// Create worker threads
for( i=0; i < THREADCOUNT; i++ )
{
aThread[i] = CreateThread(
NULL, // default security attributes
0, // default stack size
(LPTHREAD_START_ROUTINE) ThreadProc,
NULL, // no thread function arguments
0, // default creation flags
&ThreadID); // receive thread identifier
if( aThread[i] == NULL )
{
printf("CreateThread error: %d\n", GetLastError());
return 1;
}
}
// Wait for all threads to terminate
WaitForMultipleObjects(THREADCOUNT, aThread, TRUE, INFINITE);
// Close thread and semaphore handles
for( i=0; i < THREADCOUNT; i++ )
CloseHandle(aThread[i]);
CloseHandle(ghSemaphore);
return 0;
}
DWORD WINAPI ThreadProc( LPVOID lpParam )
{
// lpParam not used in this example
UNREFERENCED_PARAMETER(lpParam);
DWORD dwWaitResult;
BOOL bContinue=TRUE;
while(bContinue)
{
// Try to enter the semaphore gate.
// 超时时间设置为0表示,当信号量对象处于不可用状态时,函数会立即返回
dwWaitResult = WaitForSingleObject(
ghSemaphore, // handle to semaphore
0L); // zero-second time-out interval
switch (dwWaitResult)
{
// The semaphore object was signaled.
case WAIT_OBJECT_0:
// TODO: Perform task
printf("Thread %d: wait succeeded\n", GetCurrentThreadId());
bContinue=FALSE;
// Simulate thread spending time on task
Sleep(5);
// Release the semaphore when task is finished
if (!ReleaseSemaphore(
ghSemaphore, // handle to semaphore
1, // increase count by one
NULL) ) // not interested in previous count
{
printf("ReleaseSemaphore error: %d\n", GetLastError());
}
break;
// The semaphore was nonsignaled, so a time-out occurred.
case WAIT_TIMEOUT:
printf("Thread %d: wait timed out\n", GetCurrentThreadId());
break;
}
}
return TRUE;
}
分析如下:
- 首先创建一个被多个线程(这里是13个,13=THREADCOUNT +1,1是主线程)共享的信号量对象,并且指定他的初始count计数和最大count计数都为MAX_SEM_COUNT(10)
- 然后创建12个线程,将线程的句柄保存在
aThread
数组中 - 然后主线程调用
WaitForMultipleObjects
来等待12个线程对象都变成可用状态。 - 12个线程内部的执行逻辑(
ThreadProc
内部)如下- 调用
WaitForSingleObject
来等待ghSemaphore
变为可用状态,可用时才能执行线程中真正的业务代码,体现在WAIT_OBJECT_0
case语句中
- 调用
- 主线程等到12个线程都成为可用状态后,释放线程句柄、释放信号量对象,结束程序。
以上虽然创建了12个线程来执行特定的任务(在ThreadProc
的WAIT_OBJECT_0
中),这个特定的任务当然也可以是对共享资源的访问,但是我程序只允许最多只能有10个线程来执行这个任务,所以可以用信号量的机制,并指定初始值10来做这种限制。
信号量的机制原理就是:指定一个最大值max,每一次一个线程获取到对于该信号量的拥有权(换种方式说:一个线程等到该信号量是可用状态时),信号量的计数值就会自动减1,如果计数值减到0之后,那么其他线程就等不到这个信号量变为有用状态,言外之意就是在这个信号量约束下的线程中的代码就得不到执行。当其中某个线程结束并且释放了信号量,那么该信号量的计数值就增加1,那么就可以有其他线程可以等到信号量并被调度执行。所以信号量机制的存在可以保证有多个线程执行某一特殊任务,但是对于多线程读写的行为,还是不宜用信号量,因为会发生数据的错误。但是多线程的读读行为,并且对于线程资源有数量上的约束行为,可以使用信号量。
特例:对于信号量的最计数值为1的信号量,可以实现多线程的读写行为,因为此时信号量和mutex、event在功能上没有任何区别。
Critical Section
临界区对象。
// Global variable
CRITICAL_SECTION CriticalSection;
int main( void )
{
...
// Initialize the critical section one time only.
if (!InitializeCriticalSectionAndSpinCount(&CriticalSection,
0x00000400) )
return;
...
// Release resources used by the critical section object.
DeleteCriticalSection(&CriticalSection);
}
DWORD WINAPI ThreadProc( LPVOID lpParameter )
{
...
// Request ownership of the critical section.
EnterCriticalSection(&CriticalSection);
// Access the shared resource.
// Release ownership of the critical section.
LeaveCriticalSection(&CriticalSection);
...
return 1;
}
- 创建全局的临界区对象
CriticalSection
- 调用
InitializeCriticalSectionAndSpinCount
初始化临界区对象 - 主线程main中的
...
可以表示对共享资源的访问,这里的写法比较懒,竟然连创建其他线程的代码也省去了,具体怎么创建可以参照章节"Mutex"、“Event”、“Semaphore” ThreadProc
定义了其他线程对共享资源的访问代码,在访问前需要调用EnterCriticalSection
进入临界区,访问结束,调用LeaveCriticalSection
离开临界区
Condition Variables
条件变量。
注意 在Windows Server 2003和Windows XP系统上,条件变量是不支持的。
以微软的代码来看结合条件变量和临界区对象实现的生产者消费者队列。
#include <windows.h>
#include <stdlib.h>
#include <stdio.h>
#define BUFFER_SIZE 10
#define PRODUCER_SLEEP_TIME_MS 500
#define CONSUMER_SLEEP_TIME_MS 2000
LONG Buffer[BUFFER_SIZE];
LONG LastItemProduced;
ULONG QueueSize;
ULONG QueueStartOffset;
ULONG TotalItemsProduced;
ULONG TotalItemsConsumed;
CONDITION_VARIABLE BufferNotEmpty;
CONDITION_VARIABLE BufferNotFull;
CRITICAL_SECTION BufferLock;
BOOL StopRequested;
DWORD WINAPI ProducerThreadProc (PVOID p)
{
ULONG ProducerId = (ULONG)(ULONG_PTR)p;
while (true)
{
// Produce a new item.
Sleep (rand() % PRODUCER_SLEEP_TIME_MS);
ULONG Item = InterlockedIncrement (&LastItemProduced);
EnterCriticalSection (&BufferLock);
while (QueueSize == BUFFER_SIZE && StopRequested == FALSE)
{
// Buffer is full - sleep so consumers can get items.
SleepConditionVariableCS (&BufferNotFull, &BufferLock, INFINITE);
}
if (StopRequested == TRUE)
{
LeaveCriticalSection (&BufferLock);
break;
}
// Insert the item at the end of the queue and increment size.
Buffer[(QueueStartOffset + QueueSize) % BUFFER_SIZE] = Item;
QueueSize++;
TotalItemsProduced++;
printf ("Producer %u: item %2d, queue size %2u\r\n", ProducerId, Item, QueueSize);
LeaveCriticalSection (&BufferLock);
// If a consumer is waiting, wake it.
WakeConditionVariable (&BufferNotEmpty);
}
printf ("Producer %u exiting\r\n", ProducerId);
return 0;
}
DWORD WINAPI ConsumerThreadProc (PVOID p)
{
ULONG ConsumerId = (ULONG)(ULONG_PTR)p;
while (true)
{
EnterCriticalSection (&BufferLock);
while (QueueSize == 0 && StopRequested == FALSE)
{
// Buffer is empty - sleep so producers can create items.
SleepConditionVariableCS (&BufferNotEmpty, &BufferLock, INFINITE);
}
if (StopRequested == TRUE && QueueSize == 0)
{
LeaveCriticalSection (&BufferLock);
break;
}
// Consume the first available item.
LONG Item = Buffer[QueueStartOffset];
QueueSize--;
QueueStartOffset++;
TotalItemsConsumed++;
if (QueueStartOffset == BUFFER_SIZE)
{
QueueStartOffset = 0;
}
printf ("Consumer %u: item %2d, queue size %2u\r\n",
ConsumerId, Item, QueueSize);
LeaveCriticalSection (&BufferLock);
// If a producer is waiting, wake it.
WakeConditionVariable (&BufferNotFull);
// Simulate processing of the item.
Sleep (rand() % CONSUMER_SLEEP_TIME_MS);
}
printf ("Consumer %u exiting\r\n", ConsumerId);
return 0;
}
int main ( void )
{
InitializeConditionVariable (&BufferNotEmpty);
InitializeConditionVariable (&BufferNotFull);
InitializeCriticalSection (&BufferLock);
DWORD id;
HANDLE hProducer1 = CreateThread (NULL, 0, ProducerThreadProc, (PVOID)1, 0, &id);
HANDLE hConsumer1 = CreateThread (NULL, 0, ConsumerThreadProc, (PVOID)1, 0, &id);
HANDLE hConsumer2 = CreateThread (NULL, 0, ConsumerThreadProc, (PVOID)2, 0, &id);
puts ("Press enter to stop...");
getchar();
EnterCriticalSection (&BufferLock);
StopRequested = TRUE;
LeaveCriticalSection (&BufferLock);
WakeAllConditionVariable (&BufferNotFull);
WakeAllConditionVariable (&BufferNotEmpty);
WaitForSingleObject (hProducer1, INFINITE);
WaitForSingleObject (hConsumer1, INFINITE);
WaitForSingleObject (hConsumer2, INFINITE);
printf ("TotalItemsProduced: %u, TotalItemsConsumed: %u\r\n",
TotalItemsProduced, TotalItemsConsumed);
return 0;
}
-
首先定义了几个全局变量
-
指定一个大小为
BUFFER_SIZE
的Buffer
-
CONDITION_VARIABLE BufferNotEmpty;
被消费者使用 -
CONDITION_VARIABLE BufferNotFull;
被生产者使用 -
CRITICAL_SECTION BufferLock;
保护生产者-消费者队列
-
-
先看生产者队列
对于生产者来说,只要队列不满,就可以往里放数据,满了,就不允许再放数据,让自己进入等待状态,具体看执行体
ProducerThreadProc
- 进入临界区
- 当队列满了的时候并且
StopRequested
为FALSE
,将生产者线程睡眠在条件变量BufferNotFull
上 - 如果
StopRequested
为TRUE
离开临界区,此时临界区对象保护的是条件变量BufferNotFull
- 当队列没有满的时候,往Buffer中放入一个数据
- 放入数据结束离开临界区
- 如果有一个消费者等在
BufferNotEmpty
上(Buffer中有数据,且有消费者可以消费,告诉你,你来消费吧),调用WakeConditionVariable (&BufferNotEmpty);
唤醒正在等着的线程,让CPU调度其运行
-
再看消费者队列
对于消费者来说,只要队列不空,就可以从里取数据进行消费,空了,就不允许再取数据,让自己进入等待状态,具体看执行体
ConsumerThreadProc
- 进入临界区
- 当队列为空且
StopRequested
为FALSE
,将消费者线程睡眠在条件变量BufferNotEmpty
- 如果
StopRequested
为TRUE
离开临界区且队列为空,离开临界区 - 取数据进行消费
- 离开临界区
- 如果有一个生产者等在
BufferNotFull
上(Buffer上至少有一个空槽可以让生产者生产数据,告诉生产者,你可以生产数据啦),唤醒它
-
最后看主线程的main函数
- 对条件变量和临界区变量的初始化
- 创建一个生产者线程,两个消费者线程
- 通过临界区变量设置标志
StopRequested
为TRUE - 唤醒所有的条件变量
- 等待生产者线程变为可用状态(生产者线程执行结束)
- 等待消费者线程1变为可用状态(消费者线程1执行结束)
- 等待消费者线程2变为可用状态(消费者线程2执行结束)
- 释放线程资源和其他资源(这里代码中好像没有体现…)
- 主线程结束,程序结束。
备注: 以上所说的对象变成可用状态在Windows中有一个专用名词叫做signaled状态,二者是等价的,可用状态是我个人的理解,signaled状态是官方的说法。
博客中如何插入表情,参考这位博主的文章即可。