《c++11学习笔记》 --- 线程

目录

创建线程:

用法:

互斥量

std::mutex

std::recursive_mutex 

std::timed_mutex 、std::recursive_timed_mutex

条件变量

 同步队列实现:

 使用unique_lock和condition_variable 实现的同步队列:

 原子变量

 call_once/once_flag 的使用:

 异步操作

1、获取线程函数返回值的类:std::future

 2、协助线程赋值的类:std::promise

 3、可调用对象的包装类std::package_task

 std::future、std:promise和 std::package_task之间的关系

线程异步操作函数async


创建线程:

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

void func()
{
}

int main()
{
	std::thread t(func);
	t.join();
    return 0;
}

join函数会阻塞线程,直到线程函数执行结束,如果线程函数有返回值,返回值将被忽略。
detach()方法,将线程和线程对象分离,线程不被阻塞执行。

void func()
{
}

int main()
{
	std::thread t(func);
	t.detach();
	return 0;
}

 注意:

detach之后就无法在和线程发生联系,不能detach之后在通过join来等待线程执行完。

线程接收任意参数:

void func(int i)
{
	cout<<i<<endl;
}
int main()
{
	std::thread t(func, 1);
	t.join();
	return 0;
}

线程不能复制,可以移动:

void func()
{
}
	
int main()
{
	std::thread t(func);
	std::thread t1(std::move(t));
	t.join();
	t1.join();
	return 0;
}

线程被移动之后,线程对象t将不再代表任何线程。

通过std::bind或者lambda表达式来创建线程:

void func(int i)
{
}

int main()
{
	std::thread t(std::bind(func, 1));
	std::thread t1([](int i, int b){ }, 1, 2);
	t.join();
	t1.join();
	return 0;
}

用法:

1、获取当前信息

获取当前线程ID,CPU核心数量。

void func()
{
}

int main()
{
	std::thread t(func);
	cout<<t.get_id()<<endl;   //获取当前线程ID
	//获取cpu核数,如果获取失败返回0
	cout<<std::thread::hardware_conncurrency()<<endl;
	t.join();
	return 0;
}

 2、线程休眠

当前休眠一段时间

void func()
{
	//休眠3s
	std::thread::sleep_for(std::chrono::seconds(3));
}

int main()
{
	std::thread t(func);
	t.join();
	return 0;
}

互斥量

 互斥量是一种同步原语,是一种线程同步的手段,用来保护多线程同时访问的共享数据
    std::mutex : 独占的互斥量,不能递归使用。
    std::timed_mutex  : 带超时的独占互斥量,不能递归使用。
    std::recursive_mutex  :递归互斥量,不带超时功能。
    std::recursive_timed_mutex :带超时的递归互斥量。

std::mutex

使用lock()加锁,unlock()解除。try_lock()尝试锁定互斥量,成功返回true,失败返回false,try_lock()是非阻塞的。
使用lock_guard可以简化lock和unlock的写法,更加安全,lock_guard在构造时(分配资源)会自动锁定互斥量,退出作用域后进行析构时(释放资源)会自动解锁,保证互斥量的正确操作,避免忘记unlock。

std::mutex g_lock;
std::lock_guard<std::mutex> locker(g_lock);//出作用域之后自动解锁
std::this_thread::get_id()//获取线程id

std::recursive_mutex 

递归锁允许同一线程多次获得该互斥锁,可以用来解决同一线程需要多次获取互斥量时死锁的问题。
一个线程多次获取同一个互斥量时会发生死锁。

class Complex 
{
	std::mutex mutex;
	int i;
	Complex():i(0) {}
	void mul(int x) 
	{
		std::lock_guard<std::mutex> 1ock(mutex) ;
		i *= x;
	}
	void div(int x) 
	{
		std::1ock_guard<std::mutex> lock(mutex) ;
		i /= x;
	}
	void both(int x,int y) 
	{
		std::lock_guard<std::mutex> 1ock(mutex) ;
		mul(x) ;
		div(y) ;
	}
};
	
int main() 
{
	Complex complex;
	complex.both(32, 23) ;
	return 0;
}

/*
会发生死锁,在调用both时获取互斥量,之后调用mul又要获取相同的互斥量,
但这个互斥量已经被当前的线程获取,无法释放,这时就会发生死锁。
*/

解决此办法就是用递归锁,允许在同一个线程中多次获得互斥量。

class Complex 
{
	std::recursive_mutex mutex;
	int i;
	Complex():i(0) {}
	void mul(int x) 
	{
		std::lock_guard<std::recursive_mutex> 1ock(mutex) ;
		i *= x;
	}
	void div(int x) 
	{
		std::1ock_guard<std::recursive_mutex> lock(mutex) ;
		i /= x;
	}
	void both(int x,int y) 
	{
		std::lock_guard<std::recursive_mutex> 1ock(mutex) ;
		mul(x) ;
		div(y) ;
	}
};

