C++11读书笔记—8(多线程使用简介)

C/C++程序员最苦恼的是自己跨平台能力不是一半弱。如果想跨平台,有一大波函数库等着你来深入研究。你再反观java。。。。

一、原子操作

所谓原子操作,就是多线程中“最小的且不可并行化的操作”。通常原子操作都是互斥访问保证的。但是互斥一般靠平台相关汇编指令,这也是为什么C++11之前一直没有做的原因。

#include  //原子操作需要的头文件
#include  //线程头文件
#include 

using namespace std;

atomic_llong total{ 0 };//原子数据类型long long
			//这样的构词法还有atomic_int等等
//下面的东西不是这一节的内容
void func(int)
{
	for (long long i = 0; i < 100000000LL; ++i)
		total = total + i;
}
int main()
{
	thread t1(func, 0);
	thread t2(func, 0);
	t1.join();
	t2.join();
	cout << total << endl;
	return 0;
}
可以通过中查看内置的原子操作。这就出现一个问题。非内置类型始终怎么实现原子操作的。这就是atom模板类,std::atomic t; 如:

atomic ad{ 12.7f };//这种写法是C++11推荐的

原子操作通常属于“资源型”的数据。这意味着多个线程通常只能访问单个原子类型的拷贝。因此C++11中,原子类型只能从其模板参数中进行构造,标准不允许原子类型进行拷贝构造,移动构造,以及使用operator=等 ,以防止出现意外。这样无法编译。

atomic ad1{ad };/./错误


为了避免线程间关于a的竞争。模板改了很多地方。。

atomic a;

int b =a;//相当于b = a.load();

int a =1;//相当于a.store(1);

二、线程

1.线程对象的创建

#include 
#include 

using namespace std;

void func(int)
{}
int main()
{
	thread t1(func, 0);
	t1.join();
	return 0;
}
线程的构造函数参数,可以视为,(要执行的函数名,该函数参数1,该函数参数2,。。。。)
在这里,join是阻塞函数。意义为等上面跑完才开始执行下一个操作。 我们如果没有他会怎么样呢?主线程继续往下跑,跑到return 0;但此时t1线程可能没有跑完,线程对象却要被强制释放。如下面代码;
#include 
#include 
using namespace std;

void func(int a)
{
	for (int i = 0; i < 10; ++i)
		std::cout << a << endl;
}
int main()
{
	int a;//停机变量无意义
	{
		thread t1(func, 1);
		thread t2(func, 2);
	}//运行到这里,t1,t1没有了
	std::cin >> a;
	return 0;
}
如果改成这样,可以正常执行:
int main()
{
	int a;
	{
		thread t1(func, 1);
		thread t2(func, 2);
		t1.join();//主线程被阻塞了,停在这里,等t1线程对象的线程执行完再运行
		t2.join();
	}
	std::cin >> a;
	return 0;
}
C++11读书笔记—8(多线程使用简介)_第1张图片

如果不希望线程被阻塞吗,将线程与线程对象分离可以用t1.detach();将线程与线程对象分离。这里要说明下,线程与线程对象是两码事(这个很重要)。我们仅是依托线程对象来创建线程。

如下;

int main()
{
	int a;
	{
		thread t1(func, 1);
		thread t2(func, 2);
		t1.detach();//主线程没有被阻塞,将线程与线程对象分离
		t2.detach();
	}//运行到这一步,线程对象依然会析构,但是线程却可以继续运行。
	std::cin >> a;
	return 0;
}
这也从侧面描述了,用detach将线程与线程对象分开后,就不能合并了。

2.线程的特点

线程不能复制,但可以移动(即使用std::move())。线程移动后,线程对象t不再代表任何线程。。


另外还可以用std::bind或lambda表达式创建。

		thread t1(std::bind(func, 1));
		thread t2([](int, int) {},1,2);
		t1.join();
		t2.join();

三、互斥量(实质就是锁的变量)

互斥量是一种同步原语,线程同步手段,用于保护多线程同时访问共享数据。“互斥量”的翻译十分有迷惑性。它就是“锁类”。以至于如果不这样理解,将会对后面的条件变量混淆。C++11提供了4种互斥量。

1.独占互斥量std::mutex

互斥量接口都很相似,一般用法是通过lock()方法来阻塞线程,直到获得互斥量所有权为止。线程获得互斥量并完成任务之后,就必须使用unlock()来解除对互斥量的占用,lock()和unlock()必须成对出现。try_lock()尝试锁定互斥量,如成功返回true如失败返回false,他是非阻塞的,看来可以用来检查当前互斥量的状态。

改动上面的程序将函数变成加锁的:

#include 
#include 
#include 
using namespace std;

std::mutex uni_lock;

void func(int a)
{
	uni_lock.lock();
	for (int i = 0; i < 10; ++i)
		std::cout << a << endl;
	uni_lock.unlock();
}
int main()
{
	int a;
	{
		thread t1(func, 1);
		thread t2(func, 2);
		t1.join();
		t2.join();
	}
	std::cin >> a;
	return 0;
}

