线程、协程、原子特性

线程、原子特性

std::thread在#include头文件中声明,因此使用std::thread时需要包含#include头文件。

1.1.1语法

构造函数

  • 默认构造函数
//创建一个空的thread执行对象。
thread() _NOEXCEPT
				{		
						// construct with no thread
						_The_set_null(_Thr);
					}
  • 初始化构造函数
//创建std::thread执行对象,该thread对象可被jionable,新产生的线程会调用threadFun函数,该函数的参数由args给出
themplate<class Fn ,class ....  Args>
explicit thread(Fn&& fn,Args&& ...  args);
  • 拷贝构造函数
//拷贝构造函数(被禁用),意味着thread不可被拷贝构造
thread(const thead&) = delete;
  • Move构造函数
//move构造函数,调用成功之后x不代表任何thread执行对象。
//注意:可被joinable的thread对象必须在他们销毁之前被主线程join或者其他将其设置为detached。
thread(thread&& x)noexcept

主要成员函数

  • get_id():获得线程ID,返回类型std::tread::id对象。
  • joinable():判断线程是否可以加入等待。
  • join():等该线程执行完后才返回。
  • detach():将本线程从调用线程中分离出来,允许本线程独立执行。(但是当主进程结束的时候,即便时detach()出去的子线程不管有没有完成都会被强制杀死)。detach相当于不管该线程的意思。一般谁创建的线程,谁就要负责join它,确保它退出,正确释放资源。

1.1.2简单线程的创建

使用std::thread创建线程,提供线程函数或者函数对象,并可以同时指定线程函数的参数。

#include<iostream>
#include<thread>
using namespace std;

void func1()
{
	cout << "func1 into" << endl; 
}

void func2(int a, int b)
{
	cout << "func2 a + b = " << a + b << endl; 
}

class A
{
	public:
			static void func3(int a)
			{
					std::this_thread::sleep_for(std::chrono::seconds(5));
					cout << "a  = " <<  a << endl;
			}
};

void func4(int a, int b)
{
	std::this_thread::sleep_for(std::chrono::seconds(5));
	cout << "func4 a + b = " << a + b << endl; 
}

int main()
{
	std::thread t1(func1);	//只传递函数,即刻运行
	t1.join();	//阻塞等待线程函数执行结束
	
	int a = 10;
	int b = 20;
	std::thread t2(func2, a, b);		//加上参数传递,可以任意参数
	t2.join();

	std::thread t3(&A::func3, 1);	//绑定类静态函数
	t3.join();
	
	std::thread t4(func4, a, b);		//加上参数传递,可以任意参数
	t4.detach();	

	std::this_thread::sleep_for(std::chrono::seconds(10));
	
	return 0;
}

1.1.3 线程封装(参考腾讯tars的代码)

thread.h

#ifndef THREAD_H
#define THREAD_H
#include<thread>

clase Thread
{
	public:
			Thread();		//构造函数
			virtual ~Thread();	//析构函数
			bool start();
			void stop();
			bool isAlive() const;		//线程是否存活
			std::thread::id id() {return _th -> get_id();}
			std::thread* getThread() {return _th;}	
			void join();	//等待线程结束,不能在当前线程上调用
			void detach();	//能在当前线程上调用
			static size_t CURRENT_THREADID();
	protected:
			static void threadEntry(Thread* pThread);	//静态函数,线程入口
			virtual void run() =  0 ;		//运行
	protected:
			bool _running;		//是否在运行
			std::thread* _th;		
}

thread.cpp

#include "thread.h"
#include<sstream>
#include<iostream>
#include<exception>

Thread::Thread():_running(false),_th(NULL)
{

}

Thread::~Thread()
{
	if(_th != NULL)
	{
			//如果资源没有被detach或者被join,则自己释放
			if(_th->joinable())
			{
					_th->detach();
			}
			delete _th;
			_th = NULL ;
	}
	std::cout << "~Thread()" << std::endl;
}

bool Thread::start()
{
		if(_running)
		{
				return false;	
		  }
		  try
		  {
					_th = new std::thread(&ThreadLLthreadEntry, this);
			}
			cath(...)
			{
						throw "[Thread::start] thread start error";
			}
			return true;
}

void Thread::stop()
{
	_running = false;
}

bool Thread::isAlive() const
{
	return _running;
}

void Thread::jion()
{
	if(_th->joinable())
	{
		_th->join();
	 }
}


void Thread::detach()
{
	_th->detach();
}

size_t Thread::CURRENT_THREADID()
{
	//声明为thread_local的本地变量在线程中是持续存在的,不同于普通临时变量的生命周期,
	//它具有static变量一样的初始化特征和生命周期,即使它不被声明为static
	static thread local size_t threadId = 0 ;
	if(threadld == 0 )
	{
		std::stringstream ss;
		ss << std::this_thread::get_id();
		threadId = strtol(ss.str().c_str(),NULL, 0);
	}
	return threadId;
}

void Thread::threadEntry(Thread *pThread)
{
	pThread->_running = true;
	try
	{
		pThread->run();//函数运行所在
	}
	catch(std::exception &ex)
	{
		pThread->_running = false;
		throw ex;
	}
	catch(...)
	{
		pThread->_running = false;
		throw;
	}
	pThread->_running = false;
}