int main() 
{
	Complex complex;
	complex.both(32,23) ;
	return 0;
}

注意:尽量不要使用递归锁,原因:
1)需要用到递归锁定的多线程互斥处理往往本身就是可以简化的,允许递归互斥很容 易放纵复          杂逻辑的产生,从而导致一些多线程同步引起的晦涩问题。
2)递归锁比起非递归锁,效率会低一些。
3)递归锁虽然允许同一个线程多次获得同一个互斥量,可重复获得的最大次数并未具体说明,一       旦超过一定次数,再对lock进行调用就会抛出std::system错误。 

std::timed_mutex 、std::recursive_timed_mutex

std::timed_mutex是超时的独占锁,std::recursive_timed_mutex是超时的递归锁,主要用在获取锁时增加超时等待功能,因为有时不知道获取锁需要多久,为了不至于一直在等待获取互斥量,就设置一个等待超时时间,在超时后还可以做其他的事情。
std::timed_mutex比std:mutex多了两个超时获取锁的接口: try_lock_for和try_lock_until,这两个接口是用来设置获取互斥量的超时时间,使用时可以用一个while循环去不断地获取互斥量。

std:timed_ mutex 的基本用法:   

std::timed_mutex mutex;
void work()
{
	std::chrono::milliseconds timeout(100);
	while(true)
	{
		if(mutex.try_lock_for(timeout))
		{
			std::chrono::milliseconds sleepDuration(250);
			std::this_thread::sleep_for(sleepDuration);
			mutex.unlock();
			std::this_thread::sleep_for(sleepDuration);
		}
		else
		{
			std::chrono::milliseconds sleepDuration(100);
			std::this_thread::sleep_for(sleepDuration);
	    }
	}
}

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

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

	return 0;
}

通过while不断的获取超时锁,如果超时还没获取到锁时,就休眠100毫秒,再尝试获取超时锁。

条件变量

条件变量用于等待同步机制,能阻塞一个或多个线程,直到收到另一个线程发出的通知或者超时,才会唤醒当前阻塞的线程。条件变量需要和互斥量配合使用。
condition_variable,配合std::unique_lock<std::mutex>进行wait操作(性能更好)
condition_variable_any,和任意带有lock、unlock语义的mutex搭配使用,比较灵活,效率比condition_variable差一些。

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

使用条件变量实现一个同步队列,同步队列作为一个线程安全的数据共享区,常用于线程之间数据读取。

 同步队列实现:

#include <condition_variable>
#include <lsit>
#include <mutex>
#include <thread>
template<typename T>
class SyncQueue
{
	bool isFull() const
	{
		return m_queue.size() == m_maxSize;
	}
	bool isEmpty() const
	{
		return m_queue.empty;
	}
public:
	SyncQueue(int maxSize): m_maxSize(maxSize){}
	//添加数据
	void put(const T& x)
	{
		std::lock_guard<std::mutex> locker(m_mutex);
		while(isFull())
		{
			cout<<"缓冲区满了,需要等待..."<<endl;
			m_notFull.wait(m_nutex);
		}
		m_queue.push_back(x);
		m_notEmpty.notify_one();
	}
	//取数据
	void take(T& x)
	{
		std::lock_guard<std::mutex> locker(m_mutex);
		while(isEmpty())
		{
			cout<<"缓冲区空了,需要等待..."<<endl;
			m_notEmpty.wait(m_nutex);
		}
		x = m_queue.front();
		m_queue.pop_front();
		m_notFull.notify_one();
	}
	
	bool empty()
	{
		std::lock_guard<std::mutex> locker(m_mutex);
		return m_queue.empty();
	}
	bool full()
	{
		std::lock_guard<std::mutex> locker(m_mutex);
		return m_queue.size == m_maxSize;
	}
	size_t size()
	{
		std::lock_guard<std::mutex> locker(m_mutex);
		return m_queue.size();
	}
	int count()
	{
		return m_queue.size();
	}
private:
	std::list<T> m_queue; //缓冲区
	std::mutex m_mutex;  //互斥量和条件变量结合使用
	std::condition_variable_any m_notEmpty; //不为空的条件变量
	std::condition_variable_any m_notFull; //没有满的条件变量
	int m_maxSize;  //同步队列最大的size
}

这个同步队列在没有满的情况下可以插人数据,如果满了,则会调用m_notFull阻塞等待,待消费线程取出数据之后发一个未满的通知,然后前面阻塞的线程就会被唤醒继续往下执行;如果队列为空,就不能取数据,会调用m_notEmpty条件变量阻塞,等待插入数据的线程发出不为空的通知时,才能继续往下执行。

