摘要

本篇文章围绕以下几个问题展开:

  1. 进程和线程的区别
  2. 何为并发?C++中如何解决并发问题?C++中多线程的基本操作 浅谈C++11中的多线程(一) - 唯有自己强大 - 博客园 (cnblogs.com)
  3. 同步互斥原理以及如何处理数据竞争
  4. 条件变量和原子操作
  5. Qt中的多线程应用

 一,同步互斥原理

首先说明两个专业名词。

临界资源:对于同一进程的多个线程,进程资源中有些对线程是共享的,但有些资源一次只能供一个线程使用,这样的资源被称为临界资源,也可以叫做互斥资源,即只能被各个线程互斥访问。

临界区:线程中对临界资源实施操作的那段程序,称之为临界区。

  • 同步是指散步在不同任务之间的若干程序片断,它们的运行必须严格按照规定的某种先后次序来运行,这种先后次序依赖于要完成的特定的任务。比如 A 任务的运行依赖于 B 任务产生的数据。

举例:假如程序中有一个静态变量,static int a;线程1负责往里写入数据,线程2需要读取其中的数据,那么线程2在读数据之前必须是线程1写入了数据,如果不是,那么线程2必须停下来等待线程1的操作结束。这就是线程之间在某些地方上的合作关系,协同工作嘛!

  • 互斥是指散步在不同任务之间的若干程序片断,当某个任务运行其中一个程序片段时,其它任务就不能运行它们之中的任一程序片段,只能等到该任务运行完这个程序片段后才可以运行。

举例:还是假如程序中有一个静态变量,static int b;线程1想要往里写入数据,线程2也想要往里写入数据,那么此时静态变量b就是一个临界资源(互斥资源),即一次只能被一个线程访问。

同步跟互斥都是针对于线程来说的,可以把这理解为是线程之间的合作关系和制约关系。可以这样理解,同一进程的各个线程之间不可能是完全独立的,或多或少会有关系,或是合作关系,或是制约关系。具体的例子放在了同步和互斥的介绍中。

二,如何处理数据竞争

从上面数据竞争形成的条件入手,数据竞争源于并发修改同一数据结构,那么最简单的处理数据竞争的方法就是对该数据结构采用某种保护机制,确保只有进行修改的线程才能看到数据被修改的中间状态,从其他访问线程的角度看,修改不是已经完成就是还未开始。

在线程里也有这么一把锁——互斥锁(mutex),互斥锁是一种简单的加锁的方法来控制对共享资源的访问。只要某一个线程上锁了,那么就会强行霸占公共资源的访问权,其他的线程无法访问直到这个线程解锁了,从而保护共享资源。

1️⃣ lock与unlock保护共享资源

Mutex全名mutual exclusion(互斥体),是个object对象,用来协助采取独占排他方式控制对资源的并发访问。这里的资源可能是个对象,或多个对象的组合。为了获得独占式的资源访问能力,相应的线程必须锁定(lock) mutex,这样可以防止其他线程也锁定mutex,直到第一个线程解锁(unlock) mutex。mutex类的主要操作函数见下表:

由图可知互斥锁只有两种状态,即上锁( lock )和解锁( unlock )。mutex不仅提供了常规锁,还为常规锁可能造成的阻塞提供了尝试锁(带时间的锁需要带时间的互斥类timed_mutex支持,具体见下文)。下面先给出一段示例代码:

// mutex1.cpp         通过互斥体lock与unlock保护共享全局变量

