C++多线程(4)——条件变量、异步任务、原子atomic

条件变量std::condition_variable

  • 在前面使用互斥量实现从消息队列中读写数据的代码中,从消息队列读取元素(消费者)的时候,会先加锁,然后判断消息队列是否为空,当写数据(生产者)速度较慢时,读数据会不断的给共享数据加锁,导致做了很多无用功,CPU占用率很高。
  • 常用解决办法:双重锁定,消费者延时
// 双重锁定
bool outMsgLULproc(int& command)
{
    
    
    if(!msgRecvQueue.empty())
    {
    
    
        std::lock_guard<mutex> guard(my_mutex);
		if (!msgRecvQueue.empty())
		{
    
    
			command = msgRecvQueue.front();
			msgRecvQueue.pop_front();
			return true;
		}
		return false;
    }
}

// 消费者延时
void outMsgRecvQueue()
{
    
    
	int command = 0;
	for (int i = 0; i < 100000; i++)
	{
    
    
		bool result = outMsgLULproc(command);
		if (result == true)
		{
    
    
			cout << "outMsgRecvQueue()执行,取出一个元素" << command << endl;
			// 其他处理代码...
		}
		else
		{
    
    
			cout << "outMsgRecvQueue()执行,目前消息队列为空" << endl;
            this_thread::sleep_for(chrono::milliseconds(500));// 延时
		}
	}
}
  • 更好的解决办法:条件变量std::condition_variable。条件变量需要和互斥量一起使用,其中有两个重要的接口,notify_one()wait()wait()可以让线程陷入休眠状态,在消费者生产者模型中,如果生产者发现队列中没有东西,就可以让自己休眠,但是不能一直不干活啊,notify_one()就是唤醒处于wait中的条件变量
  • wait()被唤醒之后,会不断尝试重新获取互斥量锁,如果获取不到,那么就会阻塞着等着获取,如果获取到了锁,那么就判断第二个参数是否为true,如果为true则流程继续执行,否则释放该锁,继续休眠。如果wait()没有第二个参数,则跟true是一样的,流程继续执行。
class A
{
    
    
public:
	// 向消息队列中插入元素
	void inMsgRecvQueue()
	{
    
    
		for (int i = 0; i < 100000; i++)
		{
    
    
			cout << "inMsgRecvQueue()执行,插入一个元素" << i << endl;
			unique_lock<mutex> guard(my_mutex);
			msgRecvQueue.push_back(i);
			my_cond.notify_one();
		}
	}

	// 从消息队列中取出首元素
	void outMsgRecvQueue()
	{
    
    
		int command = 0;
		for (int i = 0; i < 100000; i++)
		{
    
    
			unique_lock<mutex> guard(my_mutex);
			my_cond.wait(guard, [this] {
    
    
				if (!msgRecvQueue.empty())
					return true;
				return false;
			});

			int command = msgRecvQueue.front();
			msgRecvQueue.pop_front();
			cout << "outMsgRecvQueue()执行,取出一个元素" << command << endl;
		}
	}
private:
	list<int> msgRecvQueue; // 消息队列
	mutex my_mutex; // 互斥量
	condition_variable my_cond; // 条件变量
};

int main()
{
    
    
	A a;
	thread inThread(&A::inMsgRecvQueue, std::ref(a));
	thread outThread(&A::outMsgRecvQueue, std::ref(a));
	inThread.join();
	outThread.join();

	return 0;
}

注意

  • 使用的是std::unique_lock而不是std::lock_guardwait()函数会先调用互斥锁的unlock()函数,然后再将自己睡眠,在被唤醒后,又会继续持有锁,保护后面的队列操作。而lock_guard没有lockunlock接口,而unique_lock提供了。
  • 减小锁的粒度,在使用notify_one()唤醒条件变量之后尽快将锁unlock()
  • notify_one()不一定绝对会把另一个线程的wait()唤醒,当另一个线程在处理其他事物时,没有堵塞在wait()等待被唤醒时,此时的notify_one()就无效。
  • wait()也有可能被虚假唤醒,比如多次调用notify_one()或者notify_all()多个wait()时使得其中一个从消息队列中取出了元素导致其他wait()再去取元素时消息队列已经为空。解决虚假唤醒的重要方法是给wait()带上第二个判断的参数,如上面代码段所示。

std::async, std::future

int my_func()
{
    
    
	cout << "function start threadid = " << std::this_thread::get_id() << endl;
	std::this_thread::sleep_for(chrono::seconds(5));
	cout << "function end threadid = " << std::this_thread::get_id() << endl;
	return 5;
}

int main()
{
    
    
	std::future<int> result = std::async(my_func);
	std::future_status status = result.wait_for(std::chrono::seconds(6));
	if(status==std::future_status::timeout)
	{
    
    
		cout << "超时,线程还没执行完毕" << endl;
		cout << result.get() << endl;// 卡在这里,等线程执行完
	}
	else if (status == std::future_status::ready)
	{
    
    
		cout << "线程执行完毕,返回" << endl;
		cout << result.get() << endl;
	}
	else if (status == std::future_status::deferred)
	{
    
    
		// 如果std::async()的第一个参数被设置为std::launch::deffered,则本条件成立
		cout << "线程被延迟执行" << endl;
		cout << result.get() << endl;// 开始执行线程入口函数,但线程根本不会被创建!
	}
	return 0;
}
  • std::async()创建一个异步任务并开始执行,实现与线程函数的绑定,返回一个std::future对象。

  • std::future对象的get()成员函数等待线程执行结束并返回结果。

  • std::future对象的wait()成员函数只是等待线程返回,本身并不返回结果。

  • std::future对象只能调用一次get()函数,这是因为get()是一个移动语义,在get()之后future的值就为空了。要想能够在多个线程中多次get(),可以使用std::shared_future对象。