这显示这就友好了

C++11读书笔记—8(多线程使用简介)_第2张图片

官方建议尽量使用更安全的lock_guard。因为他在构造时自动加锁,析构时自动解锁,防止忘解锁的事情发生。lock_guard是个 类模板,形如其名托管互斥量。后面的几个互斥量基本都用这种方法。

std::mutex u_lock;
void func(int a)
{
	std::lock_guard locker(u_lock);
	for (int i = 0; i < 10; ++i)
		std::cout << a << endl;

}

2.递归互斥量std::recursive_mutex

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

需要说明的是,还是尽量不要使用递归互斥量的好

(1)需要用到递归锁定的多线程,往往可以简化为迭代。允许递归容易放纵复杂逻辑产生。

(2)递归锁效率一般低一些。

(3)递归超过一定数目再lock进行调用会抛出std::system错误

3.带超时的互斥量std::timed_mutex与std::recursive_timed_mutex

可以看做前两个锁的改进。超时锁,主要用在获取锁时增加超时等待功能,因为有时不知道获取锁需要多久,为了不至于一直在等待获取互斥量,就设置一个等待超时时间。我们用try_lock_for,try_lock_until两个接口设置互斥量超时时间。
std::timed_mutex u_lock;

void func(int a)
{
	std::chrono::milliseconds timeout(100);
	while (1)
	{
		if (u_lock.try_lock_for(timeout))
		{
			///...///
		}
	}

}

4.给互斥量上的的两种区域锁

上面我们介绍了lock_guard,这其实是一种区域锁,内部用实现机制是类模板。
std::lock_guard,方便线程对互斥量上锁。
std::unique_lock,方便线程对互斥量上锁,但提供了更好的上锁和解锁控制。两者功能很类似吗,但有一点区别。
用lock_guard去guard一个mutex,必然是在lock_guard的对象离开其作用域时unlock它所guard的mutex,不提供提前unlock的功能。
而unique_lock则提供这个功能,除了像lock_guard一样在离开作用域时unlock它guard的mutex外,unique还提供unlock函数,使用者可以手动执行unlock。此外,unique_lock还可以设置超时,下一部分条件变量就是用更灵活的unique_lock。

四、条件变量

条件变量是C++11提供的另一种用于等待的同步机制,它能阻塞一个或多个线程。直到收到另一个线程发出的通知或者超时,才会唤醒当前阻塞的线程。条件变量需要和互斥的量配合起来用。C++11提供两种条件变量。
  • std::condition_variable:必须与std::unique_lock配合使用(上文提到一种区间锁)
  • std::condition_variable_any:更加通用的条件变量,可以与任意类型的锁配合使用,相比前者使用时会有额外的开销。

他们的成员函数相同。 

成员函数 说明
notify_one 通知一个等待线程
notify_all 通知全部等待线程
wait 阻塞当前线程直到被唤醒
wait_for 阻塞当前线程直到被唤醒或超过指定的等待时间(长度)
wait_until 阻塞当前线程直到被唤醒或到达指定的时间(点)
与其他语言一样,条件变量必须与锁一起使用。就像这样,无论是notify_one或notify_all都是类似于发出脉冲信号,如果对wait的调用发生在notify之后是不会被唤醒的,所以接收者在使用wait等待之前也需要检查条件(标识)是否满足,另一个线程(通知者)在nofity前需要修改相应标识供接收者检查。
下面是一个例子来整合一下我们上面提到的应用:
#include              // std::cout
#include                // std::thread
#include                 // std::mutex, std::unique_lock
#include    // std::condition_variable


std::mutex mtx;					// 全局互斥锁.
std::condition_variable cv;		// 全局条件变量.
bool ready = false;	// 全局标志位.
//下面是重点
void do_print_id(int id)
{
	std::unique_lock  lck(mtx); //独占锁
	while (!ready)				// 如果标志位不为 true, 则等待...
		cv.wait(lck);			// 当前线程被阻塞, 当全局标志位变为 true 之后,
								// 线程被唤醒, 继续往下执行打印线程编号id.
	std::cout << "thread " << id << '\n';
}

void go()
{
	std::unique_lock  lck(mtx);
	ready = true;				// 设置全局标志位为 true.
	cv.notify_all();			// 通知唤醒所有线程.与上面额wait函数有关。
}
//上面是重点。main函数就是为了生成10个线程。每个线程先死循环,之后突然运行go()打开死循环。
int main()
{
	std::thread threads[10];
	//下面开10个线程:
	for (int i = 0; i < 10; ++i)
		threads[i] = std::thread(do_print_id, i);

	go(); // go!

	for (auto & th : threads) //直接诶是
		th.join();

	return 0;
}
C++11读书笔记—8(多线程使用简介)_第3张图片

 
   

你可能感兴趣的:(C-C++)