#include <chrono>
#include <mutex>
#include <thread>
#include <iostream> std::chrono::milliseconds interval(100); std::mutex mutex;
//两个全局变量
int job_shared = 0; //两个线程都能修改'job_shared',mutex将保护此变量
int job_exclusive = 0; //只有一个线程能修改'job_exclusive',不需要保护 //此线程只能修改 'job_shared'
void job_1()
{
mutex.lock();
std::this_thread::sleep_for(5 * interval); //令‘job_1’持锁等待
++job_shared;
std::cout << "job_1 shared (" << job_shared << ")\n";
mutex.unlock();
} // 此线程能修改'job_shared'和'job_exclusive'
void job_2()
{
while (true) { //无限循环,直到获得锁并修改'job_shared'
if (mutex.try_lock()) { //尝试获得锁成功则修改'job_shared'
++job_shared;
std::cout << "job_2 shared (" << job_shared << ")\n";
mutex.unlock();
return;
}
else { //尝试获得锁失败,接着修改'job_exclusive'
++job_exclusive;
std::cout << "job_2 exclusive (" << job_exclusive << ")\n";
std::this_thread::sleep_for(interval);
}
}
} int main()
{
std::thread thread_1(job_1);
std::thread thread_2(job_2); thread_1.join();
thread_2.join(); getchar();
return 0;
}

从上面的代码看,创建了两个线程和两个全局变量。

  • 全局变量job_exclusive只有job2调用,因此两线程并不共享,不会产生数据竞争,所以不需要锁保护。
  • 另一个全局变量job_shared是两线程共享的,会引起数据竞争,因此需要锁保护。线程thread_1持有互斥锁lock的时间较长,线程thread_2为免于空闲等待,使用了尝试锁try_lock,如果获得互斥锁则操作共享变量job_shared,未获得互斥锁则操作排他变量job_exclusive,提高多线程效率。

输出结果:

2️⃣ lock_guard与unique_lock保护共享资源

但lock与unlock必须成对合理配合使用,使用不当可能会造成资源被永远锁住,甚至出现死锁(两个线程在释放它们自己的lock之前彼此等待对方的lock)。


相关知识点:

是不是想起了C++另一对儿需要配合使用的对象new与delete,若使用不当可能会造成内存泄漏等严重问题,为此C++引入了智能指针shared_ptr与unique_ptr。智能指针借用了RAII技术(Resource Acquisition Is Initialization—使用类来封装资源的分配和初始化,在构造函数中完成资源的分配和初始化,在析构函数中完成资源的清理,可以保证正确的初始化和资源释放)对普通指针进行封装,达到智能管理动态内存释放的效果。


同样的,C++也针对lock与unlock引入了智能锁lock_guard与unique_lock,同样使用了RAII技术对普通锁进行封装,达到智能管理互斥锁资源释放的效果。lock_guard与unique_lock的区别如下:

lock_guard:

unique_lock:

从上面两个支持的操作函数表对比来看,unique_lock功能丰富灵活得多。如果需要实现更复杂的锁策略可以用unique_lock,如果只需要基本的锁功能,优先使用更严格高效的lock_guard。两种锁的简单概述与策略对比见下表:

如果将上面的普通锁lock/unlock替换为智能锁lock_guard,其中job_1函数代码修改如下:

void job_1()
{
std::lock_guard<std::mutex> lockg(mutex); //获取RAII智能锁,离开作用域会自动析构解锁
std::this_thread::sleep_for(5 * interval); //令‘job_1’持锁等待
++job_shared;
std::cout << "job_1 shared (" << job_shared << ")\n";
}

如果也想将job_2的尝试锁try_lock也使用智能锁替代,由于lock_guard锁策略不支持尝试锁,只好使用unique_lock来替代,代码修改如下(其余代码和程序执行结果与上面相同):

void job_2()
{
while (true) { //无限循环,直到获得锁并修改'job_shared'
std::unique_lock<std::mutex> ulock(mutex, std::try_to_lock); //以尝试锁策略创建智能锁
//尝试获得锁成功则修改'job_shared'
if (ulock) {
++job_shared;
std::cout << "job_2 shared (" << job_shared << ")\n";
return;
} else { //尝试获得锁失败,接着修改'job_exclusive'
++job_exclusive;
std::cout << "job_2 exclusive (" << job_exclusive << ")\n";
std::this_thread::sleep_for(interval);
}
}
}

