个人C++11多线程学习笔记

本文章不经许可不允许以任何方式转载和复制保存

第三课

线程启动,结束,join,detach

1.前提:需要#include

2.线程启动
(1)线程类对象构造完后线程已经启动了
如:
在这里插入图片描述
3.join()和detach()
(1)join:主线程等待子线程执行完后主线程才继续做后面的事情,即join是阻塞的
(2)detach:子线程和主线程之间无联系,各跑各的,有安全隐患,即detach是非阻塞的
(3)如:
在这里插入图片描述
4.创建了线程(构造了thread对象),一定要调用该对象的join()或者detach()函数,否则程序崩溃
5.joinable()函数:当前子线程是否可以join或者detach

第四课

线程传参详解,detach大坑

1.对于使用detach()时的传参
(1)对于简单类型,如int等,用值传递安全,不要用引用
(2)对于类对象,避免隐式类型转换,因为存在主线程结束了但还未转换的可能(即可能会在一个新的线程中来进行转换构造)。
(3)安全做法:直接构造临时对象传参数,且用引用来接,因为用引用来接可以少执行一次拷贝构造函数,效率高
(4)尽量不用detach()而用join()

试验
在这里插入图片描述
在这里插入图片描述
解析

(1)std::this_thread::get_id()函数可以拿到当前线程的id
(2)输出杂乱因为某个时刻哪个线程拿到cpu资源是不确定的 (3)调用了Test的两次拷贝构造函数,所以可以推测:
(4)在传参的时候,thread的机制是先把main函数里的t对象拷贝一次再传进t2的构造函数参数里,然后会再拷贝一次
(5)假如t2是要选择detach()的方式,那么那样构造t2是不安全的,应该用临时对象传参

在这里插入图片描述

(6)假如Test类中有转换构造函数Test(int i),那么在初始化Test类对象时,整型可以隐式转换成Test类对象,那么给thread对象构造传参时不要直接传整型值

1.关于传参时的拷贝问题
(1)即使在线程入口函数的形参里用引用来接收实参,但是thread的做法是,先把实参拷贝了一份,即形参即使为引用,但是其实是跟实参是在不同内存的,修改形参的值,不会影响实参
例子:
在这里插入图片描述
在这里插入图片描述
(2)用引用来接收实参,只是为了防止再拷贝一次而已
例子:
在这里插入图片描述
在这里插入图片描述
(3)以引用来接收时,形参必须加上const属性,否则编译报错

2.解决第2点的用引用接收但是仍然会拷贝的问题
(1)方法:用std::ref()函数
如:
在这里插入图片描述
(a)此时,main里的t1和func里的t是同一个对象
(b)用了std::ref(),那么func的形参可以不加const

3.用智能指针传参
(1)必须要用std::move()
(2)最好用join(),用detach不安全
如:
在这里插入图片描述
4.传类的成员函数
例子
在这里插入图片描述
5.传operator()
(1)例子
在这里插入图片描述

第六课

互斥量,死锁

1.互斥量mutex

(1)锁住:lock()
(2)解锁:unlock()
(3)注意:lock和unlock一定要成对出现
(4)例子:

class Command
{
protected:
	list<int> m_list;
	mutex m_mutex;

public:
	void inMsgQueue()
	{
		for (int i = 0; i < 1000000; i++)
		{
			m_mutex.lock();
			m_list.push_back(i+1);
			m_mutex.unlock();
			cout << "线程:" << std::this_thread::get_id() << "  往队列里加入了元素:  " << i + 1 << endl;
		}
	}

	bool getCommand(int& command)
	{
		m_mutex.lock();
		if (!m_list.empty())
		{
			command = m_list.front();
			m_list.pop_front();
			m_mutex.unlock();
			return true;
		}
		m_mutex.unlock();

		return false;
	}

	void outMsgHandle()
	{
		int command = 0;

		while (true)
		{
			if (getCommand(command))
			{
				cout << "outMsgHandle() : 取出了一个元素==》 " << command << endl;
			}
			else
			{
				cout << "队列为空" << endl;
			}
		}
	}
};

int main()
{
	Command c1;
	thread t1(&Command::inMsgQueue,std::ref(c1));
	thread t2(&Command::outMsgHandle,std::ref(c1));

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

	return 0;
}

在这里插入图片描述
2.线程停止方法
(1)方法
std::chrono::milliseconds dura(100);
std::this_thread::sleep_for(dura);

3.死锁
(1)当有多个互斥量时,如果操作不当会造成死锁
(2)如
mutex m1;
mutex m2;

在线程1中:
m1.lock();
m2.lock();

m2.unlock();
m1.unlock();

在线程2中:
m2.lock();
m1.lock();

m1.unlock();
m2.unlock();

