线程同步是指同一进程中的多个线程互相协调工作从而达到一致性。之所以需要线程同步,是因为多个线程同时对一个数据对象进行修改操作时,可能会对数据造成破坏,下面是多个线程同时修改同一数据造成破坏的例子:

 #include <thread>
#include <iostream> void Fun_1(unsigned int &counter);
void Fun_2(unsigned int &counter); int main()
{
unsigned int counter = ;
std::thread thrd_1(Fun_1, counter);
std::thread thrd_2(Fun_2, counter);
thrd_1.join();
thrd_2.join();
system("pause");
return ;
} void Fun_1(unsigned int &counter)
{
while (true)
{
++counter;
if (counter < )
{
std::cout << "Function 1 counting " << counter << "...\n";
}
else
{
break;
}
}
} void Fun_2(unsigned int &counter)
{
while (true)
{
++counter;
if (counter < )
{
std::cout << "Function 2 counting " << counter << "...\n";
}
else
{
break;
}
}
}

运行结果如图所示:

显然输出的结果存在问题,变量并没有按顺序递增,所以线程同步是很重要的。在这里记录三种线程同步的方式:

  ①使用C++标准库的thread、mutex头文件:

 #include <thread>
#include <mutex>
#include <iostream> void Fun_1();
void Fun_2(); unsigned int counter = ;
std::mutex mtx; int main()
{
std::thread thrd_1(Fun_1);
std::thread thrd_2(Fun_2);
thrd_1.join();
thrd_2.join();
system("pause");
return ;
} void Fun_1()
{
while (true)
{
std::lock_guard<std::mutex> mtx_locker(mtx);
++counter;
if (counter < )
{
std::cout << "Function 1 counting " << counter << "...\n";
}
else
{
break;
}
}
} void Fun_2()
{
while (true)
{
std::lock_guard<std::mutex> mtx_locker(mtx);
++counter;
if (counter < )
{
std::cout << "Function 2 counting " << counter << "...\n";
}
else
{
break;
}
}
}

  这段代码与前面一段代码唯一的区别就是在两个线程关联的函数中加了一句 std::lock_guard<std::mutex> mtx_locker(mtx); 在C++中,通过构造std::mutex的实例来创建互斥元,可通过调用其成员函数lock()和unlock()来实现加锁和解锁,然后这是不推荐的做法,因为这要求程序员在离开函数的每条代码路径上都调用unlock(),包括由于异常所导致的在内。作为替代,标准库提供了std::lock_guard类模板,实现了互斥元的RAII惯用语法(资源获取即初始化)。该对象在构造时锁定所给的互斥元,析构时解锁该互斥元,从而保证被锁定的互斥元始终被正确解锁。代码运行结果如下图所示,可见得到了正确的结果。

  ②使用windows API的临界区对象:

 //header.h
#ifndef CRTC_SEC_H
#define CRTC_SEC_H #include "windows.h" class RAII_CrtcSec
{
private:
CRITICAL_SECTION crtc_sec;
public:
RAII_CrtcSec() { ::InitializeCriticalSection(&crtc_sec); }
~RAII_CrtcSec() { ::DeleteCriticalSection(&crtc_sec); }
RAII_CrtcSec(const RAII_CrtcSec &) = delete;
RAII_CrtcSec & operator=(const RAII_CrtcSec &) = delete;
//
void Lock() { ::EnterCriticalSection(&crtc_sec); }
void Unlock() { ::LeaveCriticalSection(&crtc_sec); }
}; #endif
 //main.cpp
