Dekker互斥算法
核心代码如下:
const int g_totalThreads = 2;
/*实现方法二,Dekker互斥算法*/
/*在下面定义算法相关的全局变量*/
int flag[2]= {0,0}; //表示线程是否想进入临界区
int turn = 0; // 表示轮到的进程编号
//参数id为当前的线程号
void Lock(int id)
{
/*实现方法二,Dekker互斥算法*/
// 首先将flag置为1,想要进入临界区
flag[id] = 1;
//判断对方是否也想进入临界区
int Wait_count = 0;
while(flag[(id^1)])
{
// 轮到了对方,那么自己就因该忙等,为了不干扰对方的进度,将自己的flag先暂时置为0,最后再恢复
if(turn == (id^1))
{
flag[id] = 0;
}
// 忙等。等到自己的轮次,为止。随便记录一下等待次数
while(turn == (id^1))
{
Wait_count++;
};
// 终于轮到了我了。。再把之前的flag变回来
flag[id] = 1;
}
// 顺便输出一下我等了多久qaq
//std::cout<<"I Have waited "<<Wait_count<<" times"<<std::endl;
// 进入临界区。。
}
//参数id为当前的线程号
void unLock(int id)
{
/*实现方法二,Dekker互斥算法*/
//从临界区出来了。。就应该老实点。把位置让出来
turn = (id^1);
// 然后。将flag置为0
flag[id] = 0;
}
实验结果
输出效果 | 算法效率 |
---|---|
![]() | ![]() |
可以看到,算法效率还可以接受。除了第一次会出现忙等很多次。其他情况忙等次数为0
Peterson互斥算法
核心代码如下:
const int g_totalThreads = 2;
/*实现方法二,Dekker互斥算法*/
/*在下面定义算法相关的全局变量*/
int flag[2]= {0,0}; //表示线程是否想进入临界区
int turn = 0; // 表示轮到的进程编号
//参数id为当前的线程号
void Lock(int id)
{
/*实现方法二,Peterson互斥算法*/
// 自己要临界区的时候,同时把turn转给对方。防止两个人同时想进临界区。出现问题
flag[id] = 1;
turn = id^1;
int Wait_count = 0;
// 忙等对方,统计等待次数
while(flag[id^1]&&turn == id^1)
{
Wait_count++;
}
//进入临界区,输出等待次数
//std::cout<<"I Have waited "<<Wait_count<<" times ";
// 进入临界区。。
}
//参数id为当前的线程号
void unLock(int id)
{
/*实现方法二,Peterson互斥算法*/
// 出了临界区,将自己的flag置为0
flag[id] = 0;
}
实验结果:
实验结果 | 算法效率 |
---|---|
![]() | ![]() |
可以看到总体上和Derkker算法效率不相上下,但是编程实现更加简单。
Lamport互斥算法
核心代码:
const int g_totalThreads = 5;
/*实现方法三,Lamport互斥算法*/
/*在下面定义算法相关的全局变量*/
int choosing[g_totalThreads]= {0,0,0,0,0}; //1表示正在抓号,0表示没抓号,或者已经抓完
int number[g_totalThreads] = {0,0,0,0,0};// 表示抓的号数
//参数id为当前的线程号
void Lock(int id)
{
/*实现方法三,Lamport互斥算法*/
//互斥操作,表示当前进程正在抽号
choosing[id] = 1;
// 抽号的时候选择最大的编号,进行加1,保证序列递增
for(int j=0; j<g_totalThreads; j++)
{
if(number[id]<number[j])
{
number[id] = number[j];
}
}
number[id]++;
// 抽号完毕
choosing[id] = 0;
// 开始判断是否进入临界区,以及是否忙等
int Wait_count = 0;
for(int j=0;j<g_totalThreads;j++){
// j 号进程正在抽号,需要忙等
while(choosing[j]){
Wait_count++;
}
// j号进程优先级比较高,需要忙等
while(number[j]!=0 && number[id]==number[j] && id>j){
Wait_count++;
}
}
// 可以进入临界区了
//std::cout<<"I Have waited "<<Wait_count<<" times ";
}
//参数id为当前的线程号
void unLock(int id)
{
/*实现方法三,Lamport互斥算法*/
// 离开临界区
number[id] = 0;
}
实验结果
实验结果 | 算法效率 |
---|---|
![]() | ![]() |
可以看到算法效率比较高,几乎没有忙等。
Eisenberg-Mcguire 互斥算法
核心代码如下:
const int g_totalThreads = 5;
/*实现方法三,Eisenberg-Mcguire互斥算法*/
/*在下面定义算法相关的全局变量*/
#define idle 0 // 进程不想进入临界区
#define want_in 1 // 进程想进如临界区
#define in_cs 2 // 进程想进入临界区,或者已经进入临界区
int turn = 0; // 当前的进程编号
int flag[g_totalThreads] = {0,0,0,0,0}; // 进程的意向
//参数id为当前的线程号
void Lock(int id)
{
/*实现方法三,Eisenberg-Mcguire互斥算法*/
// 当前进程向进入临界区
int j = 0;
int Wait_count = 0;
do
{
flag[id] = want_in;
j = turn;
// 扫描当前进程前面的左右进程,判断自己是否可以进入临界区
while(j!=id)
{
// 如果之前的进程出现了,want_in 或者 in_cs 需要重新开始扫描,即忙等一次
if(flag[j]!=idle)
{
Wait_count++;
j = turn;
}
else
{
// 否则继续向前扫描
j = (j+1)%g_totalThreads;
}
}
//首先将自己的意向置为in_cs,然后扫描前面的进程
flag[id] = in_cs;
j = 0;
// 如果其他所有进程,都不是in_cs才能进入临界区
while(j<g_totalThreads && (j==id || flag[j]!=in_cs))
{
j++;
}
}
while(j<g_totalThreads);
// 这个时候可以进入临界区了
turn = id;
//std::cout<<"I Have waited "<<Wait_count<<" times ";
}
//参数id为当前的线程号
void unLock(int id)
{
/*实现方法三,Eisenberg-Mcguire互斥算法*/
// 离开临界区
// 从turn的下一个开始找第一个想进入临界区的进程
int j = (turn+1)%g_totalThreads;
while(flag[j]==idle){
j = (j+1)%g_totalThreads;
}
// 替换当前的turn
turn = j;
flag[id] = idle;
}
实验结果:
实验结果 | 算法效率 |
---|---|
![]() | ![]() |
通过比较可以发现相对于Lamport算法,Eisenberg-Mcguire算法不仅实现复杂,而且效率相对低下。
四种算法的完整代码如下,删掉对应注释即可
// UserLock.cpp : 定义控制台应用程序的入口点。
//
#include <iostream>
#include <windows.h>
//#include <tchar.h>
//#include <StrSafe.h>
#include <process.h> // For _beginthreadex
#include <math.h>
#include <time.h>
// This macro function calls the C runtime's _beginthreadex function.
// The C runtime library doesn't want to have any reliance on Windows' data
// types such as HANDLE. This means that a Windows programmer needs to cast
// values when using _beginthreadex. Since this is terribly inconvenient,
// I created this macro to perform the casting.
typedef unsigned(__stdcall *PTHREAD_START) (void *);
#define chBEGINTHREADEX(psa, cbStackSize, pfnStartAddr, \
pvParam, dwCreateFlags, pdwThreadId) \
((HANDLE)_beginthreadex( \
(void *) (psa), \
(unsigned) (cbStackSize), \
(PTHREAD_START) (pfnStartAddr), \
(void *) (pvParam), \
(unsigned) (dwCreateFlags), \
(unsigned *) (pdwThreadId)))
//调节线程睡眠的时间区间
const int MIN = 200, MAX = 1000;
volatile LONG g_fShutdown = FALSE; // Signals for threads to die
// Handles to all client threads
HANDLE g_hThreads[MAXIMUM_WAIT_OBJECTS];
//注意,如果要实现Dekker或Peterson互斥算法,需要将线程数量改为2
//注意,如果要实现Lamport或Eisenberg-Mcguire互斥算法,需要将线程数量最少改为5
const int g_totalThreads = 5;
//const int g_totalThreads = 2;
/*实现方法一,利用Windows内核对象*/
//全局锁,以mutex形式实现
HANDLE g_hLock;
/*实现方法二,Dekker或Peterson互斥算法*/
/*在下面定义算法相关的全局变量*/
//int flag[2]= {0,0}; //表示线程是否想进入临界区
//int turn = 0; // 表示轮到的进程编号
/*实现方法三,Lamport或Eisenberg-Mcguire互斥算法*/
/*在下面定义算法相关的全局变量*/
//int choosing[g_totalThreads]= {0,0,0,0,0}; //1表示正在抓号,0表示没抓号,或者已经抓完
//int number[g_totalThreads] = {0,0,0,0,0};// 表示抓的号数
#define idle 0 // 进程不想进入临界区
#define want_in 1 // 进程想进如临界区
#define in_cs 2 // 进程想进入临界区,或者已经进入临界区
int turn = 0; // 当前的进程编号
int flag[g_totalThreads] = {0,0,0,0,0}; // 进程的意向
//参数id为当前的线程号
void Lock(int id)
{
/*****实现某一种算法时,需要将其他算法注释掉*****/
/*实现方法一,利用Windows内核对象*/
//WaitForSingleObject(g_hLock, INFINITE);
/*实现方法二,Dekker或Peterson互斥算法*/
// 首先将flag置为1,想要进入临界区
/*
flag[id] = 1;
//判断对方是否也想进入临界区
int Wait_count = 0;
while(flag[(id^1)])
{
// 轮到了对方,那么自己就因该忙等,为了不干扰对方的进度,将自己的flag先暂时置为0,最后再恢复
if(turn == (id^1))
{
flag[id] = 0;
}
// 忙等。等到自己的轮次,为止。随便记录一下等待次数
while(turn == (id^1))
{
Wait_count++;
};
// 终于轮到了我了。。再把之前的flag变回来
flag[id] = 1;
}
// 顺便输出一下我等了多久qaq
std::cout<<"I Have waited "<<Wait_count<<" times ";
// 进入临界区。。
*/
/*
// 自己要临界区的时候,同时把turn转给对方。防止两个人同时想进临界区。出现问题
flag[id] = 1;
turn = id^1;
int Wait_count = 0;
// 忙等对方,统计等待次数
while(flag[id^1]&&turn == id^1)
{
Wait_count++;
}
//进入临界区,输出等待次数
std::cout<<"I Have waited "<<Wait_count<<" times ";
*/
/*实现方法三,Lamport或Eisenberg-Mcguire互斥算法*/
/*
//互斥操作,表示当前进程正在抽号
choosing[id] = 1;
// 抽号的时候选择最大的编号,进行加1,保证序列递增
for(int j=0; j<g_totalThreads; j++)
{
if(number[id]<number[j])
{
number[id] = number[j];
}
}
number[id]++;
// 抽号完毕
choosing[id] = 0;
// 开始判断是否进入临界区,以及是否忙等
int Wait_count = 0;
for(int j=0;j<g_totalThreads;j++){
// j 号进程正在抽号,需要忙等
while(choosing[j]){
Wait_count++;
}
// j号进程优先级比较高,需要忙等
while(number[j]!=0 && number[id]==number[j] && id>j){
Wait_count++;
}
}
// 可以进入临界区了
//std::cout<<"I Have waited "<<Wait_count<<" times ";
*/
// 当前进程向进入临界区
int j = 0;
int Wait_count = 0;
do
{
flag[id] = want_in;
j = turn;
// 扫描当前进程前面的左右进程,判断自己是否可以进入临界区
while(j!=id)
{
// 如果之前的进程出现了,want_in 或者 in_cs 需要重新开始扫描,即忙等一次
if(flag[j]!=idle)
{
Wait_count++;
j = turn;
}
else
{
// 否则继续向前扫描
j = (j+1)%g_totalThreads;
}
}
//首先将自己的意向置为in_cs,然后扫描前面的进程
flag[id] = in_cs;
j = 0;
// 如果其他所有进程,都不是in_cs才能进入临界区
while(j<g_totalThreads && (j==id || flag[j]!=in_cs))
{
j++;
}
}
while(j<g_totalThreads);
// 这个时候可以进入临界区了
turn = id;
//std::cout<<"I Have waited "<<Wait_count<<" times ";
}
//参数id为当前的线程号
void unLock(int id)
{
/*****实现某一种算法时,需要将其他算法注释掉*****/
/*实现方法一,利用Windows内核对象*/
//ReleaseMutex(g_hLock);
/*实现方法二,Dekker或Peterson互斥算法*/
/*
//从临界区出来了。。就应该老实点。把位置让出来
turn = (id^1);
// 然后。将flag置为0
flag[id] = 0;
*/
// 出了临界区,将自己的flag置为0
//flag[id] = 0;
/*实现方法三,Lamport或Eisenberg-Mcguire互斥算法*/
// 离开临界区
//number[id] = 0;
// 从turn的下一个开始找第一个想进入临界区的进程
int j = (turn+1)%g_totalThreads;
while(flag[j]==idle){
j = (j+1)%g_totalThreads;
}
// 替换当前的turn
turn = j;
flag[id] = idle;
}
DWORD WINAPI ClientThread(PVOID pvParam)
{
int nThreadNum = PtrToUlong(pvParam);
int nRequestNum = 0;
//rand init for different threads
LARGE_INTEGER nFrequency;
if (::QueryPerformanceFrequency(&nFrequency))
{
LARGE_INTEGER nStartCounter;
::QueryPerformanceCounter(&nStartCounter);
::srand((unsigned)nStartCounter.LowPart);
}
while (!g_fShutdown)
{
Lock(nThreadNum);
// Keep track of the current processed element
nRequestNum++;
// Only output first 30 times.
if(nRequestNum < 30)
std::cout << "Thread " << nThreadNum << " get lock!" << std::endl;
unLock(nThreadNum);
// [MIN, MAX]
int t = rand() % (MAX - MIN + 1) + MIN;
/*可以把下面一行注释掉,观察不同的实验结果*/
Sleep(t); // Wait for scheduling another time
}
return(0);
}
int main()
{
g_hLock = CreateMutex(NULL, FALSE, NULL);
DWORD dwThreadID;
// Create threads
int nNumThreads = 0;
for (int x = 0; x < g_totalThreads; x++)
g_hThreads[nNumThreads++] =
chBEGINTHREADEX(NULL, 0, ClientThread, (PVOID)(INT_PTR)x,
0, &dwThreadID);
char a;
// Input any char to quit!
std::cin >> a;
g_fShutdown = TRUE;
// Wait for all the threads to terminate & then cleanup
WaitForMultipleObjects(nNumThreads, g_hThreads, TRUE, INFINITE);
while (nNumThreads--)
CloseHandle(g_hThreads[nNumThreads]);
return 0;
}
思考题
我认为单CPU的互斥算法不能用在多CPU上,因为在多CPU中指令可能并行执行,导致指令的顺序可能按照非预期的顺序执行,进而导致多个进程同时进入一个临界区。
总结
本次实验回顾了学习过的几种经典互斥算法,提高了对算法的认识,从理论层面提升到了基本实践的程度。为进一步的学习打下了良好的基础。