C++11多线程库重点接口

目录

一.thread构造函数

二.移动构造,移动赋值

小结 

 三.获取线程id的方法

四.thread与lambda表达式联用

 五.Mutexs的总览

六.互斥锁 

七.Locks的总览

八. 条件变量总览

九.条件变量的wait和notify 

十.典型例题

十一.原子类

十二.智能指针和单例模式的线程安全问题


一.thread构造函数

void Print(int n)
{
	for (int i = 0; i < n; i++)
	{
		cout << "I am a thread: " << i << endl;
	}
}
int main()
{
	thread t(Print, 1000);
	t.join();
	return 0;
}

如果想要传递引用,需要使用ref()函数将对象包装成引用以便向下传递。原因在于参数并不是直接传递给执行的方法,中间经过了thread的构造函数,为了完美转发保持属性而产生副作用,生成了一个拷贝的临时对象,则执行方法中引用的是临时对象,非const左值引用来引用临时对象是不被允许的。(简单说一说,底层非常复杂,不值得深究)。

如果用const引用来接收就不必使用ref

bind也是如此,如果想要传递引用就必须ref

二.移动构造,移动赋值

thread类的拷贝构造被禁用,但是有移动构造和移动赋值,以应对需要同时创建多个线程的场景

void Print(int j, int n)
{
	for (int i = j; i < n; i++)
	{
		cout << "thread id: " << this_thread::get_id() << ", " << i << endl;
	}
}
int main()
{
	const int threadNum = 5;
	vector<thread> threads(threadNum);
	for (int i = 0; i < threads.size(); i++)
	{
		threads[i] = thread(Print, 0, 1000); //匿名对象右值,调用移动赋值,减少拷贝
	}

	for (int i = 0; i < threads.size(); i++)
	{
		threads[i].join();
	}

	return 0;
}

//移动构造:
/*
for (int i = 0; i < threadNum; i++)
{
	threads.emplace_back(thread(Print, 0, 1000)); //emplace_back调用移动构造

}
*/

思考:thread禁用了拷贝构造,有办法把用一个thread对象构造另一个对象吗? 

//使用移动构造:副作用:t1将不可用
thread t1(Print, 0, 1000);
thread t2(move(t1));

小结 

创建线程的方法:

  1. 用有参的构造传递方法和参数
  2. 创建多个线程用容器集中管理时,使用移动构造或者移动赋值
  3. 将一个线程的资源转移给另一个线程,move+移动构造

 三.获取线程id的方法

//线程的执行方法中获取:
this_thread::get_id();
//线程外获取:
t.get_id(); //t是创建的线程对象

四.thread与lambda表达式联用

lambda表达式的优势在于能捕获局部的变量,而无需传参

int x = 0;
int main()
{

	int n1 = 10000;
	int n2 = 10000;
	thread t1([n1]()
		{
			for (int i = 0; i < n1; i++)
			{
				x++;
			}
		}
	);
	

	thread t2([n2]()
		{
			for (int i = 0; i < n2; i++)
			{
				x++;
			}
		}
	);
	t1.join();
	t2.join();
	cout << x << endl;
	return 0;
}

//注意:以上代码有线程安全问题

 五.Mutexs的总览

六.互斥锁 

mutex不支持拷贝,只能引用

int x = 0;
void Add(int n, mutex& mtx) //这里的互斥锁只能用引用接收,因为mutex禁用了拷贝构造
//mtx不能加const,否早无法lock,unlock
{
	mtx.lock();
	for (int i = 0; i < n; i++)
	{
		x++;
	}
	mtx.unlock();
}
int main()
{
	mutex mtx;
	thread t1(Add, 10000, ref(mtx)); //想要传锁必须使用ref把mutex包装成一个引用对象,
	thread t2(Add, 10000, ref(mtx)); 
//因为mtx不是直接传给Add的,中间经过thread的构造函数,
//最终Add引用的是mtx的拷贝(完美转发保持属性的同时生成了一个拷贝的临时对象,简单说一说,不必深究)
	t1.join();
	t2.join();
	cout << x << endl;
	return 0;
}

七.Locks的总览

八. 条件变量总览

九.条件变量的wait和notify 

 十.典型例题

