线程同步问题解决

算法介绍:Dekker互斥算法

这个算法的目的是解决临界区问题,即多个进程或线程需要访问共享资源,但是每次只能有一个进程或线程可以访问该资源,以避免数据竞争和不确定行为的发生。

具体来说,Dekker's algorithm解决了以下问题:

  1. 竞争条件(Race Condition):在多线程或多进程的环境中,当两个或多个线程或进程试图同时访问共享资源时,可能会导致竞争条件的发生。竞争条件可能会导致不一致的结果或数据损坏。
  2. 临界区问题(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

当其他进程需要与当前进程抢资源时,可以通过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后,才激活线程),而互斥是为了解决不同线程抢夺相同资源时的同步问题。

CreateEvent

OpenEvent

ResetEvent

SetEvent

事件代码示例:

// 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函数即可

信号量代码示例:

CreateSemaphore

ReleaseSemaphore

OpenSemaphore

信号量:指同时可以存在多个信号的事件,当抢到多个信号时,则有多个线程批量完成相同的人物

// 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中的解决方法是,线程投放消息到主线程,由主线程操作控件,就不会出现多线程同步访问资源的冲突问题。

  • 24
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值