使用lock_guard,它利用了RAII机制可以保证安全释放mutex。

两种写法效果是一样的,但是后者更简洁,条件变量会先检查判断式是否满足条件,如果满足条件,则重新获取mutex,然后结束wait,继续往下执行;如果不满足条件,则释放mutex,然后将线程置为waiting状态,继续等待。wait函数中会释放mutex,而lock_guard还拥有mutex,只会在出作用域之后才释放mutex。wait释放锁之后会处于等待状态,再被notify_one或者notify_all唤醒之后会先获取mutex,这个相当于lock_guard的mutex在释放之后又获取到了mutex。unique_lock可以随时释放锁。

std::lock_guard<std::mutex> locker(m_mutex);
while(isFull())
{
	cout<<"缓冲区满了,需要等待..."<<endl;
	m_notFull.wait(m_nutex);
}

可以改写:
std::lock_guard<std::mutex> locker(m_mutex);
m_notFull.wait(locker, [this]{ return !isFull();});

把 std::lock_guard改成std::unique_lock,把std::condition_variable_any改为std:condition_variable,并且用等待一个判断式的方法来实现一个简单的线程池。

 使用unique_lock和condition_variable 实现的同步队列:

#include <condition_variable>
#include <lsit>
#include <mutex>
#include <thread>
template<typename T>
class SyncQueue
{
public:
	SyncQueue(){}
	//添加数据
	void put(const T& x)
	{
		std::lock_guard<std::mutex> locker(m_mutex);
		m_queue.push_back(x);
		m_notEmpty.notify_one();
	}
	//取数据
	void take(T& x)
	{
		std::unique_lock<std::mutex> locker(m_mutex);
		m_notEmpty.wait(locker, [this]{ return !m_queue.empty();});
		x = m_queue.front();
		m_queue.pop_front();
	}
	
	bool empty()
	{
		std::lock_guard<std::mutex> locker(m_mutex);
		return m_queue.empty();
	}
	size_t size()
	{
		std::lock_guard<std::mutex> locker(m_mutex);
		return m_queue.size();
	}
private:
	std::list<T> m_queue; //缓冲区
	std::mutex m_mutex;  //互斥量和条件变量结合使用
	std::condition_variabl m_notEmpty; //不为空的条件变量
}

用unique_lock代替lock _guard,使语义更加准确,用性能更好的condition_variable替代condition_variable_any,对程序加以优化,仍然可以用condition_variable_any。执行wait时不再通过while循环来判断,而是通过lambda表达式来判断,写法上更简洁。

 原子变量

原子类型:std::atomic<T>

使用原子变量就不需要使用互斥量来保护该变量。可以更方便的实现线程保护。

计数器:

//使用mutex
struct Counter
{
	int value;
	std::mutex mutex;
	
	void increment()
	{
		std::lock_guard<std::mutex> lock(mutex);
		++value;
	}
	
	void decrement()
	{
		std::lock_guard<std::mutex> lock(mutex);
		--value;
	}
	
	int get()
	{
		return value;
	}
};

//使用原子变量
struct Counter
{
	std::atomic<int> value;
	
	void increment()
	{
		++value;
	}
	
	void decrement()
	{
		--value;
	}
	
	int get()
	{
		return value.load();
	}
};

 call_once/once_flag 的使用:

 为了保证在多线程环境中某个函数仅被调用一次,比如,需要初始化某个对象,而这个对象只能初始化一次时,就可以用std::call_once来保证函数在多线程环境中只被调用一次。使用std:call_once时,需要一个once_flag作为call_once的入参,它的用法比较简单。

std::once_flag flag;
void do_once()
{
    std::call_once(flag, [](){
	    cout<< " called once" <<endl;
	});
}

int main()
{
	std::thread t1(do_once);
	std::thread t2(do_once);
	std::thread t3(do_once);
		
	t1.join();
	t2.join();
	t3.join();
}

 异步操作

C++11中异步操作相关的类,主要有std::future、std:promise和 std::package_task。std:future作为异步结果的传输通道,可以很方便地获取线程函数的返回值;

std::promise用来包装一个值,将数据和 future绑定起来,方便线程赋值;

std::package_task用来包装一个可调用对象,将函数和 future绑定起来,以便异步调用。

1、获取线程函数返回值的类:std::future

thread 库提供了future用来访问异步操作的结果,因为一个异步操作的结果不能马上获取,只能在未来某个时候从某个地方获取,这个异步操作的结果是一个未来的期待值,所以被称为future,future提供了获取异步操作结果的通道。可以以同步等待的方式来获取结果,可以通过查询future的状态( future_status)来获取异步操作的结果。future_status有如下3种状态:
    deferred,异步操作还没开始。
    ready,异步操作已经完成。
    timeout,异步操作超时。

