C++11: Mutex和Lock

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

-----------------------------我是分割线-------------------------------------

简单使用Mutex和Lock:

假设我们有 int val 我们想在并发访问(concurrent access)中保护该 val,下面是一个比较粗浅的做法.

int val;
std::mutex valMutex;

以后每次访问 val 都必须锁住 valMutex 以求独占.下面是一个很不粗糙的做法:

//线程1:
//如果线程1先执行了那么线程2就会block直到线程1种的valMutex解锁.
valMutex.lock();

if(val >= 0){
 f(val);

}else{
f(-val);
}

valMutex.unlock();


//线程2:

valMutex.lock();
++val;
valMutex.unlock();

从上面我们可以看出来,凡是可能发生并发访问(concurrent access)的地方都需要使用同一个std::mutex,不论是读写都是如此.但是上面也随之带来了问题,当上面的某个线程throw exception的时候,就会造成资源(比如上面例子中的 val 永远被锁住)。

C++标准库提供了更好的解决方案:

int val;
std::mutex valMutex;

...

std::lock_guard<std::mutex> lg(valMutex); //注意这里
if(val >= 0){
   f(val);

}else{

   f(-val)

}

上面的std::lock_guard保证即当前线程 异常造成生命周期结束 它所管理的Mutex对象也一定会被解锁.但是需要注意的是,这样的lock应该被限制在可能之最短周期内,因为std::lock_guard会阻塞其他代码并行的机会。因此安插大括号令unlock尽量控制在最短的周期内.

int val;
std::mutex valMutex;

...

{
 
 {
    std::lock_guard<std::mutex> lg(valMutex);
    if(val >= 0){
       f(val);
    }else{
       f(-val);
    }
  } //lg在这个大括号结束的时候解锁valMutex.
}

下面我们来看一个完整的例子:

#include <iostream>
#include <future>
#include <mutex>
#include <string>

std::mutex printMutex;

void print(const std::string& str)
{
	std::lock_guard<std::mutex> autoLock(printMutex);
	for (const char& cr : str) {
		std::cout.put(cr);
	}

	std::cout << std::endl;
}

int main()
{
	std::future<void> result1 = std::async(std::launch::async, print, "shihuawoaini");
	std::future<void> result2 = std::async(std::launch::async, print, "marryme");
	
	print("i want use my life to love you");

	return 0;
}

在我自己的电脑上面输出结果是:

"i want use my life to love"

"shihuawoaini"

"marryme"

但是如果没有使用std::lock_gard和std::mutex可能输出的结果就是乱七八糟了.线程1在打印的时候线程2也在打印线程3也在打印于是乎根本没有完整的句子.

递归的(Recursive) Lock:

class DatabaseAccess{
    private:
   
    std::recursive_mutex mutex;
    
 
    public:
    void insertData(...)
   {
     std::lock_guard<std::recursive_mutex> lg(mutex);
     ...
    }