std::launch::async和std::launch::deferred参数

  • std::async()函数可以传入两个参数,std::launch::asyncstd::launch::deferred,前者是创建一个新线程并立即执行线程入口函数,后者表示线程入口函数等到std::futureget()wait()函数调用时才执行,但是线程根本不会被创建!
  • 如果传入的参数是std::launch::async | std::launch::deferred,则系统会自动选择其中一种来运行。该参数也是std::async()函数的默认参数。

与std::thread的区别

  • std::thread创建线程,如果系统资源紧张,就会导致创建线程失败,程序会报异常崩溃
  • std::async创建线程,系统会自动选择创建运行方式,如果资源紧张,则会以同步的方式执行线程入口函数,即以std::launch::deferred为参数执行。

std::packaged_task, std::promise

std::packaged_task

int my_func1(int var)
{
    
    
	cout << "my_func1 start threadid = " << std::this_thread::get_id() << endl;
	cout << var << endl;
	//为了突出效果,可以使线程休眠5s
    std::this_thread::sleep_for(chrono::milliseconds(5000));
	cout << "my_func1 end threadid = " << std::this_thread::get_id() << endl;
	return 5;
}

void my_func2(std::future<int>& tmpf)
{
    
    
	cout << "my_func2 start threadid = " << std::this_thread::get_id() << endl;
	auto result = tmpf.get();
	cout << "my_func2 result = " << result << endl;
	return;
}

int main()
{
    
    
	std::packaged_task<int(int)> mypt(my_func1);
	std::future<int> result = mypt.get_future();

	std::thread t1(std::ref(mypt), 1);
	std::thread t2(my_func2, std::ref(result));
	t1.join();
	t2.join();

	return 0;
}
  • std::package_task是个类模板,模板参数是各种可调用对象,他将可调用对象包装起来,方便将来作为线程入口函数来调用。
  • std::packaged_task包装起来的可调用对象也可以直接调用,所以packaged_task对象也是一个可调用对象。
int main()
{
    
    
	std::packaged_task<int(int)> mypt1(my_func);
	mypt1(10);
	std::future<int> result = mypt1.get_future();
	cout << result.get() << endl;

	vector<std::packaged_task<int(int)>> my_task;
	std::packaged_task<int(int)> mypt2(my_func);
	my_task.push_back(std::move(mypt2));// 移动语义,进去后mypt为空
	auto iter = my_task.begin();
	std::packaged_task<int(int)> mypt3;
	mypt3 = std::move(*iter);
	my_task.erase(iter);
	mypt3(15);
	std::future<int> result2 = mypt3.get_future();
	cout << result2.get() << endl;
    return 0;
}

std::promise

  • 能够在某个线程中给他赋值,然后在其他线程中取出来,实现线程之间的参数传递
  • 通过std::promise保存一个值,通过std::future绑定到这个std::promise上来得到这个绑定的值
void Thread_Fun1(std::promise<int>& p)
{
    
    
	//为了突出效果,可以使线程休眠5s
	std::this_thread::sleep_for(std::chrono::seconds(5));

	int iVal = 233;
	std::cout << "传入数据(int):" << iVal << std::endl;

	//传入数据iVal
	p.set_value(iVal);
}

void Thread_Fun2(std::future<int>& f)
{
    
    
	//阻塞函数,直到收到相关联的std::promise对象传入的数据
	auto iVal = f.get();		//iVal = 233

	std::cout << "收到数据(int):" << iVal << std::endl;
}

int main()
{
    
    
	std::promise<int> myprom;
	std::future<int> fu1 = myprom.get_future();

	std::thread t1(Thread_Fun1, std::ref(myprom));
	std::thread t2(Thread_Fun2, std::ref(fu1));
	t1.join();
	t2.join();
	return 0;
}

原子操作std::atomic

  • 原子操作是一种不需要用到互斥量加锁(加锁)技术的多线程编程方式。
  • 原子操作是指”不可分割的操作“。
  • 原子操作比互斥量效率要高。
  • 互斥量加锁一般针对一个代码段,而原子操作一般针对的是一个变量。
int icount = 0;

void my_func()
{
    
    
	for (int i = 0; i < 100000; i++)
		icount++;
	return;
}

int main()
{
    
    
	std::thread t1(my_func);
	std::thread t2(my_func);
	t1.join();
	t2.join();

	cout << icount << endl;
	return 0;
}
  • 按理说,上述两个线程运行完之后icount的值应为200000,但程序输出并不是,说明一个简单的自增运算在多线程中也需要加锁。
  • 如果使用std::mutex在数据自增前加锁,自增后解锁,可以保证自增运算的完整性,使程序输出结果符合预期,但是这样效率太低。
  • 使用原子操作可以较好地解决上述问题:
std::atomic<int> icount = 0;
  • 注意:原子操作支持的操作符:++,–,+=,&=,|=,^=等复合运算符,像下面这样是不对的!
void my_func()
{
    
    
	for (int i = 0; i < 100000; i++)
		//icount++;
        icount = icount + 1;// 运行结果不符合预期!
	return;
}

猜你喜欢

转载自blog.csdn.net/qq_34731182/article/details/113477846