同步锁和条件变量

本文详细介绍了C++中Mutex和Lock的使用,包括基本的mutex用例、递归Lock解决自调用问题、常识性Lock与带时间限制的Lock、处理多个Lock的策略、unique_lock的使用及其构造函数,以及条件变量在多线程同步中的应用。文章通过示例代码解释了各种锁机制的工作原理和避免死锁的方法。
摘要由CSDN通过智能技术生成

一.C++ Mutex 和 Lock

为了独占式的获得资源访问的能力,相应的线程必须锁定(lock)mutex,这样可以防止其他线程也锁定 mutex,直到第一个线程解锁(unlock)mytex.

1.1mutex 第一个完整用例

#include<iostream>
#include<future>
#include<mutex>
#include<iostream>
#include<unistd.h>
#include<string>

std::mutex printMutex;

void print(const std::string& s)
{
    std::lock_guard<std::mutex> l(printMutex);
    int count = 5;
    for(char c:s)
    {
        count--;
        if(count == 0)
        {
            sleep(1);
        }
        std::cout.put(c);
    }
    std::cout << std::endl;
}

int main()
{   
    auto f1 = std::async(std::launch::async,print,"Hello from a first thread");
    auto f2 = std::async(std::launch::async,print,"Hello from a second thread");
    print("hello from thr main thread");

    return 0;
}

若是没有 std::lock_guardstd::mutex l(printMutex);
可能会出现或其他样的打印

hellHellHello from thr main thread
o from a first thread
o from a second thread

加上,现在输出是这样:

hello from thr main thread
Hello from a second thread
Hello from a first thread

只不过三者输出顺序可能会改变

1.2 递归的(Recursive)Lock

有时候,递归锁定是必要的,典型的例子是 active object 或 monitor。例如一个数据库的接口可能是这样的:

class DatabaseAccess
{
	private:
		std:;mutex dbMutex;
		...//state of database access
	public:
		void createTable(...)
		{
			std:;lock_guard(std:;mutex) lg(dbMutex);
			...
		}
		
		void insertData(...)
		{
			std:;lock_guard(std:;mutex) lg(dbMutex);
			...
		}
		...
};

当我们引入一个public 成员函数而它可能调用其他public成员函数,情况就会变得很复杂。

void createTableAndInsertData(...)
{
	std:;lock_guard<std::mutex> lg(dbMutex);
	...
	createTable(...); //Error:deadlock because dbMutex is locked again.
}

这个就额可以借助 recursive_mutex 解决,mutex 允许同一线程多次锁定,并在最近一次(lase)相应的unlock()时释放lock.

class DatabaseAccess
{
	private:
		std::recursive_mutexdbMutex;
		...//state of database access
	public:
		void createTable(...)
		{
			std:;lock_guard(std::recursive_mutex) lg(dbMutex);
			...
		}
		
		void insertData(...)
		{
			std:;lock_guard(std::recursive_mutex) lg(dbMutex);
			...
		}
		void createTableAndInsertData(...)
		{
			std:;lock_guard<std::recursive_mutex> lg(dbMutex);
			...
			createTable(...); //OK:no deadlock
		}
		...
};

例子:

#include<iostream>
#include<future>
#include<mutex>
#include<iostream>
#include<unistd.h>
#include<string>


class Demo
{

    private:
        std::mutex dbMutex;

    public:
        void A()
        {
            std::lock_guard<std::mutex> l(dbMutex);
            std::cout << "this is A" << std::endl;
        }

        void B()
        {
            std::lock_guard<std::mutex> l(dbMutex);
            std::cout << "this is B" << std::endl;
        }

        void C()
        {
            std::lock_guard<std::mutex> l(dbMutex);
            A();
        }

};

int main()
{   
    Demo d;
    d.C();

    return 0;
}

不行,会死锁

#include<iostream>
#include<future>
#include<mutex>
#include<iostream>
#include<unistd.h>
#include<string>


class Demo
{