这样会造成死锁,互相等待,但是永远等不到。
(3)解决方法:在不同线程中,申请互斥量的顺序要一致,即都是先申请m1再申请m2

4.std::lock_guard类模板
(1)功能:自动lock和unlock
(2)原理:在构造函数中进行lock,在析构函数中判断mutex如果没有unlock那就自动调用mutex的unlock
(3)用法

在这里插入图片描述
(4)技巧:图中可以直接用一对花括号{ }将相应的代码包含起来,那么lg的生命周期就只在与那个花括号内,出了花括号,lg就析构了

5.std::lock()函数模板
(1)作用:可以同时锁住多个互斥量
(2)特点:要么多个互斥量都成功锁住,要么多个互斥量都没锁住
(3)例子
mutex m1,m2;
std::lock(m1,m2);

若成功锁住了m1后,申请锁住m2时失败了,那么就会把锁住的m1给unlock掉
(4)注意:std::lock()只能保证同时锁住多个互斥量,但是不会自动解锁,所以要手动将那些互斥量unlock

6.解决上述要手动unlock的问题
(1)方法:lock_guard和std::lock结合,并且利用lock_guard的第二个参数std::adopt_lock
(2)std::adopt_lock:这个参数指示lock_guard对象在构造时不对mutex进行加锁
(3)例子
mutex m1,m2;
std::lock(m1,m2);
lock_guard lg1(m1,std::adopt_lock);
lock_guard lg2(m2,std::adopt_lock);

第七课

unique_lock详解

1.unique_lock 是一个类模板,能像lock_guard一样自动lock和unlock
(1)特点:灵活,但效率低,内存占用高,工作中一般用lock_guard
(2)关键参数:std::try_to_lock
(3)例子

在这里插入图片描述

(4)为什么灵活?
答:因为如果尝试lock失败,那么ul并不会像mutex和lock_guard那样阻塞着一直等待lock成功。

2.另一个参数:std::defer_lock
(1)功能:初始化一个没有加锁的mutex
(2)前提:那个mutex对象不能已经被lock了,否则会抛异常
(3)unique_lock类对象有lock和unlock成员函数,直接作用于它绑定的mutex对象
(4)例子

在这里插入图片描述

3.try_lock()成员函数
(1)当unique_lock的第二个参数为std::defer_lock时才适合用
(2)功能:尝试对相应mutex对象加锁,若成功加锁则返回true,否则返回false

4.release()成员函数
(1)功能:返回unique_lock对象所绑定的mutex对象的指针,并释放对该mutex对象的所有权。
(2)注意:如果在release之前绑定的mutex对象已被加锁,那么release之后该mutex对象不会自动解锁,状态不会变
(3)注意与unlock()的作用区分开
(4)例子

在这里插入图片描述

5.关于所有权问题
(1)unique_lock对象对mutex对象的所有权只能转移,不能复制
(2)方法一:用std::move()
mutex m;
std::unique_lock ul1(m);
std::unique_lock ul2(std::move(ul1));
注意:所有权转移前后,m的状态(已加锁或未加锁)不会变
(3)方法二:在函数中返回局部的unique_lock对象
在这里插入图片描述
注意:初始化ul的时候调用的是移动构造函数。

第八课

单例模式,call_once

1.单例对象最好在主线程中创建

2.技巧:双重检查,提高效率

mutex m_mutex;
static Single* GetInstance()
{
  if(m_instance == NULL)
  {
      m_mutex.lock();
      if(m_instance == NULL)
      {
          //.....
	  }
}

	  return m_instance;
}

在lock之前加了一个if(m_instance == NULL),目的是为了当m_instance为空的时候才尝试去lock,而不是不管怎样一上来就lock,这样效率低,因为lock之后可能m_instance为空,但是其他线程拿不到锁只能等着。加了最外层的if(m_instance == NULL)可以减少等待的机率。

3.std::call_once()函数
(1)功能:它的第二个参数是一个函数名,它能保证该函数只被调用一次
(2)它需要与一个标记结合使用,即std::once_flag,这是一个结构体
(3)若成功调用该函数,那将once_flag设置为一种已调用状态
(4)当两个或多个线程同时调用call_once时,只有一个线程能成功调用call_once的第二个参数的函数,其他线程都要等该线程执行完该参数函数,如果标记为已调用,那其他线程不会再调用第二个参数的函数,就像加了锁一样
(5)例子
在这里插入图片描述

第九课

condition_variable,wait,notify_once

1.std::condition_variable
(1)一个类,需要与互斥量配合工作
(2)关键成员函数:wait(),notify_one(),notify_all()
(3)wait:等待,直到当前condition_variable对象的notify_one或notify_all函数被调用,才可能有机会被唤醒;
notify_one:唤醒一个线程里的当前condition_variable对象的wait函数
notify_all:唤醒所有线程里的当前condition_variable对象的wait函数
(4)例子:

#include <iostream>
#include <thread>
#include <condition_variable>
#include <mutex>
#include <list>

#define COUNT 10000

using namespace std;

class Test
{
public:
	std::mutex m_mutex;
	std::condition_variable m_cond;
	list<int> m_list;
	unsigned int m_count;
	bool m_finished;

	Test() 
	{
		m_count = 0; 
		m_finished = false;
	}

	void inMsgQueue()
	{
		for (int i = 0; i < COUNT; i++)
		{
			std::unique_lock<std::mutex> ul(m_mutex);
			m_list.push_back(i + 1);
			m_cond.notify_one();

			cout << "thread id:  " << std::this_thread::get_id() << "  put a command  " << i + 1 << "  in Queue" << endl;
		}
	}

	void outMsgQueue()
	{
		if (m_count < COUNT)
		{
			while (m_count < COUNT)
			{
				std::unique_lock<std::mutex> ul(m_mutex);
				m_cond.wait(ul, [this]() {
					if (m_list.empty())
						return false;
					return true;
					});

				if (!m_finished)
				{
					cout << "thread id:  " << std::this_thread::get_id() << "  got a command  " << m_list.front() << "  in Queue" << endl;
					m_list.pop_front();
					m_count++;
				}
				else
				{
					m_list.pop_front();
				}
			}
		}
		
		cout << "thread id:  " << std::this_thread::get_id() << "  退出了" << endl;

		if (!m_finished)
		{
			m_finished = true;
			m_list.push_back(0);
			m_cond.notify_one();
		}
	}
};

int main()
{
	Test test;
	thread t1(&Test::inMsgQueue, &test);
	thread t2(&Test::outMsgQueue, &test);
	thread t3(&Test::outMsgQueue, &test);

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

	cout << "Mission Finished!" << endl;

	return  0;
}

在这里插入图片描述

(5)解析
a)在outMsgQueue()函数里,当ul拿到锁后,执行到wait函数,当wait函数有两个参数时:
Ⅰ)若第二个参数返回true,则往下执行wait函数后面的代码
Ⅱ)若第二个参数返回false,则立即将ul的互斥量解锁,并在wait函数这一行阻塞,直到在另一个线程中调用notify_one()或notify_all()函数。
另一个线程调用notify_one()或notify_all()函数后,wait函数被唤醒,不断让ul去申请拿到互斥量锁。若加锁成功,则再重新计算wait函数第二个参数的返回值,为true则继续执行wait函数后面的代码,为false则将ul的互斥量解锁,再次在当前行阻塞等待被唤醒;若加锁失败,则一直卡在wait函数这里等待,直到成功加锁。
b)若wait函数只有一个参数,即m_cond.wait(m_mutex);那么它的效果跟有第二个参数时且第二个参数返回false时一样。

第十课

async,future,packaged_task,promise

1.std::async
(1)是一个函数模板,用来启动一个异步任务。启动完一个异步任务后,返回一个std::future对象(类模板)
(2)启动一个异步任务:自动创建一个线程,并执行对应的线程入口函数
(3)std::future:含有对应的入口函数所返回的结果,可以其类的成员函数get()来获取
(4)需要#include
(5)例子:

int entryFunc()
{
	cout << "entryFunc thread id:  " << std::this_thread::get_id() << endl;

	return 1;
}

int main()
{
	cout << "main thread id:  " << std::this_thread::get_id() << endl;

	std::future<int> result = std::async(entryFunc);

	int ret = result.get();

	cout << ret << endl;

	return  0;
}

在这里插入图片描述
(6)解析
(a)用async创建的线程是异步的,即不会阻塞在当前行
(b)future类的get()是阻塞的,即是要等到async里的线程最终返回一个值后get()才会返回
(7)future类里有个wait()函数,也是阻塞的,要等到内部的线程执行完才返回,但是wait()不能像get()那样拿到返回值
(8)注意:构造async对象会创建一个线程,即只有一个线程,那么只能调用future的get()函数一次,不能多次,否则会抛异常,因为get()函数用的是移动语义,调用完一次后result里保存的值已为空
(9)也可以用类的成员函数来初始化async类对象,如:
std::future result = std::async(&Test::func,&test);
(10)若用async创建了线程,而没调用future的get(),若async里的线程要执行很久,则程序会在main函数的return 0处阻塞,等待线程执行完毕才退出

