文档章节

C++11 并发编程教程 - Part 3 : 锁的进阶与条件变量

 天下杰论
发布于 2013/12/29 18:52
字数 2059
阅读 243
收藏 5
点赞 0
评论 0

上一篇文章中我们学习了如何使用互斥量来解决一些线程同步问题。这一讲我们将进一步讨论互斥量的话题,并向大家介绍 C++11 并发库中的另一种同步机制 —— 条件变量。


递归锁

考虑下面这个简单类:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct  Complex {
    std::mutex mutex;
    int  i;
    Complex() : i(0) {}
    void  mul(int  x){
        std::lock_guard<std::mutex> lock(mutex);
        i *= x;
    }
    void  div(int  x){
        std::lock_guard<std::mutex> lock(mutex);
        i /= x;
    }
};

现在你想添加一个操作以便无误地一并执行上述两项操作,于是你添加了一个函数:

1
2
3
4
5
void  both(int  x,  int  y){
    std::lock_guard<std::mutex> lock(mutex);
    mul(x);
    div(y);
}

让我们来测试这个函数:

1
2
3
4
5
int  main(){
    Complex complex;
    complex.both(32, 23);
    return  0;
}

如果你运行上述测试,你会发现这个程序将永远不会结束。原因很简单,在 both() 函数中,线程将申请锁,然后调用 mul() 函数,在这个函数[译注:指 mul() ]中,线程将再次申请该锁,但该锁已经被锁住了。这是死锁的一种情况。默认情况下,一个线程不能重复申请同一个互斥量上的锁。

这里有一个简单的解决办法:std::recursive_mutex 。这个互斥量能够被同一个线程重复上锁,下面就是 Complex 结构体的正确实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct  Complex {
    std::recursive_mutex mutex;
    int  i;
    Complex() : i(0) {}
    void  mul(int  x){
        std::lock_guard<std::recursive_mutex> lock(mutex);
        i *= x;
    }
    void  div(int  x){
        std::lock_guard<std::recursive_mutex> lock(mutex);
        i /= x;
    }
    void  both(int  x,  int  y){
        std::lock_guard<std::recursive_mutex> lock(mutex);
        mul(x);
        div(y);
    }
};

这样一来,程序就能正常的结束了。


计时锁

有些时候,你并不想某个线程永无止境地去等待某个互斥量上的锁。譬如说你的线程希望在等待某个锁的时候做点其他的事情。为了达到这一目的,标准库提供了一套解决方案:std::timed_mutex std::recursive_timed_mutex (如果你的锁需要具备递归性的话)。他们具备与 std::mutex 相同的函数:lock()  unlock(),同时还提供了两个新的函数:try_lock_for()  try_lock_until() 

第一个函数,也是最有用的一个,它允许你设置一个超时参数,一旦超时,就算当前还没有获得锁,函数也会自动返回。该函数在获得锁之后返回 true,否则 false。下面我们来看一个简单示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
std::timed_mutex mutex;
void  work(){
    std::chrono::milliseconds timeout(100);
    while(true){
        if(mutex.try_lock_for(timeout)){
            std::cout << std::this_thread::get_id() <<  ": do work with the mutex"  << std::endl;
            std::chrono::milliseconds sleepDuration(250);
            std::this_thread::sleep_for(sleepDuration);
            mutex.unlock();
            std::this_thread::sleep_for(sleepDuration);
        }  else  {
            std::cout << std::this_thread::get_id() <<  ": do work without mutex"  << std::endl;
            std::chrono::milliseconds sleepDuration(100);
            std::this_thread::sleep_for(sleepDuration);
        }
    }
}
int  main(){
    std::thread  t1(work);
    std::thread  t2(work);
    t1.join();
    t2.join();
    return  0;
}

(这个示例在实践中是毫无用处的)