    private:
        std::recursive_mutex dbMutex;

    public:
        void A()
        {
            std::lock_guard<std::recursive_mutex> l(dbMutex);
            std::cout << "this is A" << std::endl;
        }

        void B()
        {
            std::lock_guard<std::recursive_mutex> l(dbMutex);
            std::cout << "this is B" << std::endl;
        }

        void C()
        {
            std::lock_guard<std::recursive_mutex> l(dbMutex);
            A();
        }

};

int main()
{   
    Demo d;
    d.C();

    return 0;
}

OK 正常输出

1.3 常识性的 Lock 和 带时间的 Lock

有时候程序想要获得一个Lock但是如果不可能成功的话它不想永远 Block(阻塞)。针对这种情况,mutex提供成员函数 try_lock(),它试图取得一个Lock,成功就返回true,失败就返回false.
为了仍能使用lock_guard(使当前作用域下的)任何出口都会自动unlock_mutex,你可以传一个额外实参adopt_lock给其构造函数:

std::mutex m;

//try to acquire a lock and do othre stuff while this isn't possible
while(m.try_lock() == false)
{
	doSomeOtherStuff();
}
std::lock_guard<std::mutex> lg(m,std:;adopt_lock);

为了等待特定长度的时间,你可以选用(带时间性的)所谓time_mutex.有两个特殊mutex_class std::timed_mutex 和 std::recursive_timed_mutex 额外允许你调用try_lock_for() 或 try_lock_until(),用以等待某个时间段,或直至抵达某个时间点。这对于实施需求(real-time requirement)或避免可能的deadline或许有帮助。

std::timed_mutex m;

//try for one second to acquire a lock
if(m.try_lock_for(std::chrono::seconds(1)))
{
	std::lock_guard<std::timed_mutex> lg(m,std::adopt_lock);
	...
}else{
	coundNotGetTheLock();
	...
}

1.4 处理多个Lock

这种情况下若是以之前介绍过的lokc来处理,可能变得复杂且具有风险:或者你取得了第一个lock却拿不到第二个lock,或者发生死锁(若是以不同的次序去锁住相同的lock)

C++标准库提供了若干便捷函数,让你锁定多个mutex

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);
	...
}//automatically unlock all mutexes

全局函数 std:;lock()会锁住他收到的所有mutex,而且阻塞直到所有mutex都被锁定或发生异常。如果是后者,已被成功锁定的mutex都会被解锁。一如以往,成功锁定之后应该使用lock_guard,并且以adpot_lock作为第二实参,保证任何情况下这些mutex在离开作用域之后都会被解锁。

以此方式,你可以尝试“取得多个lock” 且 “若非所有lock都可以用也不至于造成阻塞”。全局函数std::try_lock()会在取得所有lock的情况下返回-1,否则返回第一个失败的lock的索引(从0开始计)。且如果这样的话所有成功的lock又会被unlock。

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

int idx = std::try_lock(m1,m2);
if(idx < 0)
{
	//both locks successed
	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 lock mutex m" << idx+1 << std::endl;
}

注:这个try_lock()不提供deadlock回避机制,但他保证以出现于实参的次序来试着完成锁定。

1.5 unique_lock

参考:unique_lock
提供的接口与class lock_guard<> 相同,又允许明确写出“何时”以及“如何”锁定或解锁其mutex.
对于Unique_lock ,你可以调用owns_lock() 或 bool() 来查询其mutex目前是否被锁住

优点:
如果析构时mutex仍然被锁住,其析构函数会自动调用unlock(),如果当时没有锁住mutex,则析构函数不会做任何事情。

1.6 三种构造函数
  • try_to_lock,尝试锁定mutex但不希望阻塞
std::unique_lock<std::mutex> lock(mutex,std::try_to_lock);
#include<iostream>
#include<thread>
#include<string>
#include<vector>
#include<list>
#include<mutex>
 
using namespace std;
 
