在多线程环境中,当多个线程同时访问共享资源时,由于操作系统CPU调度的缘故,经常会出现一个线程执行到一半突然切换到另一个线程的情况。以多个线程同时对一个共享变量做加法运算为例,自增的汇编指令大致如下,先将变量值存放在某个寄存器中(eax),然后对寄存器进行加一,随后将结果回写到变量内存上

mov [#address#] eax;    // 这里#address#简要表示目标变量的地址   // 1
inc eax;    // 2
mov eax [#address#];    // 3

假设存在两个线程同时对变量a进行加法操作,a初值为0,如果其中一个线程在第一步执行完后被切走,那么最终a的结果可能不是2而是1

由图片可知,由于cpu调度的缘故,多线程下同时对共享变量进行操作,可能会导致最终的结果并不是期望值。所以,为了保护共享变量,保证同一时刻只能允许一个线程对共享变量进行操作,就需要借助互斥量的协助

Linux下的原生互斥量

Linux下提供了原生互斥量api,定义在头文件<pthread.t>中。互斥量,形象点理解就是一把锁,在对共享变量进行操作之前,先上锁,只有获得锁的这个线程能够继续运行,而其他线程运行到上锁语句时,会阻塞在那里直到获得锁的那个线程执行解锁操作,随后继续争抢锁,抢到锁的线程接着执行,没有抢到锁的线程继续阻塞

示例:利用互斥锁解决多线程共享变量问题

在上一篇中提到了创建10个线程同时对一个共享变量进行自增,发现结果和预期不同,接下来利用互斥量解决这一问题

#include <unistd.h>
#include <pthread.h>
#include <sys/types.h>#include <iostream>
#include <vector>long long int total = 0;
pthread_mutex_t m;void* thread_task(void* arg)
{for(int i = 0; i < 10000; ++i){/* 对total进行加法之前先上锁,保证同一时刻只能有一个线程执行++total */::pthread_mutex_lock(&m);++total;/* 解锁 */::pthread_mutex_unlock(&m);}::pthread_exit(nullptr);
}int main()
{/* 初始化互斥量 */::pthread_mutex_init(&m, nullptr);std::vector<pthread_t> tids;for(int i = 0; i < 10; ++i){pthread_t tid;::pthread_create(&tid, nullptr, thread_task, nullptr);tids.emplace_back(tid);}for(auto& tid : tids)::pthread_join(tid, nullptr);/* 释放互斥量 */::pthread_mutex_destroy(&m);std::cout << total << std::endl;return 0;
}

C++11下的互斥量和条件变量

互斥量

对比linux原生的库函数,C++11提供的互斥量突出的特点有

  • 无需考虑互斥量的初始化和销毁,在类的构造和析构函数中管理,无需使用者操心
  • 采用RAII对互斥量进行了不同封装,提供了更加友好的上锁机制

C++11提供的互斥量位于<mutex>头文件中,提供的接口有

  • lock,上锁
  • try_lock,尝试上锁,如果失败则返回false
  • unlock,解锁

这三个函数和linux下的接口差不多,其实也没什么不同嘛~。事实上,多数程序都不直接使用std::mutex,标准库采用RAII(资源获取时就进行初始化)对std::mutex进行了封装,使用起来当然是方便得不得了

简单的锁机制lock_guard

最简单的封装是std::lock_guard,单纯利用RAII,构造时上锁,析构时解锁,使用示例为

#include <iostream>
#include <thread>
#include <mutex>
#include <vector>int main()
{long long int total = 0;std::mutex m;std::vector<std::thread> threads;    for(int i = 0; i < 10; ++i){threads.emplace_back([&m, &total]{for(int i = 0; i < 10000; ++i){{std::lock_guard<std::mutex> lock(m);++total;}}});}for(auto& th : threads)th.join();std::cout << total << std::endl;return 0;
}

想对于共享数据的提供保护,使用std::lock_guard是完全没有问题的,进入共享区前上锁,离开后解锁

更灵活的锁unique_lock

稍微复杂的封装是std::unique_lock,它提供了更灵活的上锁机制,即通过构造函数的参数进行设置,分别可以

  • 直接上锁
  • 延迟上锁,仅保存互斥量,不进行上锁工作
  • 尝试上锁

但是多数情况下采用默认的直接上锁就可以了,而在std::unique_lock的生存期间,使用者也可以对其进行解锁再上锁等工作,这个作用体现在和条件变量的配合上

条件变量

标准库中条件变量位于头文件<condition_variable>中

其中有三个接口用于阻塞当前线程,常用的是wait

void wait(std::unique_lock<std::mutex>& lock);
template <class Predicate>
void wait(std::unique_lock<std::mutex>& lock, Predicate pred);

原子操作释放锁lock,阻塞当前线程,并将当前线程添加到*this上的等待线程列表,等待notify_one或者notify_all调用时结束阻塞(第二个重载当pred返回true时也会结束阻塞)

此外,还有两个接口用于通知一个或多个等待线程,将其从阻塞状态变为非阻塞

void notify_one() noexcept;
void notify_all() noexcept;

示例,利用互斥量和条件变量实现线程池

线程池工作原理

线程池的工作原理是预先创建若干线程,同时维护一个任务队列,每个线程不断地从任务队列中取出任务并执行,使用者可以随时向任务队列中添加新任务。当任务队列为空,线程池中的线程要么执行自己那个没有结束的任务,要么处于睡眠状态

在这个问题模型中,任务队列就相当于共享变量,同一时刻只能有一个线程访问任务队列并从中取出任务,而添加任务时也需要避免添加和取出同时进行,这就需要互斥量的协助,凡是涉及到对任务队列的存和取,都需要事先上锁。

另外,如果任务队列为空,那么每个线程都不断的上锁,取任务(发现为空),解锁,再上锁,取任务(发现为空),解锁…这样的busy loop会极大消耗cpu,造成了不必要的开销,所以需要引入条件变量,当任务队列为空时,采用条件变量令线程睡眠

线程池定义

可以明确的是,线程池除了构造析构函数外,需要提供一个接口用于调用者添加任务,所以线程池的定义可以明确如下

#include <future>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <vector>
#include <queue>class ThreadPool
{public:ThreadPool(std::size_t threadNums);~ThreadPool();void stop() { quit_ = true; }public:/* 用于添加任务,std::future<>用于保存函数f的执行结果 */template <class F, class... Args>auto enqueue(F&& f, Args... args)-> std::future<typename std::result_of<F(Args...)>::type>;private:std::vector<std::thread> threads_;std::queue<std::function<void()>> tasks_;std::atomic<bool> quit_;std::mutex mutex_;std::condition_variable cond_;
};

构造函数

当线程池构造时,创建threadNums个线程,每个线程都从任务队列中取出任务然后执行

ThreadPool::ThreadPool(std::size_t threadNums): quit_(false)
{for(std::size_t i = 0; i < threadNums; ++i){threads_.emplace_back([this]{while(!this->quit_){std::function<void()> task;{std::unique_lock<std::mutex> lock(this->mutex_);/* 利用条件变量,等待直到线程池退出或者任务队列不为空 */cond_.wait(lock, [this]() { return this->quit_ || !this->tasks_.empty(); });if(this->quit_) return;task = this->tasks_.front();this->tasks_.pop();}task();}});     }
}

析构函数

析构函数用于回收线程资源

ThreadPool::~ThreadPool()
{stop();cond_.notify_all();for(auto& th : threads_)th.join();
}

添加任务

enqueue函数用于添加任务,涉及到了一些std::future的内容,这里先简单看看

/* class... 表示不定长参数列表 */
template <class F, class... Args>
/* * auto会根据->后的内容自动推导返回类型* std::future用于保存函数运行结果* std::result_of用于获取函数运行结果* std::packaged_task<T>是一个函数包,类似std::function,用于包装函数* std::packaged_task<T>::get_future用于返回函数运行结果* std::unique_lock<std::mutex> 上锁(这里也可以用std::lock_guard */
auto ThreadPool::enqueue(F&& f, Args... args)-> std::future<typename std::result_of<F(Args...)>::type>
{using return_type = typename std::result_of<F(Args...)>::type;auto task = std::make_shared<std::packaged_task<return_type()>>(std::bind(std::forward<F>(f), std::forward<Args>(args)...));std::future<return_type> res = task->get_future();std::unique_lock<std::mutex> lock(mutex_);tasks_.push([task]() { (*task)(); });return res;
}

测试代码

int main()
{ThreadPool pool(4);std::vector<std::future<int>> results;for(int i = 0; i < 10; ++i){results.emplace_back(pool.enqueue([i]{/* std::this_thread::sleep_for(std::chrono::seconds(1)); */return i * i;}));}for(auto&& result : results)std::cout << result.get() << std::endl;return 0;
}

小结

互斥锁是多线程环境中不可缺少的重要部分,用于保护共享资源免受cpu调度的危害。另外,条件变量和互斥锁配合使用可以避免busy loop带来的不必要损耗

C++11学习笔记-----互斥量以及条件变量的使用相关推荐

  1. 进程通信学习笔记(互斥锁和条件变量)

    1.互斥锁:上锁和解锁 Posix互斥锁作为数据类型pthread_mutex_t的变量声明.如果互斥锁变量是静态分配的,那么可以把它初始化成常值PTHREAD_MUTEX_INITIALIZER.如 ...

  2. 互斥量、条件变量与pthread_cond_wait()函数的使用,详解(二)

    互斥量.条件变量与pthread_cond_wait()函数的使用,详解(二) 1.Linux"线程" 进程与线程之间是有区别的,不过linux内核只提供了轻量进程的支持,未实现线 ...

  3. c++ 互斥量和条件变量

    线程同步时会遇到互斥量和条件变量配合使用的情况,下面看一下C++版的. test.h #include <pthread.h> #include <iostream>class ...

  4. RT-Thread学习笔记——互斥量

    前言 前面学习了RT-Thread的信号量,但信号量在一些场合使用会存在优先级翻转问题,接下来我们学习互斥量,在 RT-Thread 操作系统中,互斥量可以解决优先级翻转问题,实现的是优先级继承算法. ...

  5. 5.FreeRTOS学习笔记- 互斥量

    基本概念 互斥量又称互斥信号量(本质是信号量),是一种特殊的二值信号量 互斥量 支持互斥量所有权.递归访问以及防止优先级翻转的特性,用于实现对临界资源(如显示器.打印机)的独占式访问. 任意时刻互斥量 ...

  6. Linux下互斥量与条件变量详细解析

    1. 首先pthread_cond_wait 的定义是这样的 The pthread_cond_wait() and pthread_cond_timedwait() functions are us ...

  7. 信号灯文件锁linux线程,linux——线程同步(互斥量、条件变量、信号灯、文件锁)...

    一.说明 linux的线程同步涉及: 1.互斥量 2.条件变量 3.信号灯 4.文件读写锁 信号灯很多时候被称为信号量,但个人仍觉得叫做信号灯比较好,因为可以与"SYSTEM V IPC的信 ...

  8. 并发编程(一): POSIX 使用互斥量和条件变量实现生产者/消费者问题

    boost的mutex,condition_variable非常好用.但是在Linux上,boost实际上做的是对pthread_mutex_t和pthread_cond_t的一系列的封装.因此通过对 ...

  9. 一个简单的互斥量与条件变量例子

    #include <pthread.h> #include <stdio.h> #include <stdlib.h> //互斥变量和条件变量静态初始化 pthre ...

最新文章

  1. tornado 09 cookie和session
  2. 【贪心】【P5078】Tweetuzki 爱军训
  3. c++对象长度之静态数据成员(3)
  4. logback 常用配置详解(二) appender
  5. 【机器学习基础】数学推导+纯Python实现机器学习算法13:Lasso回归
  6. 三、mongodb数据库系列——mongodb和python交互 总结
  7. SWIG Python-C封装 char*相关问题(3)
  8. sql 查询一个月的数据按天显示_数据分析-sql复杂查询
  9. 如何使用以太网将 Mac 接入互联网?
  10. arduino+16路舵机驱动板连接测试
  11. 平面设计常用计算机工具,平面设计中常用的计算机软件及其具体使用
  12. 基于springboot旅游系统
  13. QMC5883L 校准方法
  14. 如何通过回测报告中的指标评估策略优劣?
  15. git版本控制gitosis的安装与使用
  16. 在Android面试前背八股和学面试技巧真的有用吗?
  17. matlab画爱心的代码
  18. 北大数学英才班,没有一名新生经历高三
  19. Packets larger than max_allowed_packet are not allowed
  20. Java Web视频(2013)

热门文章

  1. jfinal mysql增删改查_Jfinal简单实现增删改查
  2. Java黑皮书课后题第6章:*6.4(反序显示一个整数)使用下面的方法体编写方法,反序显示一个整数…例如reverse(3456)返回6543,编写一个测试程序,提示用户输入一个整数,然后显示它的反序
  3. 计算机在材料科学的应用论文,计算机在材料科学中的应用论文
  4. 改变 input[type=range] css样式
  5. 十六、python沉淀之路--迭代器
  6. 使用预编译头提高编译速度
  7. Git_git的诞生
  8. .Net 应用框架设计系列(二)
  9. linux进入命令是什么,linux进入目录的命令是什么
  10. OpenGL ES之纹理翻转的解决策略