3️⃣timed_mutex与recursive_mutex提供更强大的锁

前面介绍的互斥量mutex提供了普通锁lock/unlock和智能锁lock_guard/unique_lock,基本能满足我们大多数对共享数据资源的保护需求。但在某些特殊情况下,我们需要更复杂的功能,比如某个线程中函数的嵌套调用可能带来对某共享资源的嵌套锁定需求,mutex在一个线程中却只能锁定一次;再比如我们想获得一个锁,但不想一直阻塞,只想等待特定长度的时间,mutex也没提供可设定时间的锁。针对这些特殊需求,< mutex >库也提供了下面几种功能更丰富的互斥类,它们间的区别见下表:

继续用前面的例子,将mutex替换为timed_mutex,将job_2的尝试锁tyr_lock()替换为带时间的尝试锁try_lock_for(duration)。由于改变了尝试锁的时间,所以在真正获得锁之前的尝试次数也有变化,该变化体现在尝试锁失败后对排他变量job_exclusive的最终修改结果或修改次数上。更新后的代码如下所示:

#include <chrono>
#include <mutex>
#include <thread>
#include <iostream> std::chrono::milliseconds interval(100); std::timed_mutex tmutex; int job_shared = 0; //两个线程都能修改'job_shared',mutex将保护此变量
int job_exclusive = 0; //只有一个线程能修改'job_exclusive',不需要保护 //此线程只能修改 'job_shared'
void job_1()
{
std::lock_guard<std::timed_mutex> lockg(tmutex); //获取RAII智能锁,离开作用域会自动析构解锁
std::this_thread::sleep_for(5 * interval); //令‘job_1’持锁等待
++job_shared;
std::cout << "job_1 shared (" << job_shared << ")\n";
} // 此线程能修改'job_shared'和'job_exclusive'
void job_2()
{
while (true) { //无限循环,直到获得锁并修改'job_shared'
std::unique_lock<std::timed_mutex> ulock(tmutex,std::defer_lock); //创建一个智能锁但先不锁定
//尝试获得锁成功则修改'job_shared'
if (ulock.try_lock_for(3 * interval)) { //在3个interval时间段内尝试获得锁
++job_shared;
std::cout << "job_2 shared (" << job_shared << ")\n";
return;
} else { //尝试获得锁失败,接着修改'job_exclusive'
++job_exclusive;
std::cout << "job_2 exclusive (" << job_exclusive << ")\n";
std::this_thread::sleep_for(interval);
}
}
} int main()
{
std::thread thread_1(job_1);
std::thread thread_2(job_2); thread_1.join();
thread_2.join(); getchar();
return 0;
}


前章解答

前一篇文章中最后的程序运行结果可能会出现某行与其他行交叠错乱的情况,主要是由于不止一个线程并发访问了std::cout显示终端资源导致的,解决方案就是对cout << “somethings” << endl语句加锁,保证多个线程对cout资源的访问同步。为了尽可能降低互斥锁对性能的影响,应使用微粒锁,即只对cout资源访问语句进行加锁保护,cout资源访问完毕尽快解锁以供其他线程访问该资源。添加互斥锁保护后的代码如下:

//thread2.cpp  增加对cout显示终端资源并发访问的互斥锁保护