class A
{
public:
	void inMsgRecvQueue()
	{
		for (int i = 0; i < 10000; i++)
		{
			cout << "inMsgRecvQueue()执行,插入一个元素" << i << endl;
			{ 
				std::unique_lock<std::mutex> sbguard(my_mutex, std::try_to_lock);
				if (sbguard.owns_lock())
				{
					//拿到了锁
					msgRecvQueue.push_back(i); 
					//...
					//其他处理代码
				}
				else
				{
					//没拿到锁
					cout << "inMsgRecvQueue()执行,但没拿到锁头,只能干点别的事" << i << endl;
				}
			}
		}
	}
 
	bool outMsgLULProc(int &command)
	{
		my_mutex.lock();//要先lock(),后续才能用unique_lock的std::adopt_lock参数
		std::unique_lock<std::mutex> sbguard(my_mutex, std::adopt_lock);
 
		std::chrono::milliseconds dura(20000);
		std::this_thread::sleep_for(dura);  //休息20s
 
		if (!msgRecvQueue.empty())
		{
			//消息不为空
			int command = msgRecvQueue.front();//返回第一个元素,但不检查元素是否存在
			msgRecvQueue.pop_front();//移除第一个元素。但不返回;
			
			return true;
		}
		return false;
	}
	//把数据从消息队列取出的线程
	void outMsgRecvQueue()
	{
		int command = 0;
		for (int i = 0; i < 10000; i++)
		{
			bool result = outMsgLULProc(command);
 
			if (result == true)
			{
				cout << "outMsgRecvQueue()执行,取出一个元素" << endl;
				//处理数据
			}
			else
			{
				//消息队列为空
				cout << "inMsgRecvQueue()执行,但目前消息队列中为空!" << i << endl;
			}
		}
		cout << "end!" << endl;
	}
 
private:
	std::list<int> msgRecvQueue;//容器(消息队列),代表玩家发送过来的命令。
	std::mutex my_mutex;//创建一个互斥量(一把锁)
};
 
int main()
{
	A myobja;
 
	std::thread myOutMsgObj(&A::outMsgRecvQueue, &myobja);
	std::thread myInMsgObj(&A::inMsgRecvQueue, &myobja);
 
	myOutMsgObj.join();
	myInMsgObj.join();
 
	cout << "主线程执行!" << endl;
 
	return 0;
}
  • 传递时间,尝试在时间周期内锁定
std:;unique_lock<std::mutex> lock(mytex,std::chrono::seconds(1));
  • defer_lock,初始化lock objece,但尚未打算锁住mutex
std::unique_lock<std::mutex> lock(mytex,std::defer_lock);
...
lock.lock();
..
void inMsgRecvQueue()
	{
		for (int i = 0; i < 10000; i++)
		{
			cout << "inMsgRecvQueue()执行,插入一个元素" << i << endl;
			std::unique_lock<std::mutex> sbguard(my_mutex, std::defer_lock);//没有加锁的my_mutex
			sbguard.lock();//咱们不用自己unlock
			//处理共享代码
 
		    //因为有一些非共享代码要处理
			sbguard.unlock();
			//处理非共享代码要处理。。。
 
			sbguard.lock();
			//处理共享代码
 
			msgRecvQueue.push_back(i);
			//...
			//其他处理代码
			sbguard.unlock();//画蛇添足,但也可以
		}
	}
1.7 unique_lock所有权的传递

std::unique_lockstd::mutex sbguard(my_mutex);//所有权概念

sbguard拥有my_mutex的所有权;sbguard可以把自己对mutex(my_mutex)的所有权转移给其他的unique_lock对象;

所以unique_lock对象这个mutex的所有权是可以转移,但是不能复制。

std::unique_lockstd::mutex sbguard1(my_mutex);

std::unique_lockstd::mutex sbguard2(sbguard1);//此句是非法的,复制所有权是非法的

std::unique_lock<std::mutex> sbguard2(std::move(sbguard));//移动语义,现在先当与sbguard2与my_mutex绑定到一起了
 