2.async构造时的参数
(1)可以在初始化async时加一个参数:std::launch
(2)std::launch是一个枚举,其有两个值,分别是std::launch::deferred和std::launch::async
(3)std::launch::deferred这个标记表示在初始化async对象时所对应的线程入口函数先不执行,当调用future类对象的wait()或get()函数之后才会执行
注意:用了std::launch::deferred标记,那么当调用了wait()或get()函数之后,那么线程入口函数是在主线程中执行而不是在新线程中执行
(4)std::launch::async这个标记表示强制创建一个线程来执行线程入口函数而不是在主线程中执行
(5)例子:
在这里插入图片描述
(6)若构造async类对象时不加std::launch参数,那么跟下面的情况一样:
std::future result = std::async(std::launch::deferred | std::launch::async,entryFunc);
那么,这种情况下,创不创建新线程来执行入口函数由操作系统决定
(7)所以,async与thread的最大不同:async有时并不创建新线程

3.std::packaged_task
(1)一个类模板,模板参数是可调用对象
(2)作用:把可调用对象包装起来,方便将来作为线程入口函数
(3)成员函数get_future()可以返回一个future对象,拿到线程函数的返回值
(4)例子:
在这里插入图片描述
注意:传给thread对象的时候要用std::ref来将pt的引用传进去才不会拷贝,那么后续调用get_future的时候才会是pt的值而不是pt的拷贝里保存的值

(5)包装lambda表达式
在这里插入图片描述
(6)packaged_task对象本身也是一个可调用对象,如
在这里插入图片描述
4.std::promise
(1)一个类模板
(2)作用:在一个线程中给promise对象赋值,在另一个线程中取promise对象中保存的值
(3)promise类也有get_future成员函数,通过它获取值
(4)例子:
在这里插入图片描述

第十一课

future其他成员函数,shared_future,atomic

1.future类的其他成员函数
(1)wait_for
(2)valid函数,查看返回值是否有效
(3)例子:
在这里插入图片描述
2.std::shared_future
(1)类模板
(2)跟std::future的区别:get()函数只是复制数据,而不是移动语义,所以可以多次调用get()

3.std::atomic
(1)类模板,原子操作,操作过程不会被打断
(2)问题的引出:
比如:
int i = 0;
i++;
对于i++操作,在机器内部是会分成好几步来完成的,当多个线程对i进行自增操作时,那么自增操作的过程很可能还没完成就被另一个线程打断,那么结果是不可预料的
(3)原子操作一般用于变量,而互斥量mutex一般用于代码段
(4)用atomic锁住变量时,原子操作的效率比mutex高
(5)例子:

void func(std::atomic<int>& at)
{
	for (int i = 0; i < 10000; i++)
	{
		at++;
	}
}

int main()
{
	std::atomic<int> at_i = 0;

	thread t(func,std::ref(at_i));
	t.join();

	cout << at_i << endl;

	return  0;
}

(6)注意:atomic不是支持所有操作符都变成原子操作,比如at_i = at_i + 1;这一句不是原子操作

第十三课

windows临界区,其他各种mutex互斥量

1.在windows临界区中,在同一个地方,同一个临界区可以连续enter多次;而在C++11中,在同一个地方,同一个mutex对象不能连续lock多次,否则会抛异常;lock_guard也是

2.std::recursive_mutex
(1)可以在同一个线程中对同一个std::recursive_mutex对象连续lock多次

3.带超时功能的互斥量
(1)std::timed_mutex
(2)std::recursive_timed_mutex
(3)关键成员函数:try_lock_for
(a)例子:

int main()
{
	
	std::chrono::seconds dura(1);
	std::timed_mutex m;
	m.lock();

	if (m.try_lock_for(dura))
	{
		cout << "一秒内拿到锁了" << endl;
	}
	else
	{
		cout << "一秒内没拿到锁,则走到了这里,不会在上面卡着一直申请拿锁" << endl;
		m.unlock();
	}

	return  0;
}

(4)关键成员函数:try_lock_until
(a)例子:
int main()
{
std::chrono::seconds dura(1);
std::timed_mutex m;

if (m.try_lock_until(std::chrono::steady_clock::now() + std::chrono::milliseconds(100)))
{
	cout << "在未来的100毫秒前拿到锁了" << endl;
}
else
{
	cout << "在未来的100毫秒前没拿到锁,则走到了这里,不会在上面卡着一直申请拿锁" << endl;
}

return  0;

}

4.注意condition_variable的wait函数的虚假唤醒

5.对于atomic类对象之间,不能进行拷贝构造和用“=”进行相互赋值,因为对系统来说,很难把这种操作做成原子操作
(1)解决方法:借用atomic类的成员函数
(a)读出:load()
(b)写入:store(值)
(2)例子:
atomic at1 = 0;
atomic at2(at1.load());
at2.store(666);

发布了16 篇原创文章 · 获赞 1 · 访问量 1120

猜你喜欢

转载自blog.csdn.net/u012321968/article/details/104497622