#include <iostream>
#include <thread>
#include <chrono>
#include <mutex> using namespace std; std::mutex mutex1; void thread_function(int n)
{
std::thread::id this_id = std::this_thread::get_id(); //获取线程ID for (int i = 0; i < 5; i++) {
mutex1.lock();
cout << "子线程" << this_id << " 运行 : " << i + 1 << endl;
mutex1.unlock();
std::this_thread::sleep_for(std::chrono::seconds(n)); //进程睡眠n秒
}
} class Thread_functor
{
public:
// functor行为类似函数,C++中的仿函数是通过在类中重载()运算符实现,使你可以像使用函数一样来创建类的对象
void operator()(int n)
{
std::thread::id this_id = std::this_thread::get_id(); for (int i = 0; i < 5; i++) {
{
std::lock_guard<std::mutex> lockg(mutex1);
cout << "子仿函数线程" << this_id << " 运行: " << i + 1 << endl;
}
std::this_thread::sleep_for(std::chrono::seconds(n)); //进程睡眠n秒
}
}
}; int main()
{
thread mythread1(thread_function, 1); // 传递初始函数作为线程的参数
if (mythread1.joinable()) //判断是否可以成功使用join()或者detach(),返回true则可以,返回false则不可以
mythread1.join(); // 使用join()函数阻塞主线程直至子线程执行完毕 Thread_functor thread_functor;
thread mythread2(thread_functor, 3); // 传递初始函数作为线程的参数
if (mythread2.joinable())
mythread2.detach(); // 使用detach()函数让子线程和主线程并行运行,主线程也不再等待子线程 auto thread_lambda = [](int n) {
std::thread::id this_id = std::this_thread::get_id();
for (int i = 0; i < 5; i++)
{
mutex1.lock();
cout << "子lambad线程" << this_id << " 运行: " << i + 1 << endl;
mutex1.unlock();
std::this_thread::sleep_for(std::chrono::seconds(n)); //进程睡眠n秒
}
}; thread mythread3(thread_lambda, 4); // 传递初始函数作为线程的参数
if (mythread3.joinable())
mythread3.join(); // 使用join()函数阻塞主线程直至子线程执行完毕 unsigned int n = std::thread::hardware_concurrency(); //获取可用的硬件并发核心数
mutex1.lock();
std::cout << n << " 支持并发线程" << endl;
mutex1.unlock();
std::thread::id this_id = std::this_thread::get_id(); for (int i = 0; i < 5; i++) {
{
std::lock_guard<std::mutex> lockg(mutex1);
cout << "主线程" << this_id << " 运行: " << i + 1 << endl;
}
std::this_thread::sleep_for(std::chrono::seconds(1));
} getchar();
return 0;
}

多次运行结果:(已解决)

参考博文:(2条消息) C++多线程并发(二)---线程同步之互斥锁_流云-CSDN博客_c++多线程互斥

