线程同步问题
两种情况:
- 访问共享资源,措施是互斥;
- 线程执行有顺序要求,措施是同步(且互斥)。
原子操作
- InterlockedIncrement(), 整型变量自增1;
- InterlockedExchangeAdd(), 加一个数;
- InterlockedExchange(), 32位赋值;
- InterlockedExchange64(), 64位赋值;
- InterlockedCompareExchange(), 相等则赋值。
原子操作仅仅能够解决某一个4字节变量的计算问题。
临界区
- void InitializeCriticalSection();
- void DeleteCriticalSection();
- void EnterCriticalSection();
- void LeaveCriticalSection();
相比原子操作,可以保护一段代码,使这一段代码变为原子操作。
#include <windows.h>
#include <stdio.h>
long g_num = 0;
CRITICAL_SECTION g_ctitical;
DWORD WINAPI ThreadProc(LPVOID lParam)
{
int i = 10;
while (i--)
{
EnterCriticalSection(&g_ctitical);
InterlockedIncrement(&g_num);
LeaveCriticalSection(&g_ctitical);
}
return 0;
}
int main()
{
HANDLE hThread[2] = { 0 };
InitializeCriticalSection(&g_ctitical);
for (int i = 0; i < 2; ++i)
{
hThread[i] = CreateThread(NULL,
NULL,
ThreadProc,
NULL, NULL, NULL);
}
WaitForMultipleObjects(2,hThread,TRUE,INFINITE);
DeleteCriticalSection(&g_ctitical);
printf("%ld\n",g_num);
for (int i = 0; i < 2; ++i)
{
CloseHandle(hThread[i]);
}
return 0;
}
拥有临界区的线程不要多次进入临界区,否则会造成死锁。
只能在一个进程中的不同线程使用。后面的3种方案都是内核对象,可解决这个问题。
无法检测线程崩溃造成的临界区无法释放的问题。
互斥体mutex
相比临界区有以下优点:
- 互斥体是内核对象,可在多进程下同步;
- 线程崩溃后,没有释放互斥体,则互斥体处于激发态,不被任何线程拥有。
互斥体的2个概念:
- 两个状态:激发态,非激发态;
- 线程拥有权,与临界区类似;
被线程拥有时为非激发态,或者说锁上。
线程调用WaitForSingleObject()
后马上返回,则拥有并锁住互斥体(非激发态)。其它调用WaitForSingleObject()
的线程被阻塞,等待互斥体被释放。
WaitForXXX()
和ReleaseMutex()
之间的代码是被保护的,与临界区一样
api:
- CreateMutex()
- OpenMutex()
- ReleaseMutex()
#include<windows.h>
#include <stdio.h>
long g_n;
HANDLE g_hMutex;
DWORD WINAPI ThreadProc(LPVOID lpThreadParameter) {
for (int i = 0; i < 1000; i++)
{
//2.等待互斥体
WaitForSingleObject(g_hMutex, -1);
g_n++;
//3.释放互斥体
ReleaseMutex(g_hMutex);
}
return 0;
}
int main()
{
HANDLE hThread1 = 0, hThread2 = 0;
g_hMutex = CreateMutex(NULL, FALSE, L"foobar");
hThread1 = CreateThread(NULL,0,
ThreadProc,
NULL, NULL, NULL);
hThread2 = CreateThread(NULL, 0,
ThreadProc,
NULL, NULL, NULL);
WaitForSingleObject(hThread1, -1);
WaitForSingleObject(hThread2, -1);
CloseHandle(g_hMutex);
CloseHandle(hThread1);
CloseHandle(hThread2);
printf("%ld\n",g_n);
return 0;
}
互斥体只能被拥有者线程释放,所以多线程间不同回调函数使用互斥体可能出bug。
信号量
可以看成有多道锁的互斥体。全部锁上才不允许其它线程访问锁住的区域。
它没有拥有者的概念。
每个信号量都有一个当前信号数,只要不为0,信号量就处于激发态。
WaitForSingleObject()
将信号数减一,即上一把锁;ReleaseSemaphore()
将信号数加一,即开锁。
不同于互斥体的是,互斥体的等待与释放在同一线程内成对,信号量的释放可以在任何线程中。
经典案例:生产者消费者问题。
#include<windows.h>
#include <stdio.h>
long g_n;
HANDLE g_hSemaphore;
DWORD WINAPI ThreadProc(LPVOID lpThreadParameter)
{
for (int i = 0; i < 1000; i++)
{
//2.等待互斥体
WaitForSingleObject(g_hSemaphore, -1);
g_n++;
//3.释放互斥体
ReleaseSemaphore(g_hSemaphore,1,NULL);
}
return 0;
}
int main()
{
HANDLE hThread1 = 0, hThread2 = 0;
g_hSemaphore = CreateSemaphore(NULL,
1,1,
NULL);
hThread1 = CreateThread(NULL, NULL, ThreadProc, NULL, NULL, NULL);
hThread2 = CreateThread(NULL, NULL, ThreadProc, NULL, NULL, NULL);
WaitForSingleObject(hThread1, INFINITE);
WaitForSingleObject(hThread2, INFINITE);
CloseHandle(g_hSemaphore);
CloseHandle(hThread1);
CloseHandle(hThread2);
printf("%ld\n",g_n);
return 0;
}
生产者消费者问题
#include <iostream>
#include <Windows.h>
#include<deque>
#include<string>
using std::deque;
using std::string;
#define MAX_COUNT_FOOD 10 //最大食物
HANDLE g_hFull = NULL;//商店中的食物已满
HANDLE g_hEmpty = NULL;//商店中的食物已空
HANDLE g_hMutex = NULL;//消费者和生产者同一时间只能有一个在处理食物
deque<string> g_shop;//消息队列
创建消费者线程
DWORD WINAPI ConsumerThread(LPVOID)
{//消费食物
for (int i = 0; i < 10; i++)
{
WaitForSingleObject(g_hEmpty, INFINITE); //食物已空,不为空,可以消费食物
WaitForSingleObject(g_hMutex, INFINITE); //保证同一时间只能有生产者使用
//消费食物
std::cout << "消费了一个" << g_shop.back() << std::endl;
g_shop.pop_back();
ReleaseMutex(g_hMutex); //互斥
ReleaseSemaphore(g_hFull, 1, NULL); //已满信号减一
}
return 0;
}
生产者线程
DWORD WINAPI ProducerThread(LPVOID lParam)
{//生产食物
for (int i = 0; i < 10; i++)
{
WaitForSingleObject(g_hFull, INFINITE); //食物不满,需要产生食物
WaitForSingleObject(g_hMutex, INFINITE); //保证同一时间只能有生产者使用
//产生食物
g_shop.push_back("水果");
std::cout << "生产了一个" << "水果" << std::endl;
ReleaseMutex(g_hMutex); //互斥
ReleaseSemaphore(g_hEmpty, 1, NULL); //已空信号减一
}
return 0;
}
主函数:
int main()
{
HANDLE hThreadProducer = CreateThread(NULL, NULL, ProducerThread, NULL, NULL, NULL);
HANDLE hThreadConsumer = CreateThread(NULL, NULL, ConsumerThread, NULL, NULL, NULL);
g_hEmpty = CreateSemaphore(
NULL,
0,
MAX_COUNT_FOOD,
NULL);
g_hFull = CreateSemaphore(
NULL,
MAX_COUNT_FOOD, //初始化,不需要生产
MAX_COUNT_FOOD,
NULL);
g_hMutex = CreateMutex(NULL,FALSE,NULL);
WaitForSingleObject(hThreadProducer,INFINITE);
WaitForSingleObject(hThreadConsumer, INFINITE);
CloseHandle(g_hEmpty);
CloseHandle(g_hFull);
CloseHandle(g_hMutex);
return 0;
}
事件(最推荐使用)
唯一一个可以自己设置激发态,非激发态的内核对象。
也没有拥有者的概念。
事件对象有两种状态:
- 手动状态:不推荐
- 自动状态:被触发后只有一个等待事件的线程变为
可设置对事件对象有没有副作用,也可以设置激发态/非激发态。
可封装同步机制。
应用于windows高级编程。
- CreateEvent()
- OpenEvent()
- SetEvent(), 设置为激发态
- ResetEvent(),设置为非激发态
还有一个设置激发态的PulseEvent():
- 手动设置的对象,则唤醒所有等待它的线程,然后恢复为激发态;
- 自动设置的对象,则唤醒1个等待它的线程,然后恢复为激发态;
#include <stdio.h>
#include <windows.h>
long g_num;
HANDLE g_hEvent;
DWORD WINAPI ThreadProc(LPVOID lpThreadParameter) {
for (int i = 0; i < 100000; i++)
{
WaitForSingleObject(g_hEvent, -1);
++g_num;
SetEvent(g_hEvent);
}
return 0;
}
int main()
{
HANDLE hThread1 = 0, hThread2 = 0;
g_hEvent = CreateEvent(
NULL, //安全属性
FALSE, //是否手动 等待函数等到对象时自动设置无信号状态
TRUE, //事件初始信号 有信号
NULL);
hThread1 = CreateThread(NULL, NULL, ThreadProc, NULL, NULL, NULL);
hThread2 = CreateThread(NULL, NULL, ThreadProc, NULL, NULL, NULL);
WaitForSingleObject(hThread1, INFINITE);
WaitForSingleObject(hThread2, INFINITE);
CloseHandle(g_hEvent);
printf("%ld\n", g_num);
return 0;
}