int main()
{
	//让t1和t2两个线程,从1开始,交替打印奇数和偶数
	int x = 1;
	mutex mtx;
	condition_variable cond;
	thread t1([&]()
		{
			while (true)
			{
				unique_lock<mutex> lck(mtx);
				while (x % 2 == 0)
				{
					cond.wait(lck);
				}
				cout << "thread 1:" << x << endl;
				x++;
				Sleep(1000);
				cond.notify_one();
			}
		});

	thread t2([&]()
		{
			while (true)
			{
				unique_lock<mutex> lck(mtx);
				while (x % 2 == 1)
				{
					cond.wait(lck);
				}
				cout << "thread 2:" << x << endl;
				x++;
				Sleep(1000);
				cond.notify_one();
			}

		});

	t1.join();
	t2.join();
	return 0;
}

十一.原子类

临界区操作非常少的情况,不适合加互斥锁。因为这会导致线程状态频繁切换,不断地阻塞和唤醒。此时就可以使用C++多线程库提供的atomic类来封装内置类型,使用atomic类提供的“原子”接口。这里的原子二字打了引号,因为严格来说它并非真正意义上的原子,只不过表现出来的是原子性的效果。当然日常交流中说它是原子操作也没问题。

首先需要明确一个概念:被保护后的临界区,也并不是原子的。原子操作是不可中断的操作,要么全部执行成功,要么全部不执行,没有中间状态。而临界区操作涉及多个指令或操作步骤,收线程调度影响,可能会被中断。只不过采用互斥的机制保护,使得任意时刻只有一个线程进入临界区,从而避免了数据不一致问题,达到了原子性的效果。

CAS操作:CAS即Comparre and Swap,它是CPU提供的原子操作。这才是真正意义上的原子操作,它由硬件支持,X86下对应的是CMPXCHG 汇编指令。

这个操作所做的工作是:先比较目标内存的值是否和预期的值一样(Compare),如果一样就进行赋值(Swap->把寄存器的值和目标内存的值进行交换),如果不一样就啥都不做

bool compare_and_swap (int *addr, int oldval, int newval)
{
  if ( *addr != oldval ) {
      return false;
  }
  *addr = newval;
  return true;
}

atomic的接口底层就是使用的CAS操作。

例如:

int a = 0;
a++;

atomic<int> a(0);
a++;

atomic或者说CAS操作适用于对公共资源操作非常简单的情况,例如对一个整型变量进行++,那么CAS的成功率就比较高,相较于加锁,可以减少线程状态切换的开销。但是如果操作比较多,还是加锁合适。

十二.智能指针和单例模式的线程安全问题

智能指针是线程安全的吗?

首先,unique_ptr通常用于独占所有权的情况,它被设计为独占所有权的智能指针,通常情况下只能由一个线程拥有和访问。因此,不建议多个线程使用同一个unique_ptr,其内部并没有提供线程安全保护机制,使用结果是未定义的。

其次,shared_ptr中的引用计数是会被公共访问的资源,涉及到++,--,判断等操作。shared_ptr内部提供了保护措施,要么是加锁保护,要么使用atomic这样的原子操作。

 

所以,虽然unique_ptr没有保护机制,但是人家设计的初衷就是用于独占资源的情况,不按要求使用那是你自讨苦吃,而shared_ptr内部有保护机制,可以供多个线程使用。

综上,智能指针是线程安全的,但是智能指针管理的资源不是线程安全的,需要用户自己保护。

 单例模式是线程安全的吗?

饿汉模式在main函数之前就创建了对象,没有线程安全问题。但是懒汉模式就有,调用获取实例的接口,首先会判断指向实例的指针是否为空,如果为空就创建实例。那么就有可能多个线程同时通过if判断,而创建出多个实例。

所以需要在判断之前加锁保护。

但是线程安全问题仅仅是第一次调用接口时才会出现,后续每次获取实例都要申请锁太消耗时间了,所以外层再套一个if判断,如果是空指针就加锁,否则直接返回。

其实懒汉模式还有另一种简单巧妙的实现方式,不把实例创建在堆区,而是创建静态对象。

这份代码在C++11之后是线程安全的,静态对象不管如何只会初始化一次,初始化静态对象是原子的。

但C++11之前它是线程不安全的,静态对象在多线程情况下初始化动作式未定义的。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值