[C++11 并发编程] 05 Mutex 基本操作

Mutex是C++中最常见的数据保护机制之一,在访问一块共享数据前,lock mutex,在完成对数据的访问后,unlock mutex。线程库当一个特定mutex被某个线程lock后,其它尝试lock同一个mutex的线程都会被挂起指导这个mutex被unlock。这就保证了所有线程看到的数据都是完整的,不会被修改了一部分的数据。

在C++中,通常我们通过创建std::mutex的实例来获得一个mutex,调用它的成员函数lock()来锁住它,调用unlock来解锁,但实际实现时,并不推荐这么写,因为这需要我们记住所有需要调用unlock()的代码分支,包括异常分支。 C++标准库提供了std::lock_guard模版类,它在构造函数做锁住mutex,在析构函数中解锁,下面是一个使用std::lock_guard来保护被多个线程访问的链表的实例。

#include "stdafx.h"

#include <list>
#include <mutex>
#include <algorithm>

// 全局变量链表
std::list<int> some_list;
// 全局变量mutex
std::mutex some_mutex;

void add_to_list(int new_value)
{
	// 使用lock_guard来保护链表
	std::lock_guard<std::mutex> guard(some_mutex);
	some_list.push_back(new_value);
}
bool list_contains(int value_to_find)
{
	// 使用lock_guard来保护链表
	std::lock_guard<std::mutex> guard(some_mutex);
	return std::find(some_list.begin(), some_list.end(), value_to_find)
		!= some_list.end();
}

#include <iostream>

int main()
{
	add_to_list(42);
	std::cout << "contains(1)=" << list_contains(1) << ", contains(42)=" << list_contains(42) << std::endl;
}

通常,将mutex, add_to_list和list_contains作为某个类的成员变量和成员函数比把它们作为全局变量更好,因为这样,mutex和它要保护的数据的相关性更加明确。添加其它要访问共享数据的成员函数时,也需要在这些新加的函数中创建lock_guard实例。但是,有一点还是需要注意,如果某个成员函数返回一个共享数据的指针或引用,就会导致被保护的数据可能被意外的访问或修改,在设计接口时,应该尽量避免这种情况。

下面一个例子是一个将受保护数据交给用户自定义函数而导致风险的例子:

#include <mutex>

class some_data
{
	int a;
	std::string b;
public:
	void do_something()
	{}
};

class data_wrapper
{
private:
	some_data data;
	std::mutex m;
public:
	template<typename Function>
	void process_data(Function func)
	{
		std::lock_guard<std::mutex> l(m);
		func(data);
	}
};

some_data* unprotected;

void malicious_function(some_data& protected_data)
{
	unprotected = &protected_data;
}

data_wrapper x;

void foo()
{
	x.process_data(malicious_function);
	unprotected->do_something();
}

int main()
{
	foo();
}

在这个例子中,process_data kan看起没啥问题,它使用std::lock_gurad来保护数据,但是调用用户定义的函数func意味着foo可以传入malicious_function来绕过保护,而调用do_something()为不对数据进行lock操作。不仅仅如此,这段代码还有其它问题,比如foo() 中调用unprotected->do_something()也需要对数据进行加锁。对正确的mutex执行加锁操作是所有程序员需要做到的,其基本准则是:

不要将指向受保护数据的指针或引用传递到lock控制范围的外部,比如,通过函数返回值,使用另外的指针或引用来存储受保护数据或将它们作为参数传给其它的函数。

版权声明:本文为博主原创文章,未经博主允许不得转载。

时间: 2025-01-31 10:01:04

[C++11 并发编程] 05 Mutex 基本操作的相关文章

[C++11 并发编程] 06 Mutex race condition

上一节中介绍了mutex的基本使用方法,使用mutex来保护共享数据并不能解决race condition带来的问题,假如我们有一个堆栈数据结构类似于std::stack它提供了5个基本操作push(),pop(),top(),empty(),和size().这里的top()操作返回栈顶元素的拷贝,这样我们就可以使用一个mutex来保护栈内部的数据.但是race codition情况下,虽然使用mutex在stack的每个接口内都对共享数据进行了保护,仍然有问题存在. #include <deq

[C++11 并发编程] 06 - Mutex 死锁