#include <windows.h>
#include <iostream>
#include "header.h" DWORD WINAPI Fun_1(LPVOID p);
DWORD WINAPI Fun_2(LPVOID p); unsigned int counter = ;
RAII_CrtcSec cs; int main()
{
HANDLE h1, h2;
h1 = CreateThread(nullptr, , Fun_1, nullptr, , );
std::cout << "Thread 1 started...\n";
h2 = CreateThread(nullptr, , Fun_2, nullptr, , );
std::cout << "Thread 2 started...\n";
CloseHandle(h1);
CloseHandle(h2);
//
system("pause");
return ;
} DWORD WINAPI Fun_1(LPVOID p)
{
while (true)
{
cs.Lock();
++counter;
if (counter < )
{
std::cout << "Thread 1 counting " << counter << "...\n";
cs.Unlock();
}
else
{
cs.Unlock();
break;
}
}
return ;
} DWORD WINAPI Fun_2(LPVOID p)
{
while (true)
{
cs.Lock();
++counter;
if (counter < )
{
std::cout << "Thread 2 counting " << counter << "...\n";
cs.Unlock();
}
else
{
cs.Unlock();
break;
}
}
return ;
}

  上面的代码使用了windows提供的API中的临界区对象来实现线程同步。临界区是指一个访问共享资源的代码段,临界区对象则是指当用户使用某个线程访问共享资源时,必须使代码段独占该资源,不允许其他线程访问该资源。在该线程访问完资源后,其他线程才能对资源进行访问。Windows API提供了临界区对象的结构体CRITICAL_SECTION,对该对象的使用可总结为如下几步:

  1.InitializeCriticalSection(LPCRITICAL_SECTION lpCriticalSection),该函数的作用是初始化临界区,唯一的参数是指向结构体CRITICAL_SECTION的指针变量。

  2.EnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection),该函数的作用是使调用该函数的线程进入已经初始化的临界区,并拥有该临界区的所有权。这是一个阻塞函数,如果线程获得临界区的所有权成功,则该函数将返回,调用线程继续执行,否则该函数将一直等待,这样会造成该函数的调用线程也一直等待。如果不想让调用线程等待(非阻塞),则应该使用TryEnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection)。

  3.LeaveCriticalSection(LPCRITICAL_SECTION lpCriticalSection),该函数的作用是使调用该函数的线程离开临界区并释放对该临界区的所有权,以便让其他线程也获得访问该共享资源的机会。一定要在程序不适用临界区时调用该函数释放临界区所有权,否则程序将一直等待造成程序假死。

  4.DeleteCriticalSection(LPCRITICAL_SECTION lpCriticalSection),该函数的作用是删除程序中已经被初始化的临界区。如果函数调用成功,则程序会将内存中的临界区删除,防止出现内存错误。

  该段代码的运行结果如下图所示:

  ③使用Windows API的事件对象:

 //main.cpp
#include <windows.h>
#include <iostream> DWORD WINAPI Fun_1(LPVOID p);
DWORD WINAPI Fun_2(LPVOID p); HANDLE h_event;
unsigned int counter = ; int main()
{
h_event = CreateEvent(nullptr, true, false, nullptr);
SetEvent(h_event);
HANDLE h1 = CreateThread(nullptr, , Fun_1, nullptr, , nullptr);
std::cout << "Thread 1 started...\n";
HANDLE h2 = CreateThread(nullptr, , Fun_2, nullptr, , nullptr);
std::cout << "Thread 2 started...\n";
CloseHandle(h1);
CloseHandle(h2);
//
system("pause");
return ;
} DWORD WINAPI Fun_1(LPVOID p)
{
while (true)
{
WaitForSingleObject(h_event, INFINITE);
ResetEvent(h_event);
if (counter < )
{
++counter;
std::cout << "Thread 1 counting " << counter << "...\n";
SetEvent(h_event);
}
else
{
SetEvent(h_event);
break;
}
}
return ;
} DWORD WINAPI Fun_2(LPVOID p)
{
while (true)
{
WaitForSingleObject(h_event, INFINITE);
ResetEvent(h_event);
if (counter < )
{
++counter;
std::cout << "Thread 2 counting " << counter << "...\n";
SetEvent(h_event);
}
else
{
SetEvent(h_event);
break;
}
}
return ;
}

  事件对象是一种内核对象,用户在程序中使用内核对象的有无信号状态来实现线程的同步。使用事件对象的步骤可概括如下:

  1.创建事件对象,函数原型为:

 HANDLE WINAPI CreateEvent(
_In_opt_ LPSECURITY_ATTRIBUTES lpEventAttributes,
_In_ BOOL bManualReset,
_In_ BOOL bInitialState,
_In_opt_ LPCTSTR lpName
);

  如果该函数调用成功,则返回新创建的事件对象,否则返回NULL。函数参数的含义如下:

  -lpEventAttributes:表示创建的事件对象的安全属性,若设为NULL则表示该程序使用的是默认安全属性。

  -bManualReset:表示所创建的事件对象是人工重置还是自动重置。若设为true,则表示使用人工重置,在调用线程获得事件对象所有权后用户要显式地调用ResetEvent()将事件对象设置为无信号状态。

  -bInitialState:表示事件对象的初始状态。如果为true,则表示该事件对象初始时为有信号状态,则线程可以使用事件对象。

  -lpName:表示事件对象的名称,若为NULL,则表示创建的是匿名事件对象。

  2.若事件对象初始状态设置为无信号,则需调用SetEvent(HANDLE hEvent)将其设置为有信号状态。ResetEvent(HANDLE hEvent)则用于将事件对象设置为无信号状态。

  3.线程通过调用WaitForSingleObject()主动请求事件对象,该函数原型如下:

 DWORD WINAPI WaitForSingleObject(
_In_ HANDLE hHandle,
_In_ DWORD dwMilliseconds
);

  该函数将在用户指定的事件对象上等待。如果事件对象处于有信号状态,函数将返回。否则函数将一直等待,直到用户所指定的事件到达。

  该代码的运行结果如下图所示:

  

  ④使用Windows API的互斥对象:

 //main.cpp