//现在sbguard1指向空,sbguard2指向了my_mutex

方法1 :std::move()

方法2:return std:: unique_lockstd::mutex 代码如下:

std::unique_lock<std::mutex> rtn_unique_lock()
	{
		std::unique_lock<std::mutex> tmpguard(my_mutex);
		return tmpguard;//从函数中返回一个局部的unique_lock对象是可以的。三章十四节讲解过移动构造函数。
		//返回这种举报对象tmpguard会导致系统生成临时unique_lock对象,并调用unique_lock的移动构造函数
	}
 
	void inMsgRecvQueue()
	{
		for (int i = 0; i < 10000; i++)
		{
			std::unique_lock<std::mutex> sbguard1 = rtn_unique_lock();
 
			msgRecvQueue.push_back(i);
		}
	}

二.条件变量

它可以用来同步化线程之间的数据流逻辑依赖关系。
原则上,condition variable 运作如下。

  • 你必须同时包含 和 <condition_variable>,并声明一个mutex和 一个condition variable
#include <mutex>
#include <condition_variable>

std::mutex readyMutex;
std::condition_variable readyCondVar;
  • 那个激发“条件满足”的线程必须调用 readCondVar.notify_one() 或者 readyCondVar.notify_all();
  • 那个"等待条件被满足"的线程必须调用:
    std::unique_lockstd::mutex l(readyMutex);
    readCondVar.wait(l);

注意:
1.为了等待这个 condition_variable,你需要一个mutex 和 一个unique_lock.(lock_guard 是不够的,因为等待中的函数有可能锁定或解除mutex).

2.假醒:某个condition variable 的wait动作有可能在condition variable 尚未被notified时便返回。

代码:

#include<condition_variable>
#include<mutex>
#include<future>
#include<iostream>

bool readFlag;
std::mutex readMutex;
std::condition_variable readyCondVar;

void thread1()
{
    //do something thread2 needs as preparation
    std::cout << "return ,," << std::endl;
    std::cin.get();

    //signal that thread1 has prepared a condition
    {
        std::lock_guard<std::mutex> lg(readMutex);
        readFlag = true;
    }//release lock

    readyCondVar.notify_one();

}

void thread2()
{
    //wait untion thread1 is read (readyFlag is true)
    std::unique_lock<std::mutex> ul(readMutex);
    readyCondVar.wait(ul,[]{return readFlag;}); //检验第二参数,直到readFlag为true,处理假醒

    //do whatever
    std::cout << "done" << std::endl;


}


int main()
{
    auto f1 = std::async(std::launch::async,thread1);
    auto f2 = std::async(std::launch::async,thread2);

    return 0;
}

例子二:用condition variable 实现多线程queue

#include<condition_variable>
#include<mutex>
#include<future>
#include<iostream>
#include<queue>

std::queue<int> queue;
std::mutex queueMutex;
std::condition_variable queueCondVar;

void provider(int val)
{
    for(int i = 0; i < 5; i++)
    {
        {
            std::lock_guard<std::mutex> lg(queueMutex);
            queue.push(val+i);
        }

        queueCondVar.notify_one();

        std::this_thread::sleep_for(std::chrono::microseconds(val));
    }


}

void consumer(int num)
{
    while(true)
    {
        int val;
        {
            std::unique_lock<std::mutex> ul(queueMutex);
            queueCondVar.wait(ul,[]{return !queue.empty();});
            val = queue.front();
            queue.pop();
            std::cout << "consumer " << num << ":" << val << std::endl;
        }
    }


}



int main()
{
    auto p1 = std::async(std::launch::async,provider,100);
    auto p2 = std::async(std::launch::async,provider,300);
    auto p3 = std::async(std::launch::async,provider,500);

    auto c1 = std::async(std::launch::async,consumer,1);
    auto c2 = std::async(std::launch::async,consumer,2);


    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值