std::future_status status;
do
{
	status = future.wait_for(std::chrono::seconds(1));
	if( status == std::future_status::deferred)
	{
		cout<<"deferred"<<endl;
	}
	else if( status == std::future_status::timeout)
	{
	    cout<<"timeout"<<endl;
	}
	else if( status == std::future_status::ready)
	{
		cout<<"ready"<<endl;
	}
}while(status != std::future_status::ready);

获取future的结果有3种方式:get、wait、wait_for,
get:等待异步操作结束并返回结果
wait:只是等待异步操作完成
wait_for:超时等待返回结果

 2、协助线程赋值的类:std::promise

std:promise将数据和 future绑定起来,为获取线程函数中的某个值提供便利,在线程函数中为外面传进来的promise赋值,在线程函数执行完成之后就可以通过promis的future获取该值了。注意:取值是间接地通过promise内部提供的future来获取的

std::promise的基本用法如下:

std::promise<int> pr;
std::thread t( [](std::promise<int> &p) {
	p.set_value_at_thread_exit(10); }, std::ref(pr));
;
std::future<int> f = pr.get_future();
auto r = f.get();

 3、可调用对象的包装类std::package_task

 std:packaged_task包装了一个可调用对象的包装类(如function、lambda expression、bind expression和 another function object),将函数和future绑定起来,以便异步调用,它和std::promise在某种程度上有点像,promise保存了一个共享状态的值,而  packaged_task保存的是一个函数。

std:packaged_task的基本用法如下:

std::package_task<int> task( [](){ return 2; } );
std::thread t(std::ref(task));
std::future<int> f = task.get_future();
auto r = f.get();

 std::future、std:promise和 std::package_task之间的关系

future被promise和 package_task用来作为异步操作或者异步结果的连接通道,用std::future和std::shared_future来获取异步调用的结果。                                                                      future是不可拷贝的,只能移动,shared_future 是可以拷贝的,当需要将future放到容器中则需要用shared_future。

package_task和shared_future的基本用法:

int func( int x)
{
	return x;
}

int main()
{
	std::package_task<int(int)> tsk(func);

    //获取future的结果有3重方式:get、wait、wait_for
	std::future<int> fut = tsk.get_future(); 
		
	std::thread(std::move(tsk), 2).detach();   //tsk作为线程函数
		
	int value = fut.get();    //等待tsk完成并返回结果
    cout<< value <<endl;

	//std::future是不能复制的,不能放到容器中,需要用到shared_future
	vector<std::shared_future<int>> v;

	auto f = std::async(std::launch::async, [](int a, int b){
		return a+ b; }, 2, 3);
		
	v.push_back(f);
	cout<<v[0].get()<<endl;
	return 0;
}

//2     5

线程异步操作函数async

std::async可以用来直接创建异步的task,异步任务返回的结果也保存在future中,当需要获取异步任务的结果时,只需要调用future.get()方法即可,如果不关注异步任务的结果,只是简单地等待任务完成的话, 则调用future.wait()方法。

std::async的函数原型:

第一个参数:线程的创建策略,有两种策略,默认的策略是立即创建线程。
    std::launch::async:在调用async时就开始创建线程。
    std::launch::deferred:延迟加载方式创建线程。
    调用async时不创建线程,直到调用了future的get或者wait时才创建线程。
第二个参数:线程函数,
第三个参数:线程函数的参数。

async(std::launch::async|std::launch::deferred, f, args...)

 std::async的使用:

std::future<int> f1 = std::async(std::launch::async, [](){
	return 8;
});
cout<<f1.get()<<endl;   //8

std::future<int> f2 = std::async(std::launch::async, [](){
	cout<<8<<endl;
});
f2.wait();

std::future<int> future = std::async(std::launch::async, [](){
	std::this_thread::sleep_for(std::chrono::seconds(3));
	return 8;
});
cout<<"waiting...."<<endl;

std::future_status status;
do
{
	status = future.wait_for(std::chrono::seconds(1));
	if(status == std::future_status::deferred)
	{
		cout<<"deferred"<<endl;
	}
	else if(status == std::future_status::timeout)
	{
		cout<<"timeout"<<endl;
	}
	eles if(status == std::future_status::ready)
	{
		cout<<"ready"<<endl;
	}
	
}while(status != std::future_status::ready);

std::async是更高层次的异步操作,使得我们不用关注线程创建内部细节,就能方便地获取异步执行状态和结果,还可以指定线程创建策略:应该用std:async替代线程的创建,让它成为我们做异步操作的首选。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值