算法介绍:Dekker互斥算法
这个算法的目的是解决临界区问题,即多个进程或线程需要访问共享资源,但是每次只能有一个进程或线程可以访问该资源,以避免数据竞争和不确定行为的发生。
具体来说,Dekker's algorithm解决了以下问题:
- 竞争条件(Race Condition):在多线程或多进程的环境中,当两个或多个线程或进程试图同时访问共享资源时,可能会导致竞争条件的发生。竞争条件可能会导致不一致的结果或数据损坏。
- 临界区问题(Critical Section Problem):多个进程或线程需要访问临界区(共享资源),但是每次只能有一个进程或线程能够进入临界区,否则可能会导致数据不一致或不确定的行为。
Dekker's algorithm通过使用互斥标志和轮流访问临界区的方式来解决这些问题,确保在任何时刻只有一个进程或线程可以进入临界区,从而避免了竞争条件和临界区问题的发生。
逻辑推导:
设有进程P0和P1,两者谁要访问临界区,就让对应的flag=true(例如P0要访问临界区,就让flag[0]=true),相当于“举手示意我要访问”。初始值为0表示一开始没人要访问
turn用于标识当前允许谁进入,turn=0则P0可进入,turn=1则P1可进入。
1)P0的逻辑
do{
flag[0] = true;// 首先P0举手示意我要访问
while(flag[1]) {// 看看P1是否也举手了
if(turn==1){// 如果P1也举手了,那么就看看到底轮到谁
flag[0]=false;// 如果确实轮到P1,那么P0先把手放下(让P1先)
while(turn==1);// 只要还是P1的时间,P0就不举手,一直等
flag[0]=true;// 等到P1用完了(轮到P0了),P0再举手
}
flag[1] = false; // 只要可以跳出循环,说明P1用完了,应该跳出最外圈的while
}
visit();// 访问临界区
turn = 1;// P0访问完了,把轮次交给P1,让P1可以访问
flag[0]=false;// P0放下手
2)P1的逻辑
do{
flag[1] = true;// 先P1举手示意我要访问
while(flag[0]) {// 如果P0是否也举手了
if(turn==0){// 如果P0也举手了,那么久看看到底轮到谁
flag[1]=false;// 如果确实轮到P0,那么P1先把手放下(让P0先)
while(turn==0);// 只要还是P0的时间,P1就不举手,一直等
flag[1]=true;// 等到P0用完了(轮到P1了),P1再举手
}
flag[0] = false; // 只要可以跳出循环,说明P0用完了,应该跳出最外圈的while
}
visit();// 访问临界区
turn = 0;// P1访问完了,把轮次交给P0,让P0可以访问
flag[1]=false;// P1放下手
利弊分析:
该算法能解决在单核上多线程切换时资源分配的问题,但是却不能解决多核进程切换的问题
直接调用API解决同步问题:
以下介绍几种不同的同步问题解决方案:
临界区:
InitializeCriticalSection 初始化临界区
EnterCriticalSection 进入临界区
LeaveCriticalSection 离开临界区
tips: 此处临界区的思想与Dekker中的逻辑基本一致,临界区中设置一些标识,便于多线程资源管理,优化了Dekker算法。
代码示例:
#include <stdio.h>
#include <windows.h>
#include <thread>
#define MAX_COUT 1000000
int g_nNum = 0;
CRITICAL_SECTION g_cs;
/*
同步对象
1.临界区
*/
void WorkThread()
{
for (int i = 0; i < MAX_COUT; i++)
{
EnterCriticalSection(&g_cs); //进入临界区,如果进不去自动让出时间片
g_nNum++;
LeaveCriticalSection(&g_cs); //退出临界区
}
}
int main()
{
InitializeCriticalSection(&g_cs); //初始化临界区(不可以跨进程)
//C++封装的线程类
//可以验证加上临界区后,程序运行时长的变化
DWORD dwStart = GetTickCount(); //获取开始时间
std::thread t1(WorkThread);
std::thread t2(WorkThread);
t1.join();
t2.join();
DWORD dwEnd = GetTickCount(); //获取结束时间
printf("g_Num: %d Time:%d", g_nNum, dwEnd - dwStart);
return 0;
}
互斥(Mutex):
优点:可以跨进程同步 缺点:速度慢
CreateMutex 创建互斥
当其他进程需要与当前进程抢资源时,可以通过OpenMutex获取同一个互斥,进一步进行抢信号
ReleaseMutex 释放信号
代码示例:
// Sync.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include <stdio.h>
#include <windows.h>
#include <thread>
#define MAX_COUT 1000000
int g_nNum = 0;
HANDLE g_hMutex;
/*
同步对象
2.互斥体
*/
void WorkThread()
{
for (int i = 0; i < MAX_COUT; i++)
{
//抢互斥体信号,直到收到互斥体信号,才分配时间片。表示抢到信号了,可以操控资源了
WaitForSingleObject(g_hMutex, INFINITE);
g_nNum++;
ReleaseMutex(g_hMutex); //释放一个互斥体信号,表示我不要这个信号了,让别人抢
}
}
int main()
{
g_hMutex = CreateMutex(NULL, FALSE, NULL); //只创建互斥体,但是不要这个信号(第二个参数决定),其他线程谁抢到信号,谁获取资源
DWORD dwStart = GetTickCount(); //获取开始时间
std::thread t1(WorkThread);
std::thread t2(WorkThread);
t1.join();
t2.join();
DWORD dwEnd = GetTickCount(); //获取结束时间
printf("g_Num: %d Time:%d", g_nNum, dwEnd - dwStart);
return 0;
}
事件和信号量
这两种方法本质上与互斥时是一样的,底层用的同一套API。但是,事件和信号量的主要作用是为了线程间的通知(如:当A进程在未收到B进程的信号量时,一直处于挂起状态,当B通知A后,才激活线程),而互斥是为了解决不同线程抢夺相同资源时的同步问题。
事件代码示例:
// Sync.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include <stdio.h>
#include <windows.h>
#include <thread>
#define MAX_COUT 1000000
int g_nNum = 0;
HANDLE g_hEvent;
/*
同步对象
3.事件对象(同步)
*/
void WorkThread() {
for (int i = 0; i < MAX_COUT; i++)
{
WaitForSingleObject(g_hEvent, INFINITE); //此处是自动Reset信号解决同步问题
g_nNum++;
SetEvent(g_hEvent); //当信号被处理后需要重新为该事件创建新信号,以便其他线程可以利用这部分资源
}
}
int main()
{
g_hEvent = CreateEvent(NULL, FALSE, TRUE, NULL);
g_hPostEvent = CreateEvent(NULL, FALSE, TRUE, NULL);
//第二个参数手动适合通知(TRUE),自动适合同步
//手动表示,当调用API操作了这个句柄后,获取了这个信号,但是信号不会发生改变,依然可以被其他线程使用(cpu切换时间片),需要手动调一个API设置
//然而,我们下一行手动调用API的代码又有可能被切走了,所以手动不适合同步
//WaitForSingleObject(g_hEvent, INFINITE);
//ResetEvent(g_hEvent);
//第三个参数表示,我们在创建事件的时候,就创建一个信号
//测试时间发现,事件与互斥体消耗的事件一致
DWORD dwStart = GetTickCount(); //获取开始时间
std::thread t1(WorkThread);
std::thread t2(WorkThread);
t1.join();
t2.join();
DWORD dwEnd = GetTickCount(); //获取结束时间
printf("g_Num: %d Time:%d", g_nNum, dwEnd - dwStart);
return 0;
}
事件对象通知功能示例代码:
// Sync.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include <stdio.h>
#include <windows.h>
#include <thread>
#define MAX_COUT 1000000
int g_nNum = 0;
HANDLE g_hEvent;
HANDLE g_hPostEvent;
/*
同步对象
3.事件对象(通知)
*/
void WorkThread()
{
for (int i = 0; i < MAX_COUT; i++)
{
WaitForSingleObject(g_hEvent, INFINITE);
g_nNum++;
SetEvent(g_hEvent);
}
}
DWORD dwStart; //设置为全局,方便所有线程访问和使用
void ShowWorkThread()
{
WaitForSingleObject(g_hPostEvent, -1); //当这个线程没有收到g_hPostEvent事件的信号时,不分配时间片
DWORD dwEnd = GetTickCount();
printf("g_nNum:%d times:%d\\n", g_nNum, dwEnd - dwStart);
}
int main()
{
g_hEvent = CreateEvent(NULL, FALSE, TRUE, NULL);
g_hPostEvent = CreateEvent(NULL, FALSE, FALSE, NULL); //设置时间,但是不创建信号
dwStart = GetTickCount();
std::thread t1(WorkThread);
std::thread t2(WorkThread);
std::thread t3(ShowWorkThread);
t1.join();
t2.join();
SetEvent(g_hPostEvent); //当线程1,2都跑完时,g_hPostEvent事件创建信号,此时线程3将被激活
t3.join();
return 0;
}
tips:这种通知方式同样可以跨进程,用Open函数即可
信号量代码示例:
信号量:指同时可以存在多个信号的事件,当抢到多个信号时,则有多个线程批量完成相同的人物
// Sync.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include <stdio.h>
#include <windows.h>
#include <thread>
#define MAX_COUT 1000000
int g_nNum = 0;
HANDLE g_hPostEvent;
HANDLE g_hSem;
/*
同步对象
1.临界区
2.互斥体
3.事件对象(通知)
4.信号量(通知)
*/
void WorkThread() {
for (int i = 0; i < MAX_COUT; i++)
{
WaitForSingleObject(g_hSem, INFINITE);
g_nNum++;
ReleaseSemaphore(g_hSem, 1, NULL); //第二个参数表示更新后这次需要投放的信号量,第三个参数是一个指针,可以获取上一次投放信号的数量
}
}
DWORD dwStart;
void ShowWorkThread()
{
//while(g_nNum != MAX_COUT);
WaitForSingleObject(g_hPostEvent, -1);
DWORD dwEnd = GetTickCount();
printf("g_nNum:%d times:%d\\n", g_nNum, dwEnd - dwStart);
}
int main()
{
g_hPostEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
g_hSem = CreateSemaphore(NULL, 1, 1, NULL); //模拟同步过程,信号量只投放一个信号
//事件对象
//信号量
//通知功能
dwStart = GetTickCount();
std::thread t1(WorkThread);
std::thread t2(WorkThread);
std::thread t3(ShowWorkThread);
t1.join();
t2.join();
SetEvent(g_hPostEvent);
t3.join();
return 0;
}
tips:MFC中控件都没有实现同步问题解决,影响速度效率。MFC中的解决方法是,线程投放消息到主线程,由主线程操作控件,就不会出现多线程同步访问资源的冲突问题。