   void createTableAndInsertData()
   {
     std::lock_guard<std::recursive_mutex> lg(mutex);
     insertData(...); //OK不会再锁死了.
    }

上面的例子中std::recursive_mutex在 insertData()调用完后解锁.

尝试性的Lock以及带时间性的Lock:

有时候当前线程想要获得一个lock但是如果不成功的话并不想阻塞(block)当前线程.针对这种情况,mutex提供了成员函数try_lock(),该函数尝试锁住一个std::mutex对象,如果成功就返回true,如果失败就返回false(即使失败了也不会block当前线程).

但是需要注意的是try_lock也可能失败是因为该函数的原因,而并不是因为当前std::mutex被其他线程锁住了.

std::mutex m;

while(m.try_lock() == false){
     doOtherStuff();
}

std::lock_guard<std::mutex> lg(m, std::adopt_lock);
...

为了等待特定长的时间,我们可以选用(带时间性的)std::time_mutex,除此之外还有std::recursive_time_mutex他们都允许你调用try_lock_for()try_lock_until(),用以等待一个时间段,或者等待直到某个时间点.

std::timed_mutex m;

if(m.try_lock_for(std::chrono::seconds(1))){
   std::lock_guard<std::time_mutex> lg(m, std::adopt_lock);

}else{
  doOtherThing();
}

处理多个Lock:

通常一个线程一次只锁定一个std::mutex.但是有时候必须锁住多个mutex。拿前面说讲的各种加锁方式来说,当面临多个互斥量(mutex)的时候就会变的复杂且有风险;你或许取得了第一个的lock,但是却拿不到第二个,这样就发生deadlock.

// std::lock example
#include <iostream>       // std::cout
#include <thread>         // std::thread
#include <mutex>          // std::mutex, std::lock

std::mutex foo,bar;

void task_a () {
  foo.lock(); bar.lock(); 
  std::cout << "task a\n";
  foo.unlock();
  bar.unlock();
}

void task_b () {
  bar.lock(); foo.lock(); 
  std::cout << "task b\n";
  bar.unlock();
  foo.unlock();
}

int main ()
{
  std::thread th1 (task_a);
  std::thread th2 (task_b);

  th1.join();
  th2.join();

  return 0;
}

比如上面的代码就有可能锁死,因为task_a可能锁住了一个std::mutex, task_b可能锁住了另外一个std::mutex.这样两个线程会就相互等待.

为了解决上面例子中的问题C++标准库给出了std::lock():

我们可以这样:

std::mutex m1;
std::mutex m2;

...

{
  std::lock(m1, m2);
  std::lock_guard<std::mutex> lockM1(m1, std::adopt_lock);
  std::lock_guard<std::mutex> lockM2(m2, std::adopt_lock);

   ...

}

 

std::lock

template <class Mutex1, class Mutex2, class... Mutexes>
  void lock (Mutex1& a, Mutex2& b, Mutexes&... cde);

std::lock会锁住(lock)它收到的所有mutex,如果所接受的这些Mutex中如果有的已经被其他线程锁住了,那么会解锁已经被他锁住的Mutex,等他其他线程unlock,且阻塞(blocking)当前线程直到它所接收的这些Mutex全部在当前线程被上锁或者抛出异常.如果是抛出异常会解锁(unlock)所有已经被锁住的Mutex

也就是说std::lock()这个函数主要是用来防止需要在一个线程内锁住(lock)多个mutex的时候产生deadlock.

 

标准库还提供了全局函数 std::try_lock会尝试锁住它收到的所有std::mutex:

std::mutex m1;
std::mutex m2;

int idx = std::try_lock(m1, m2);
if(idx < 0){
 std::lock_guard<std::mutex> lockM1(m1, std::adopt_lock);
 std::lock_guard<std::mutex> lockM2(m2, std::adopt_lock);

}else{
 std::cerr << "could not all mutexs" << id+1 << std::endl;
}

std::try_lock

template <class Mutex1, class Mutex2, class... Mutexes>
  int try_lock (Mutex1& a, Mutex2& b, Mutexes&... cde);

std::try_lock尝试锁住它接收的所有的Mutex对象,如果所接收的所有Mutex并没有被上锁成功(lock),则会返回第一个上锁失败的std::mutex的索引(从0开始计,以参数传递的顺序),而且解锁所有的已经被当前线程通过当前std::try_lock上锁了的Mutex.如果成功则返回-1需要注意的是该函数并不会造成当前线程阻塞(也就是说如果接受的所有Mutex中有的在其他线程已经被block,当前线程也不会等待直到其他线程解锁,std::try_lock会直接返回第一个无法上锁的std::mutex对应的参数列表的下标)。

还有特别需要注意的是无论我们是用std::lock还是std::try_lock都会把这些上过锁完成的std::mutex adopt(过继)给std::lock_guard.

 

class std::lock_guard

接受一个已经在当前线程被上锁(block)了的mutex对象或者接受一个没有上锁的对象在构造函数中并在std::lock_guard的构造函数中给他上锁,如果该mutex对象已经被其他线程上锁了,那么阻塞(block)当前线程直到获得上锁权。

构造函数:


explicit lock_guard (mutex_type& m);
	
lock_guard (mutex_type& m, adopt_lock_t tag);
	
lock_guard (const lock_guard&) = delete;

由class std::lock_guard的构造函数我们可以看出来它是不能被拷贝的,也不能被移动的.如果接受一个已经被上锁(block)的std::mutex的时候需要指出第二个参数std::adopt_lock_t.

 

class std::unique_lock

在了解class std::unique_lock之前我们首先了解一下用于构造函数的tag:

1, 在没有指定任何tag的情况下对当前class std::unique_lock的构造函数运行时候给它所接受的mutex上锁(block),如果当前mutex被其他线程锁住了那么会阻塞当前线程.

2,try_to_lock这个tag, 调用当前接受的mutex的try_lock()函数.(不会造成当前线程的阻塞).

3, deferred_lock这个tag,只是初始化当前class std::unique_lock对象,并不锁住它所接受的mutex参数.

4,adopt_lock这个tag,表明当前class std::unique_lock接受的mutex已经在当前线程被上锁了,现在由当前class std::unique_lock来接手管理权.

unique_lock() noexcept;//默认构造函数不管理任何mutex.

explicit unique_lock (mutex_type& m);//管理一个未被上锁的mutex并在构造函数里给它上锁.如果该mutex在其他线程                                                                            //                                     被锁定了那么阻塞当前线程.

unique_lock (mutex_type& m, try_to_lock_t tag);//调用m的try_lock()函数,不会造成当前线程阻塞.

unique_lock (mutex_type& m, defer_lock_t tag) noexcept;//用m构造当前unique_lock但是不在构造函数中给m上锁

unique_lock (mutex_type& m, adopt_lock_t tag);//m已经在当前线程被上锁了,现在来接手m的管理权.

template <class Rep, class Period>
unique_lock (mutex_type& m, const chrono::duration<Rep,Period>& rel_time);
//注意这个构造函数调用m的try_lock_for()函数,也就是说m只能是std::time_mutex, std::recursive_time_mutex.
//尝试锁住m在规定的时间段内.
//如果m此时没有被任何线程锁住那么直接锁住m.
//如果m此时被其他线程锁住了,且等待超时了,其他线程也没有unlock m.那么接着运行当前线程下面的内容.
//如果m此时被其他线程锁住了,但是很快就unlock m了并没有超过规定时间也接着运行当前线程下面的内容.

template <class Clock, class Duration>
unique_lock (mutex_type& m, const chrono::time_point<Clock,Duration>& abs_time);
//注意这个够长函数调用m的try_lock_until()函数,也就是说m只能是std::time_mutex, std::recursive_time_mutex.
//尝试在规定时间点之前锁住m.
//如果m此时没有被任何其他线程锁住,那么直接锁住m.
//如果m此时被其他线程锁住了,且等待超过了给定的时间点也没有unlock m, 那么接着运行当前线程下面的内容.
//如果m此时被其他线程锁住了,且很快m就被unlock了没有超过规定的时间点也接着运行当前线程下面的内容.

unique_lock (const unique_lock&) = delete;//不能拷贝.

unique_lock (unique_lock&& x);//只支持移动

std::unique_lock::lock

void lock();

对所管理的mutex对象调用lock(),如果该mutex被其他线程在锁着那么阻塞当前线程.

std::unique_lock::try_lock

bool try_lock();

调用所管理的mutex对象的try_lock().

std::unique_lock::try_lock_for

template <class Rep, class Period>
  bool try_lock_for (const chrono::duration<Rep,Period>& rel_time);

调用所管理的mutex对象的try_lock_for(),但是该mutex对象必须支持try_lock_for()。

std::unique_lock::try_lock_until

template <class Clock, class Duration>
  bool try_lock_until (const chrono::time_point<Clock,Duration>& abs_time);

调用所管理的mutex对象的try_lock_until(),但是该对象必须支持try_lock_until().

std::unique_lock::unlock

void unlock();

解锁所管理的mutex对象,如果当前对象被std::unique_lock管理的时候被上锁了那么解锁该对象,如果在当前线程内当前std::unique_lock所管理的对象没被上锁,那么throw exception.

std::unique_lock::mutex

mutex_type* mutex() const noexcept;

该函数返回一个指针所管理的成员函数的指针.需要注意的是:虽然返回了一个指向他所管理的mutex对象的指针,但是它并没有解锁(unlock)它所管理的mutex,也没有释放它对mutex对象的拥有权.

#include <iostream>
#include <thread>
#include <mutex>

class my_mutex : public std::mutex {
private:
	mutable unsigned int id;

public:
	my_mutex(const unsigned int& ID) :id(ID) {}