1.2互斥量

mutex又称互斥量,C++11中与mutex相关的类(包括锁类型)和函数都声明在头文件中。

C++11中提供4中语义的互斥量(mutex):(在开发中不推荐使用递归锁)

  • std::mutex,独占的互斥量,不能递归使用。
  • std::time_mutex,带超时的独占互斥量,不能递归使用。
  • std::recursive_mutex,递归互斥量,不带超时功能。(不推荐使用)
  • std::recursibe_timed_mutex,带超时的递归互斥量。(不推荐使用)

1.2.1 独占互斥量std::mutex

std::mutex介绍
std::mutex是C++11中最基本的互斥量,std::mutex对象提供了独占所有权的特性——既不支持递归地对std::mutex对象上锁,而std::recursive_lock则可以递归地对互斥量对象上锁。

std::mutex的成员函数

  • 构造函数,std::mutex不允许拷贝构造,也不允许move拷贝,最初产生的mutex对象是处于unlocked状态的。
  • lock(),调用线程将锁住该互斥量。线程调用该函数会发生下面3种情况:1. 如果该互斥量当前没有被锁住,则调用线程将该互斥量锁住,知道调用unlock之前,该线程一直拥有该锁。2.如果当前互斥量被其他线程锁住,则当前的调用线程被阻塞住。3.如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)。
  • unlock(),解锁,释放对互斥量的所有权。
  • try_lock(),尝试锁住互斥量,如果互斥量被其他线程占有,则当前线程也不会被阻塞。线程调用该函数也会出现下面3种情况,(1)如果当前互斥量没有被其他线程占有,则该线程锁住互斥量,直到该线程调用unlock释放互斥量。(2)如果当前互斥量被其他线程锁住,则当前调用线程返回false,则并不会被阻塞掉。(3)如果当前互斥量被当前调用线程锁住,则会产生死锁。
#include <iostream>
#include <thread>
#include <mutex>

volatile int counter(0);		//non-atomic counter
std::mutex mtx;		//locks access to counter

void increases_10k()
{
   for(int i = 0; i < 10000; ++i){
   		//1.使用try_lock的情况
   		//  		if (mtx.try_lock()) {		//only increase if currently not locked;
   		//			++counter;
   		//			mtx.unlock();
   		//			}
   		//	2.使用lock的情况
   		{
   				mtx.lock();
   				++counter;
   				mtx.unlock();
   		}
   	}
}

int main()
{
   	std::thread threads[10];
   	for(int i = 0; i < 10; i++)
   		threads[i] = std::thread(increases_10k);
   	
   	for(auto& th : threads ) th.join();
   	std::cout << "successful increases of the counter " << counter << std::endl;
   	return 0;
}

1.2.2 超时锁

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

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 wprk 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 the mutex" << std::endl;
   		std::chrono::milliseconds sleepDuration(100);
   		std::this_thread::sleep_for(sleepDuration);
   	}
   }
}


int main(void) 
{
   std::thread t1(work);
   std::thread t2(work);

   t1.join();
   t2.join();

   std::cout << "main finish\n";

   return 0;
}

1.2.3 lock_guard 和unique_lock的使用和区别

相对于手动lock和unlock,我们可以使用RAII(通过类的构造析构)来实现更好的编码方式。
这里涉及到unique_lock,lock_guard的使用。

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

std::mutex mtx;

void print_even(int x)
{
   if (x % 2 == 0) std::cout << x << "is even\n";
   else throw(std::logic_error("not even"));
}

void print_thread_id(int id) {
   try
   {
   	//using a local lock_guard to lock mtx guarantees unlocking on destruction / exception
   	std::lock_guard<std::mutex> lck(mtx);
   	print_even(id);
   }
   catch (const std::logic_error&)
   {
   	std::cout << "[exception caught]\n";
   }
}


int main() 
{
   std::thread threads[10];
   
   for (int i = 0; i < 10; i++)
   	threads[i] = std::thread(print_thread_id, i + 1);

   for (auto& th : threads) th.join();

   return 0;
}

这里的lock_guad换成unique_lock是一样的。

unique_lock,lock_guard的区别

  • unique_lock与lock_guard都能实现自动加锁和解锁,但是前者更加灵活,能实现更多的功能。

  • unique_lock可以进行临时解锁和再上锁,如在构造对象之后使用lck.unlock()就可以进行结果,lck.lock()进行上锁,而不必等到析构时自动解锁。

#include <iostream>
#include <thread>
#include <mutex>
#include <deque>
#include <condition_varible>
#include <unistd.h>

std::deque<int> q;
std::mutex mu;
std::condition_variable cond;

void fun1() {
	while (true) {
		std::unique_lock<std::mutex> locker(mu);
		q.push_front(count);
		locker.unlock();
		cond.notify_one();
		sleep(10);
	}
}

void fun2() {
	while (true)
	{
		std::unique_lock<std::mutex> locker(mu);
		cond.wait(locker, []() {return !q.empty(); });
		data = q.back();
		q.pop_back();
		locker.unlock();
		std::cout << "thread2 get value from thread1:" << data << std::endl;
	}
}

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

	return 0;
}