值得注意的是示例中时间间隔声明:std::chrono::milliseconds 。它同样是 C++11 的新特性。你可以得到多种时间单位:纳秒、微妙、毫秒、秒、分以及小时。我们使用上述某个时间单位以设置try_lock_for() 函数的超时参数。我们同样可以使用它们并通过 std::this_thread::sleep_for() 函数来设置线程的睡眠时间。示例中剩下的代码就没什么令人激动的了,只是一些使得结果可见的打印语句。注意:这段示例永远不会结束,你需要自己把他 kill 掉。


Call Once

有时候你希望某个函数在多线程环境中只被执行一次。譬如一个由两部分组成的函数,第一部分只能被执行一次,而第二部分则在该函数每次被调用时都应该被执行。我们可以使用 std::call_once 函数轻而易举地实现这一功能。下面是针对这一机制的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
std::once_flag flag;
void  do_something(){
    std::call_once(flag, [](){std::cout <<  "Called once"  << std::endl;});
    std::cout <<  "Called each time"  << std::endl;
}
int  main(){
    std::thread  t1(do_something);
    std::thread  t2(do_something);
    std::thread  t3(do_something);
    std::thread  t4(do_something);
    t1.join();
    t2.join();
    t3.join();
    t4.join();
    return  0;
}

每一个 std::call_once 函数都有一个 std::once_flag 变量与之匹配。在上例中我使用了 Lambda 表达式[译注:此处意译]来作为只被执行一次的代码,而使用函数指针以及 std::function 对象也同样可行。


条件变量

条件变量维护着一个线程列表,列表中的线程都在等待该条件变量上的另外某个线程将其唤醒。[译注:原文对于以下内容的阐释有误,故译者参照cppreference.com `条件变量` 一节进行翻译] 每个想要在 std::condition_variable 上等待的线程都必须首先获得一个 std::unique_lock 锁。[译注:条件变量的] wait 操作会自动地释放锁并挂起对应的线程。当条件变量被通知时,挂起的线程将被唤醒,锁将会被再次申请。

一个非常好的例子就是有界缓冲区。它是一个环形缓冲,拥有确定的容量、起始位置以及结束位置。下面就是使用条件变量实现的一个有界缓冲区。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
struct  BoundedBuffer {
    int* buffer;
    int  capacity;
    int  front;
    int  rear;
    int  count;
    std::mutex lock;
    std::condition_variable not_full;
    std::condition_variable not_empty;
    BoundedBuffer(int  capacity) : capacity(capacity), front(0), rear(0), count(0) {
        buffer =  new  int[capacity];
    }
    ~BoundedBuffer(){
        delete[] buffer;
    }
    void  deposit(int  data){
        std::unique_lock<std::mutex> l(lock);
        not_full.wait(l, [&count, &capacity](){return  count != capacity; });
        buffer[rear] = data;
        rear = (rear + 1) % capacity;
        ++count;
        not_empty.notify_one();
    }
    int  fetch(){
        std::unique_lock<std::mutex> l(lock);
        not_empty.wait(l, [&count](){return  count != 0; });
        int  result = buffer[front];
        front = (front + 1) % capacity;
        --count;
        not_full.notify_one();
        return  result;
    }
};

类中互斥量由 std::unique_lock 接管,它是用于管理锁的 Wrapper,是使用条件变量的必要条件。我们使用 notify_one() 函数唤醒等待在条件变量上的某个线程。而函数 wait() 就有些特别了,其第一个参数是我们的 std::unique_lock,而第二个参数是一个断言。要想持续等待的话,这个断言就必须返回false,这就有点像 while(!predicate()) { cv.wait(l); } 的形式。上例剩下的部分就没什么好说的了。