假设有两个线程,在执行某些操作时,都需要锁定一对mutex,线程A锁定了mutex A,而线程B锁定了额mutex B,它们都在等待对方释放另一个mutex,这就会导致这两个线程都无法继续执行.这种情况就是死锁. 避免死锁最简单的方法是总是以相同的顺序对两个mutex进行锁定,比如总是在锁定mutex B之前锁定mutex A,就永远都不会死锁. 假设有一个操作要交换同一个类的两个实例的内容,为了交换操作不被并发修改影响,我们需要锁定这两个实例内部的mutex.但是,如果选定一个固定的顺序来锁定

[C++11 并发编程] 08 - Mutex std::unique_lock

相对于std::lock_guard来说,std::unique_lock更加灵活,std::unique_lock不拥有与其关联的mutex.构造函数的第二个参数可以指定为std::defer_lock,这样表示在构造unique_lock时,传入的mutex保持unlock状态.然后通过调用std::unique_lock对象的lock()方法或者将将std::unique_lock对象传入std::lock()方法来锁定mutex. #include <mutex> class some

Python并发编程05/ 死锁/递归锁/信号量/GIL锁/进程池/线程池

目录 Python并发编程05/ 死锁/递归锁/信号量/GIL锁/进程池/线程池 1.昨日回顾 2.死锁现象与递归锁 2.1死锁现象 2.2递归锁 3.信号量 4.GIL全局解释器锁 4.1背景 4.2为什么加锁 5.GIL与Lock锁的区别 6.验证计算密集型IO密集型的效率 6.1 IO密集型 6.2 计算密集型 7.多线程实现socket通信 7.1服务端 7.2客户端 8.进程池,线程池 Python并发编程05/ 死锁/递归锁/信号量/GIL锁/进程池/线程池 1.昨日回顾 #生产者消

C++11 并发编程基础(一):并发、并行与C++多线程

正文 C++11标准在标准库中为多线程提供了组件,这意味着使用C++编写与平台无关的多线程程序成为可能,而C++程序的可移植性也得到了有力的保证.另外,并发编程可提高应用的性能,这对对性能锱铢必较的C++程序员来说是值得关注的. 回到顶部 1. 何为并发 并发指的是两个或多个独立的活动在同一时段内发生.生活中并发的例子并不少,例如在跑步的时候你可能同时在听音乐:在看电脑显示器的同时你的手指在敲击键盘.这时我们称我们大脑并发地处理这些事件,只不过我们大脑的处理是有次重点的:有时候你会更关注你呼吸的

使用 C++11 并发编程入门

一.认识并发和并行 先将两个概念, 并发与并行 并发:同一时间段内可以交替处理多个操作: 图中整个安检系统是一个并发设计的结构.两个安检队列队首的人竞争这一个安检窗口,两个队列可能约定交替着进行安检,也可能是大家同时竞争安检窗口(通信).后一种方式可能引起冲突:因为无法同时进行两个安检操作.在逻辑上看来,这个安检窗口是同时处理这两个队列 并行:同一时刻内同时处理多个操作: 图中整个安检系统是一个并行的系统.在这里,每个队列都有自己的安检窗口,两个队列中间没有竞争关系,队列中的某个排队者只需等待队

C++11并发编程:原子操作atomic

一:概述 项目中经常用遇到多线程操作共享数据问题,常用的处理方式是对共享数据进行加锁,如果多线程操作共享变量也同样采用这种方式. 为什么要对共享变量加锁或使用原子操作?如两个线程操作同一变量过程中,一个线程执行过程中可能被内核临时挂起,这就是线程切换,当内核再次切换到该线程时,之前的数据可能已被修改,不能保证原子操作. C++11提供了个原子的类和方法atomic,保证了多线程对变量原子性操作,相比加锁机制mutex.locak(),mutex.unlocak(),性能有几倍的提升. 所需头文件

C++11并发编程

C++11开始支持多线程编程,之前多线程编程都需要系统的支持,在不同的系统下创建线程需要不同的API如pthread_create(),Createthread(),beginthread()等.现在C++11中引入了一个新的线程库,C++11提供了新头文件,主要包含 <thread>.<mutex>.<atomic>.<condition_varible>.<future>五个部分;<thread>等用于支持多线程,同时包含了用于启

[C++11 并发编程] 01 - Hello World

C++11标准支持了并发,其中包含了线程管理,共享资源保护,线程间同步操作和底层原子操作等功能.我们先通过一个简单的示例看看C++11标准的多线程程序是什么样的. #include <iostream> #include <thread> // 引用用于管理线程的类的头文件 using namespace std; // 线程的入口函数,程序将在新创建的线程中打印log void hello() { cout << "Hello Concurrent Worl