	my_mutex() = default;
	virtual ~my_mutex() = default;

	unsigned int get_id()const noexcept;
};

unsigned int my_mutex::get_id()const noexcept
{
	return (this->id);
}

my_mutex mutex(101);

void print_ids(const int& id)
{
	std::unique_lock<my_mutex> u_lock(mutex);
	std::cout << "thread# " << id << " locked mutex " << u_lock.mutex()->get_id() << '\n';
}

int main()
{
	std::thread threads[10];

	for (unsigned int i = 0; i < 10; ++i) {
		threads[i] = std::thread(print_ids, i);
	}

	for (std::thread& ref_thread : threads) {
		ref_thread.join();
	}

	return 0;
}

std::unique_lock::release

mutex_type* release() noexcept;

该函数返回他所管理的mutex的指针,返回后不再具有mutex的拥有权,如果返回前该mutex在当前线程被上锁了,那么返回后也处于上锁(lock)状态.

#include <iostream>
#include <thread>
#include <mutex>
#include <vector>

std::mutex mutex;
int count = 0;

void print_count_and_unlock(std::mutex* mutex)
{
	std::cout << "count: " << count << '\n';
	mutex->unlock();
}

void task()
{
	std::unique_lock<std::mutex> u_lock(mutex);
	++count;
	print_count_and_unlock(u_lock.release());
}

int main()
{
	std::vector<std::thread> threads;
	for (int i = 0; i < 10; ++i) {
		threads.push_back(std::thread(task));
	}

	for (std::thread& ref_thread : threads) {
		ref_thread.join();
	}

	return 0;
}

 

转载于:https://my.oschina.net/SHIHUAMarryMe/blog/640057

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
`std::lock_guard<std::mutex>` 和 `std::unique_lock<std::mutex>` 都是用于管理互斥量的 C++ 标准库类型,但它们有一些不同之处。 `std::lock_guard<std::mutex>` 是一个轻量级的互斥量包装器,它在构造时锁住互斥量,在析构时自动释放互斥量。它的作用是确保在作用域中的代码段执行期间,互斥量一直处于锁定状态。由于 `std::lock_guard` 是在构造时锁住互斥量并在析构时释放互斥量,因此它不支持手动解锁和再次锁定。 示例代码如下: ```cpp std::mutex mtx; { std::lock_guard<std::mutex> lock(mtx); // 锁住互斥量 // 在这个作用域内,互斥量一直处于锁定状态 // ... } // 在 lock_guard 对象的析构函数中自动释放互斥量 ``` `std::unique_lock<std::mutex>` 是一个更灵活的互斥量包装器。它也在构造时锁住互斥量,在析构时释放互斥量,但与 `std::lock_guard` 不同,`std::unique_lock` 允许手动解锁和再次锁定互斥量。这使得 `std::unique_lock` 在某些场景下更加灵活,例如等待条件变量满足时再次加锁、支持对互斥量进行尝试锁定等。 示例代码如下: ```cpp std::mutex mtx; std::unique_lock<std::mutex> lock(mtx); // 锁住互斥量 // 在这个作用域内,互斥量一直处于锁定状态 // ... lock.unlock(); // 手动解锁互斥量 // ... lock.lock(); // 再次锁定互斥量 // ... lock.unlock(); // 解锁互斥量 ``` 综上所述,`std::lock_guard` 适用于简单的互斥量保护,而 `std::unique_lock` 更灵活,适用于需要手动解锁和再次锁定的场景,例如等待条件变量、尝试锁定等。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值