#include <windows.h>
#include <iostream> DWORD WINAPI Fun_1(LPVOID p);
DWORD WINAPI Fun_2(LPVOID p); HANDLE h_mutex;
unsigned int counter = ; int main()
{
h_mutex = CreateMutex(nullptr, false, nullptr);
HANDLE h1 = CreateThread(nullptr, , Fun_1, nullptr, , nullptr);
std::cout << "Thread 1 started...\n";
HANDLE h2 = CreateThread(nullptr, , Fun_2, nullptr, , nullptr);
std::cout << "Thread 2 started...\n";
CloseHandle(h1);
CloseHandle(h2);
//
//CloseHandle(h_mutex);
system("pause");
return ;
} DWORD WINAPI Fun_1(LPVOID p)
{
while (true)
{
WaitForSingleObject(h_mutex, INFINITE);
if (counter < )
{
++counter;
std::cout << "Thread 1 counting " << counter << "...\n";
ReleaseMutex(h_mutex);
}
else
{
ReleaseMutex(h_mutex);
break;
}
}
return ;
} DWORD WINAPI Fun_2(LPVOID p)
{
while (true)
{
WaitForSingleObject(h_mutex, INFINITE);
if (counter < )
{
++counter;
std::cout << "Thread 2 counting " << counter << "...\n";
ReleaseMutex(h_mutex);
}
else
{
ReleaseMutex(h_mutex);
break;
}
}
return ;
}

  互斥对象的使用方法和c++标准库的mutex类似,互斥对象使用完后应记得释放。