浅谈C++11中的多线程(二)的更多相关文章

  1. 浅谈C++11中的多线程(三)

    摘要 本篇文章围绕以下几个问题展开: 进程和线程的区别 何为并发?C++中如何解决并发问题?C++中多线程的基本操作 浅谈C++11中的多线程(一) - 唯有自己强大 - 博客园 (cnblogs.c ...

  2. 浅谈C++11中的多线程(一)

    摘要 本篇文章围绕以下几个问题展开: 进程和线程的区别 何为并发?C++中如何解决并发问题?C++中多线程的基本操作 同步互斥原理以及多进程和多线程中实现同步互斥的两种方法 Qt中的多线程应用 c++ ...

  3. 转: 浅谈C/C++中的指针和数组(二)

    转自:http://www.cnblogs.com/dolphin0520/archive/2011/11/09/2242419.html 浅谈C/C++中的指针和数组(二) 前面已经讨论了指针和数组 ...

  4. c#Winform程序调用app.config文件配置数据库连接字符串 SQL Server文章目录 浅谈SQL Server中统计对于查询的影响 有关索引的DMV SQL Server中的执行引擎入门 【译】表变量和临时表的比较 对于表列数据类型选择的一点思考 SQL Server复制入门(一)----复制简介 操作系统中的进程与线程

    c#Winform程序调用app.config文件配置数据库连接字符串 你新建winform项目的时候,会有一个app.config的配置文件,写在里面的<connectionStrings n ...

  5. 转:浅谈C/C++中的指针和数组(一)

    再次读的时候实践了一下代码,结果和原文不一致 error C2372: 'p' : redefinition; different types of indirection 不同类型的间接寻址 /// ...

  6. 转载 浅谈C/C++中的static和extern关键字

    浅谈C/C++中的static和extern关键字 2011-04-21 16:57 海子 博客园 字号:T | T   static是C++中常用的修饰符,它被用来控制变量的存贮方式和可见性.ext ...

  7. 浅谈 Swift 2 中的 Objective-C 指针

    浅谈 Swift 2 中的 Objective-C 指针 2015-09-07  499 文章目录 1. 在 Swift 中读 C 指针 2. 在 Swift 中创建 C 指针 3. 总结 作者:Ja ...

  8. 【VS开发】【DSP开发】浅谈Linux PCI设备驱动(二)

    我们在 浅谈Linux PCI设备驱动(一)中(以下简称 浅谈(一) )介绍了PCI的配置寄存器组,而Linux PCI初始化就是使用了这些寄存器来进行的.后面我们会举个例子来说明Linux PCI设 ...

  9. 浅谈:Redis持久化机制(二)AOF篇

    浅谈:Redis持久化机制(二)AOF篇 ​ 上一篇我们提及到了redis的默认持久化方式RDB,是一种通过存储快照数据方式持久化的机制,它在宕机后会丢失掉最后一次更新RDB文件后的数据,这也是由于它 ...

随机推荐

  1. MYSQL数据库快速添加测试数据方法

    先添加两条数据,这里最好添加两条不一样的数据 INSERT INTO "表名"(**, **) VALUES (**,**),(**,**) 然后再次添加数据,这次把查询到的数据添 ...

  2. Spring Boot自定义属性配置文件开启提示

    前言:有时候在Sping Boot项目中需要自定义属性.又想在配置文件(*.properties)中显示提示时. 测试环境:Sping Boot2x + Maven + Lombok 准备测试的配置类 ...

  3. 揭秘有状态服务上 Kubernetes 的核心技术

    背景 随着 Kubernetes 成为云原生的最热门的解决方案,越来越多的传统服务从虚拟机.物理机迁移到 Kubernetes,各云厂商如腾讯自研上云也主推业务通过Kubernetes来部署服务,享受 ...

  4. C语言编译器开发之旅(一):词法分析扫描器

    本节我们先从一个简易的可以识别四则运算和整数值的词法分析扫描器开始.它实现的功能也很简单,就是读取我们给定的文件,并识别出文件中的token将其输出. 这个简易的扫描器支持的词法元素只有五个: 四个基 ...

  5. Python+Selenium学习笔记1 - pip命令

    1.用pip命令安装模块 pip install 模块名 e.g. pip install qrcode 2.用pip卸载模块 pip uninstall 模块名 e.g. pip uninstall ...

  6. MindSpore基本原理

    MindSpore基本原理 MindSpore介绍 自动微分 自动并行 安装 pip方式安装 源码编译方式安装 Docker镜像 快速入门 文档 MindSpore介绍 MindSpore是一种适用于 ...

  7. GPU编程和流式多处理器(五)

    GPU编程和流式多处理器(五) 4. 条件代码 硬件实现了"条件代码"或CC寄存器,其中包含用于整数比较的常用4位状态向量(符号,进位,零,溢出).可以使用比较指令(例如ISET) ...

  8. 深度学习Dropout技术分析

    深度学习Dropout技术分析 什么是Dropout? dropout是指在深度学习网络的训练过程中,对于神经网络单元,按照一定的概率将其暂时从网络中丢弃.注意是暂时,对于随机梯度下降来说,由于是随机 ...

  9. springboot2.x整合tkmapper

    springboot整合tkmapper 1.导入pom依赖 1.1 导入springboot的parent依赖 <parent> <artifactId>spring-boo ...

  10. .NET平台系列31:.NET团队送给.NET开发人员的云原生学习资源汇总

    系列目录     [已更新最新开发文章,点击查看详细] .NET Core 启动于2016年,跟K8S同年诞生,既拥有着悠久的历史积累,又集成了当下最新的设计理念,加上.NET团队持续对容器技术的官方 ...