我们可以使用上例的缓冲区解决“多消费者/多生产者”问题。这是一个非常普遍的同步问题,许多线程(消费者)在等待由其他一些线程(生产者)生产的数据。下面就是一个使用这个缓冲区的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
void  consumer(int  id, BoundedBuffer& buffer){
    for(int  i = 0; i < 50; ++i){
        int  value = buffer.fetch();
        std::cout <<  "Consumer "  << id <<  " fetched "  << value << std::endl;
        std::this_thread::sleep_for(std::chrono::milliseconds(250));
    }
}
void  producer(int  id, BoundedBuffer& buffer){
    for(int  i = 0; i < 75; ++i){
        buffer.deposit(i);
        std::cout <<  "Produced "  << id <<  " produced "  << i << std::endl;
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
}
int  main(){
    BoundedBuffer buffer(200);
    std::thread  c1(consumer, 0, std::ref(buffer));
    std::thread  c2(consumer, 1, std::ref(buffer));
    std::thread  c3(consumer, 2, std::ref(buffer));
    std::thread  p1(producer, 0, std::ref(buffer));
    std::thread  p2(producer, 1, std::ref(buffer));
    c1.join();
    c2.join();
    c3.join();
    p1.join();
    p2.join();
    return  0;
}

三个消费者线程和两个生产者线程被创建后就不断地对缓冲区进行查询。值得关注的是例子中使用std::ref 来传递缓冲区的引用,以免造成对缓冲区的拷贝。


总结

这一节我们讲到了许多东西,首先,我们看到如何使用递归锁实现某个线程对同一锁的多次加锁。接下来知道了如何在加锁时设定一个超时属性。然后我们学习了一种调用某个函数有且只有一次的方法。最后我们使用条件变量解决了“多生产者/多消费者”同步问题。


下篇

下一节我们将讲到 C++11 同步库中另一个新特性 —— 原子量。

本文转载自:http://billhoo.blog.51cto.com/2337751/1296334

共有 人打赏支持
粉丝 53
博文 442
码字总数 23359
作品 0
沈阳
项目经理
【C++11 并发编程教程 - Part 3 : 锁的进阶与条件变量(bill译)】

C++11 并发编程教程 - Part 3 : 锁的进阶与条件变量 注:文中凡遇通用的术语及行话,均不予以翻译。译文有不当之处还望悉心指正。 原文:C++11 Concurrency Tutorial – Part 3: Advanced l...

技术小胖子
2017/11/02
0
0
C++11 并发编程教程 - Part 2 : 保护共享数据

上一篇文章我们讲到如何启动一些线程去并发地执行某些操作,虽然那些在线程里执行的代码都是独立的,但通常情况下,你都会在这些线程之间使用到共享数据。一旦你这么做了,就面临着一个新的问...

天下杰论
2013/12/29
0
0
C++11 并发 —— 第一部分:启动线程

C++11 引入一个全新的线程库,包含启动和管理线程的工具,提供了同步(互斥、锁和原子变量)的方法,我将试图为你介绍这个全新的线程库。 如果你要编译本文中的代码,你至少需要一个支持 C+...

红薯
2012/03/22
7.5K
12
C++11 std::unique_lock与std::lock_guard的区别及多线程应用实例

C++11std::uniquelock与std::lockguard的区别及多线程应用实例 C++多线程编程中通常会对共享的数据进行写保护,以防止多线程在对共享数据成员进行读写时造成资源争抢导致程序出现未定义的行为...

moki_oschina
04/10
0
0
关于Java里面多线程同步的一些知识

# 关于Java里面多线程同步的一些知识 对于任何Java开发者来说多线程和同步是一个非常重要的话题。比较好的掌握同步和线程安全相关的知识将使得我们则更加有优势,同时这些知识并不是非常容易...

欧阳海阳
07/13
0
0
【C++11 并发编程教程 - Part 1 : thread 初探(bill译)】

C++11 并发编程教程 - Part 1 : thread 初探 注:文中凡遇通用的术语及行话,均不予以翻译。译文有不当之处还望悉心指正。 原文:C++11 Concurrency - Part 1 : Start Threads C++11 引入了一...

技术小胖子
2017/11/09
0
0
年末干货!Java技术栈2017年度精选干货总结

Java技术栈2017年总结 2017年即将收尾了 这一年,满满的都是干货 这一年,我们的更新不曾停歇 这一年,你装逼内功应已有所成 我是小猿,下面是本年度的分享知识图谱 看完是不是有点蒙逼了?没...

架构之路
2017/12/24
0
0
Java线程面试题 Top 50(转载)

原文链接:http://www.importnew.com/12773.html 本文由 ImportNew - 李 广 翻译自 javarevisited。欢迎加入Java小组。转载请参见文章末尾的要求。 不管你是新程序员还是老手,你一定在面试中...

Dreyer
2015/12/01
70
0
Java线程面试题 Top 50 (转载)

Java线程面试题 Top 50   原文链接:http://www.importnew.com/12773.html   本文由 ImportNew - 李 广 翻译自 javarevisited。欢迎加入Java小组。转载请参见文章末尾的要求。   不管你...

eddie小英俊
2014/01/27
0
0
C++11 并发编程教程 - Part 1 : thread 初探(bill译)

C++11 引入了一个新的线程库,包含了用于启动、管理线程的诸多工具,与此同时,该库还提供了包括互斥量、锁、原子量等在内的同步机制。在这个系列的教程中,我将尝试向大家展示这个新库提供的...

天下杰论
2013/12/29
0
0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

Android 复制和粘贴功能

做了一回搬运工,原文地址:https://blog.csdn.net/kennethyo/article/details/76602765 Android 复制和粘贴功能,需要调用系统服务ClipboardManager来实现。 ClipboardManager mClipboardM...

她叫我小渝
今天
0
0
拦截SQLSERVER的SSL加密通道替换传输过程中的用户名密码实现运维审计(一)

工作准备 •一台SQLSERVER 2005/SQLSERVER 2008服务 •SQLSERVER jdbc驱动程序 •Java开发环境eclipse + jdk1.8 •java反编译工具JD-Core 反编译JDBC分析SQLSERVER客户端与服务器通信原理 SQ...

紅顏為君笑
今天
6
0
jQuery零基础入门——(六)修改DOM结构

《jQuery零基础入门》系列博文是在廖雪峰老师的博文基础上,可能补充了个人的理解和日常遇到的点,用我的理解表述出来,主干出处来自廖雪峰老师的技术分享。 在《零基础入门JavaScript》的时...

JandenMa
今天
0
0
linux mint 1.9 qq 安装

转: https://www.jianshu.com/p/cdc3d03c144d 1. 下载 qq 轻聊版,可在百度搜索后下载 QQ7.9Light.exe 2. 去wine的官网(https://wiki.winehq.org/Ubuntu) 安装 wine . 提醒网页可以切换成中...

Canaan_
今天
0
0
PHP后台运行命令并管理运行程序

php后台运行命令并管理后台运行程序 class ProcessModel{ private $pid; private $command; private $resultToFile = ''; public function __construct($cl=false){......

colin_86
今天
1
0
数据结构与算法4

在此程序中,HighArray类中的find()方法用数据项的值作为参数传递,它的返回值决定是否找到此数据项。 insert()方法向数组下一个空位置放置一个新的数据项。一个名为nElems的字段跟踪记录着...

沉迷于编程的小菜菜
今天
1
1
fiddler安装和基本使用以及代理设置

项目需求 由于开发过程中客户端和服务器数据交互非常频繁,有时候服务端需要知道客户端调用接口传了哪些参数过来,这个时候就需要一个工具可以监听这些接口请求参数,已经接口的响应的数据,这种...

银装素裹
今天
0
0
Python分析《我不是药神》豆瓣评论

读取 Mongo 中的短评数据,进行中文分词 对分词结果取 Top50 生成词云 生成词云效果 看来网上关于 我不是药神 vs 达拉斯 的争论很热啊。关于词频统计就这些,代码中也会完成一些其它的分析任...

猫咪编程
今天
0
0
虚拟机怎么安装vmware tools

https://blog.csdn.net/tjcwt2011/article/details/72638977

AndyZhouX
昨天
1
0
There is no session with id[xxx]

参考网页 https://blog.csdn.net/caimengyuan/article/details/52526765 报错 2018-07-19 23:04:35,330 [http-nio-1008-exec-8] DEBUG [org.apache.shiro.web.servlet.SimpleCookie] - Found......

karma123
昨天
0
0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

返回顶部
顶部