条件变量的目的就是为了在没有获得某种提醒时长时间休眠;如果正情况下,我们需要一直循环(+sleep),这样的问题就是cpu消耗+时延问题,条件变量的意思是在cond.wait这里一直休眠直到cond.notify_one唤醒才开始执行下一句;还有cond.notify_all()接口用于唤醒所有等待的线程。

为什么必须使用unique_lock呢?
因为条件变量在wait时会进行unlock再进行休眠,lock_guard并无该操作接口。
wait:如果线程被唤醒或者超时,那么会先进行lock获取锁,再判断条件(传入的参数)是否成立,如果成立则wait函数返回,否则释放锁继续休眠。
notify:进行notify动作并不需要获取锁

总结
lock_guard
1.std::lock_guard在构造函数中进行加锁,析构函数中进行解锁。
2.锁在多线程编程中使用较多。

std::unique_lock
1.unique_lock是通用互斥包装器,允许延迟锁定,锁定的有时限尝试、递归锁定、所有权和与条件变量一同使用。
2.unique_lock比lock_guard使用更加灵活,功能更加强大。
3.使用unique_lock需要付出更多的时间、性能成本。

1.3 条件变量

互斥量是多线程间同时访问某一共享变量时,保证变量可以被安全访问的手段,但是单靠互斥量无法实现线程的同步。线程同步是指线程间需要按照预定的先后次序顺序进行的行为。

条件变量使用过程:
1.拥有条件变量的线程获取互斥量;
2.循环检查某个条件,如果条件不满足则阻塞直到条件满足;如果条件满足则向下执行;
3.某个线程满足条件执行完之后调用notify_one或notify_all唤醒一个或所有等待线程。

条件变量提供了两类操作:wait和notify。这两类操作构成了多线程同步的基础。

1.3.1成员函数

1.wait函数
void wait(unique_lock<mutex>& lck);
template <class Predicate>
	void wait (unique_lock<mutex>& lck,predicate pred);

包含两种重载,第一种只包含unique_lock对象,另外一个predicate对象(等待条件),这里必须使用unique_lock,因为Wait函数的工作原理:

  • 当前线程调用wait()后会将被阻塞并且函数会解锁互斥量,直到另外某个线程调用notify_one或者notify_all唤醒当前线程;一旦当前线程获得通知(notify),wait()函数也会自动调用’lcok(),同理不能使用lock_guard对象。
  • 如果wait没有第二个参数,第一次调用默认条件不成立,直接解锁互斥量并阻塞到本行,直到某个线程调用notify_one或者notify_all为止,被唤醒后,wait重新尝试获取互斥量,如果得不到,线程会卡在这里,直到获取到互斥量,然后无条件地继续进行后面的操作。
  • 如果wait包含第二个参数,如果第二个参数不满足,那么wait将解锁互斥量并阻塞到本行,直到某个线程调用notify_one或者notify_all为止,被唤醒后,wait重新尝试获取互斥量,如果得不到,线程会卡在这里,直到获取到互斥量,然后继续判断第二个参数,如果表达式为false,wait对互斥量解锁然后休眠,如果为true,则进行后面的操作。
2.wait_for函数
template <class Rep, class Period>
	cv_status wait_for (unique_lock<mutex>& lck,cost chrono::duration<Rep,Period>& rel_time);
template <class Rep,class period,class predicate>
	bool wait_for (unique_lcok<mutex>& lck,const chrono::duration<Rep,Period>& rel_time,Predicate pred);

和wait不同的是,wait_for可以执行一个时间段,在线程收到唤醒通知或者时间超时之前,该线程都会处于阻塞状态,如果收到唤醒通知或者时间超时,wait_for返回,剩下操作和wait类似。

3.wait_until函数
template <class Clock,calss Duration>
	cv_status wait_until (unique_lock<mutex>& lck,const chrono::time_point<Clock,Duration>& abs_time);

template	<class Clock,class Duration,class Predicate>
	bool wait_until (unique_lock<mutex>& lck,const chrono::time_point<Clock,Duration>& abs_time,Predicate pred);

与wait_for类似,只是wait_until可以指定一个时间点,在当前线程收到通知或者指定的时间点超时之前,该线程都会处于阻塞状态。如果超时或者收到唤醒通知,wait_until返回,剩下操作和Wait类似

4notify_one函数
void notify_one() noexcept;

解锁正在等待当前条件的线程中的一个,如果没有线程在等待,则函数不执行任何操作,如果正在等待的线程多于一个,则唤醒的线程是不确定的。

5.notify_all函数
void notify_all() noexcept;

解锁正在等待当前条件的所有线程,如果没有正在等待的线程,则函数不执行任何操作。

1.4原子性

原子性:一个操作或一组操作要么全部执行成功,要么全部不执行,不存在中间状态,利用互斥锁实现。(大白话就是执行过程一气呵成,中间不能断)

如果看不懂可以去看看操作系统和计算机组成原理,看完之后再回来看本文。

  • 38
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值