C++实现线程同步的几种方式的更多相关文章

  1. IOS 多线程,线程同步的三种方式

    本文主要是讲述 IOS 多线程,线程同步的三种方式,更多IOS技术知识,请登陆疯狂软件教育官网. 一般情况下我们使用线程,在多个线程共同访问同一块资源.为保护线程资源的安全和线程访问的正确性. 在IO ...

  2. Java线程同步的四种方式详解(建议收藏)

    ​ Java线程同步属于Java多线程与并发编程的核心点,需要重点掌握,下面我就来详解Java线程同步的4种主要的实现方式@mikechen 目录 什么是线程同步 线程同步的几种方式 1.使用sync ...

  3. C++线程同步的四种方式(Windows)

    为什么要进行线程同步? 在程序中使用多线程时,一般很少有多个线程能在其生命期内进行完全独立的操作.更多的情况是一些线程进行某些处理操作,而其他的线程必须对其处理结果进行了解.正常情况下对这种处理结果的 ...

  4. C++ 线程同步的四种方式

    程之间通信的两个基本问题是互斥和同步. (1)线程同步是指线程之间所具有的一种制约关系,一个线程的执行依赖另一个线程的消息,当它没有得到另一个线程的消息时应等待,直到消息到达时才被唤醒. (2)线程互 ...

  5. windows线程同步的几种方式

    以下为main函数的测试代码 具体线程同步的实现代码请下载:https://github.com/kingsunc/ThreadSynchro #include <stdio.h> #in ...

  6. Linux学习笔记21——线程同步的两种方式

    一  用信号量同步 1 信号量函数的名字都以sem_开头,线程中使用的基本信号量函数有4个 2 创建信号量 #include<semaphore.h> int sem_init(sem_t ...

  7. 【Linux】多线程同步的四种方式

    背景问题:在特定的应用场景下,多线程不进行同步会造成什么问题? 通过多线程模拟多窗口售票为例: #include <iostream> #include<pthread.h> ...

  8. java笔记--关于线程同步(7种同步方式)

    关于线程同步(7种方式) --如果朋友您想转载本文章请注明转载地址"http://www.cnblogs.com/XHJT/p/3897440.html"谢谢-- 为何要使用同步? ...

  9. iOS中保证线程安全的几种方式与性能对比

    来源:景铭巴巴 链接:http://www.jianshu.com/p/938d68ed832c 一.前言 前段时间看了几个开源项目,发现他们保持线程同步的方式各不相同,有@synchronized. ...

随机推荐

  1. 20145307第四次JAVA学习实验报告

    20145307实验四 Android开发基础 实验内容 基于Android Studio开发简单的 Android应用并部署测试; 了解Android组件.布局管理器的使用: 掌握Android中事 ...

  2. optind变量

    1.这个变量是在什么地方定义的? 答:系统定义的 2.这个变量在什么场景下使用? 答:在解析命令行参数时会用到 3.这个变量存在的意义? 在每调用一次getopt()或getopt_long()类似函 ...

  3. 采用OpenReplicator解析MySQL binlog

    Open Replicator是一个用Java编写的MySQL binlog分析程序.Open Replicator 首先连接到MySQL(就像一个普通的MySQL Slave一样),然后接收和分析b ...

  4. Mysql CASE WHEN 用法

    select sum(1) as col_0_0_, sum(case vciinfo.useable when -1 then 1 else 0 end) as col_1_0_, sum(case ...

  5. mathematical method

    mathematical method 曲线拟合 指数 \(lnY = lna + bX\) 对数 \(Y = blnX + a\) 幂函数 \(lgY=lga+blgX\) 多元线性回归模型 回归分 ...

  6. Objective C NSString 编码成URL 特殊字符处理

    找了一下网上的教程都是使用类似以下代码,Xcode提示这个CoreFoundation不受ARC管理,所以折中的方式是添加__bridge. NSString *encodedValue = (__b ...

  7. PAT1075. PAT Judge (25)

    其中在排名输出上参照了 http://blog.csdn.net/xyzchenzd/article/details/27074665,原先自己写的很繁琐,而且还有一个测试点不通过. #include ...

  8. Django框架学习笔记(windows环境下安装)

    博主最近开始学习主流框架django 网上大部分的安装环境都linux的 由于博主在windows环境下已经有了 Pycharm编辑器 ,所以决定还是继续在windows环境下学习 首先是下载 链接 ...

  9. PHP 7.3.0.beta3 发布,下个版本将进入 RC 阶段

    PHP 7.3.0 第三个测试版 beta3 已发布,源码下载地址 >>> https://downloads.php.net/~cmb/ 更新内容如下: - Core: . Fix ...

  10. C++(三十一) — 静态成员变量、静态成员函数

    1.静态成员变量 类的静态成员变量是,由该类的所以对象共同维护和使用的数据成员,每个类只有一个副本,是类数据成员的一种特例.采用 static 关键字声明. 目的:实现同一类中不同对象之间的数据共享. ...