C++11线程库简介

前言

        在c++11之前涉及多线程的问题都是和平台相关的,比如windows和linux都有一套自己的接口,这使得代码的可移植性变差。C++11中最重要的特性就是对线程进行了支持,使得C++在编程时不再依赖第三方库,而且原子操作中还引入了原子类的概念,要使用标准库的线程,必须包含头文件,让我们一起来了解一下吧。

目录

1.thread类的简单介绍

2.线程函数参数

3.原子性操作库

4.lock_guard和unique_lock

        4.1mutex的种类

        4.2lock_guard

 4.3unique_lock 

5.例子 


1.thread类的简单介绍

        线程类 

         

thread类成员函数说明
函数名 功能
thread()       构建一个线程对象,没有关联任何线程函数,即没有启动任何线程 
thread(fn,args1,args2...) 构建一个线程对象,并关联线程函数fn,args1        ,args2,...为线程函数的参数
get_id() 获取线程的id
jionable() 线程是否还在执行,jionable代表一个正在执行中的线程
jion() 该函数调用后会阻塞住线程,该线程执行结束后,主线程继续执行。
detach() 在创建线程对象后马上调用,用于把被创建的线程与线程对象分离,分离后的线程变为后台线程,创建的线程“死活”就与主线程无关了

        注意:

        1.线程是操作系统的一个概念,线程对象可以关联一个线程用于控制线程以及获取线程的状态。

         2.当创建一个线程对象后,没有提供线程函数,该对象实际没有对应任何线程。

#include
int main()
{
	std::thread t1;
	cout << t1.get_id() << endl;
	return 0;
}

        get_id()的返回值是一个id类,id类型实际上为std::thread命名空间下封装的一个类,该类中包含一个结构体:

        // vs下查看
typedef struct
{

        /* thread identifier for Win32 */
        void *_Hnd; /* Win32 HANDLE */
        unsigned int _Id;
} _Thrd_imp_t; 

        3.当创建一个线程对象后,并且给出线程关联的线程函数,该线程就被启动了,与主线程一起运行。线程函数一般情况下可以按照以下三种提供方式。

        1.函数指针

        2.lambda表达式

        3.函数对象 

        

void ThreadFunc(int a)
{
	cout << "ThreadFunc" << a << endl;
}
class TF
{
public:
	void operator()()
	{
		cout << "Thread3" << endl;
	}
};
#include
int main()
{
	//线程函数为函数指针
	std::thread t1(ThreadFunc,10);
	//线程函数为lambda表达式
	thread t2([]() {cout << "Thread2" << endl;});
	//线程函数为函数对象
	TF tf;
	thread t3(tf);

	t1.join();
	t2.join();
	t3.join();
	cout << "Mian thread" << endl;
	return 0;
}

         4.thread类是防拷贝的,不允许拷贝构造和赋值,但是可以移动构造和移动赋值,即将一个线程对象关联线程的状态转移给其他线程对象,转移期间不影响线程的执行。

        5.可以通过判断joinable()函数,判断线程是否有效,如果在一下任意情况下,则线程是无效的

        采用无参构造函数构造的线程对象

        线程对象的状态已经转移给其它对象

        线程已经调用join或者detach结束

2.线程函数参数

        线程函数的参数是以值拷贝的方式拷贝到线程空间的,因此即使线程参数为引用类型,在线程修改后也不能修改外部实参,因为其实际引用的是线程栈中的拷贝,而不是外部实参

#include
void ThreadFuncT1(int&  x)
{
	x = x + 10;
}
void ThreadFuncT2(int* x)
{
	*x *= 10;
}
int main()
{
	int a = 10;
	//在线程函数中对a修改不会影响外部实参,因为线程函数虽然是引用方式,但是其实引用的是线程栈中的拷贝
	thread t1(ThreadFuncT1, a);
	t1.join();
	cout << a << endl;
	//如果想要通过形参改变外部实参时,必须借助std::ref()函数
	thread t2(ThreadFuncT1, std::ref(a));
	t2.join();
	cout << a << endl;

	//地址拷贝
	thread t3(ThreadFuncT2, &a);
	cout << a << endl;
	t3.join();
	//ruhou
	return 0;

}

         注意:如果是类的成员函数作为线程参数的时候,必须将this指针作为线程函数参数。

3.原子性操作库

        多线程最主要的问题就是数据共享带来的问题(即线程安全的问题)。如果共享数据都是只读的,那么没有什么问题,因为只读操作不会影响到数据,更不会涉及对数据的修改,所以所有的线程都会获得同样的数据。但是,当一个或者多个线程要对数据进行修改共享数据的时候,就会产生很多潜在的麻烦。比如:

#include
#include
using namespace std;
unsigned long sum = 0L;


int main()
{
	cout << "Before joining,sum = " << sum << std::endl;
	int sum = 0;
	thread t1([&](int num) {for (int i = 0; i < num; ++i)
	{
		++sum;
	}}, 10000000);
	thread t2([&](int num) {for (int i = 0; i < num; ++i)
	{
		++sum;
	}}, 10000000);
	t1.join();
	t2.join();
	cout << "After joining,sum = " << sum << std::endl;
	return 0;
}

        这样加起来的结果是不准确的。

        C++98中会采取传统的方式解决:可以对共享修改的数据进行加锁保护。

#include
#include
#include
using namespace std;
unsigned long sum = 0L;


int main()
{
	std::cout << "Before joining,sum = " << sum << std::endl;
	int sum = 0;
	mutex mu;
	thread t1([&](int num) {for (int i = 0; i < num; ++i)
	{
		mu.lock();//加锁
		++sum;
		mu.unlock();//解锁
	}}, 10000000);
	thread t2([&](int num) {for (int i = 0; i < num; ++i)
	{
		mu.lock();//加锁
		++sum;
		mu.unlock();//解锁
	}}, 10000000);
	t1.join();
	t2.join();
	std::cout << "After joining,sum = " << sum << std::endl;
	return 0;
}

        虽然加锁可以解决这个问题,但是代码的运行效率会很慢,而且如果一个线程正在访问sum其他想要访问sum的线程就会被阻塞。会影响程序的运行效率。而且如果锁控制不好,还容易造成死锁。

        因此在C++11中 引入了原子操作。所谓的原子操作:即不可被中断的一个或者一系列的操作。C++11引入原子操作类型,使得线程间的数据同步变得非常高效。C++11线程库简介_第1张图片

        注意:使用以上的原子操作变量的时候,必须添加头文件。

int main()
{
	std::cout << "Before joining,sum = " << sum << std::endl;
	atomic_int sum = 0;//原子类型的变量sum
	mutex mu;
	thread t1([&](int num) {for (int i = 0; i < num; ++i)
	{
	
		++sum;
		
	}}, 10000000);
	thread t2([&](int num) {for (int i = 0; i < num; ++i)
	{
		++sum;
	}}, 10000000);
	t1.join();
	t2.join();
	std::cout << "After joining,sum = " << sum << std::endl;
	return 0;
}

         在C++11中,程序员不需要对原子类型的变量进行加锁解锁的操作,线程能够对原子变量互斥访问。

        更为普遍的是,程序员可以使用atomic类型的模版,定义出需要的任意原子类型。

        atomic t;//声明一个类型为T的原子变量类型

         注意:原子类型通常属于“资源型数据”,多个线程只允许访问单个原子类型的拷贝,因此在C++11中,原子类型只能从模版参数中进行构造,不允许原子类型进行拷贝构造,移动构造以及operator=等,为了防止意外,标准库已经将atomic模版类中拷贝构造,赋值运算符重载,移动构造全都进行了删除。

int main()
{
	atomic a1;
	//会报错
	atomic a2(a1);
	atomic a3;
	a3 = a1;
	return 0;
}

4.lock_guard和unique_lock

        在多线程的环境, 如果想要保证某个变量的安全性,只要将其设置为对应的原子类型就可以了,高效又不容易出现死锁的问题。但是在某些情况下:我们可能需要保证一段代码的安全性,那么就需要通过锁的方式来进行控制。

        比如一个线程对变量number进行加1 ,100次,另外一个-1,100次,每次加减以后都要输出number的值。要求最后number的值为1;

int main()
{
	
	int number = 1;
	mutex mu;
	thread t1([&](int num) {for (int i = 0; i < num; ++i)
	{
		mu.lock();
		++number;
		cout << number << endl;
		mu.unlock();
	}}, 100);
	thread t2([&](int num) {for (int i = 0; i < num; ++i)
	{
		mu.lock();
		--number;
		cout << number << endl;
		mu.unlock();
	}}, 100);
	t1.join();
	t2.join();
	cout << number << endl;
}

        上述代码存在一定的缺陷:如果锁控制不好,可能会造成死锁,最常见的比如在锁的中间返回,或者还没有解锁就抛异常了,因此C++11采用RALL的方式对锁进行封装,即lock_guardunique_lock。 

        4.1mutex的种类

        在C++11中mutex总共包含四种互斥量的种类:

        1.std::mutex

        C++11提供的最基本的互斥量,该类的对象之间不能互相拷贝,也不能进行移动 ,mutex最常用的三个函数:

函数声明 函数功能
lock()         加锁
unlock() 解锁
try_lock() 尝试锁住互斥量,如果互斥量被其它线程占有,则当前线程也不会被阻塞

        注意:

        线程函数调用lock()时,可能会出现一下三种情况:

        1.如果该互斥量没有被锁住,则线程会将该互斥量锁住,直到调用unlock()之前,该线程都是有锁的。

        2.如果当前互斥量被其他线程锁住,则当前的调用线程会被阻塞。

        3.如果当前互斥量已经被其他线程锁住则会造成死锁。

        线程调用try_lock()时,可能会发生一下三种情况:

        1.如果当前互斥量没有被其它线程占用,则该线程锁住该互斥量,直到该线程调用unlock()解锁释放互斥量。

        2.如果当前被其它线程占用,则调用线程返回false,并且不会阻塞。

        3.如果当前互斥量被当前线程锁住,则会造成死锁

        2.std::recursive_mutex

        其允许同一个线程对互斥量多次上锁(即递归锁),来获得对互斥对象的对层所有权,释放互斥量时,需要调用与该锁层次深度相同的unlock(),除此之外std::recursive_mutex的特性和std::mutex的特性大致相同。

        3.std::timed_mutex

        比std::mutex 多了两个成员,try_lock_for(),try_lock_until()。

        try_lock_for: 

        接受一个时间范围,表示在这段时间范围之内如果线程如果没有获得锁则被阻塞住,与std::mutex的try_lock不同,try_lock如果被调用时没有获得锁直接返回false,如果在此期间其它线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指定的时间内没有获得锁),则返回false。

        try_lock_until()

        接收一个时间点作为参数,在指定的时间点未到来之前线程如果没有获得锁则被阻塞,如果在此期间其它线程释放了锁,则该线程可以获得互斥量的锁,如果超时(即在指定的饿时间内还没有获得锁),则返回false。

        4.std::recursive_timed_mutex 

        4.2lock_guard

        std::lock_guard时C++中定义的模版类,定义如下:

    template
	class lock_guard
	{
	public:
		//构造之前mu还未加锁
		explicit lock_guard(Mutex& mu)
			:_mu(mu)
		{
			_mu.lock();
		}
        // 在构造lock_gard时,_Mtx已经被上锁,此处不需要再上锁
        lock_guard(_Mutex& _Mtx, adopt_lock_t)
        : _MyMutex(_Mtx)
        {}
		//析构函数进行解锁
		~lock_guard()
		{
			_mu.unlock();
		}
		//不允许拷贝构造和赋值
		lock_guard(const Mutex& mu) = delete;
		Mutex& operator()(const Mutex& mu) = delete;
	private:
		Mutex & _mu;
	};

        通过上述代码我们可以看到,lock_guard类模板主要通过RAII的方式,对其管理的互斥量进行封装,在需要加锁的地方,只需要对上述介绍的任意互斥的体实例化一个lock_guard,调用构造函数成功上锁,出作用域前,lock_guard 对象要被销毁,调用析构函数完成解锁的过程因为析构函数是对象声明周期结束以后自动调用的,所以可以自动解锁,这样就有效的避免了死锁问题

        lock_guard的缺陷是太过单一,用户没有办法对该锁进行控制,因此C++11又提供unique_lock. 

 4.3unique_lock 

        与lock_gard类似,unique_lock类模板也采用 RAII的方式对锁将进行封装,并且也是以独占所有权的方式管理mutex对象的上锁和解锁操作,即其对象之间不能发生拷贝,在构造(或者移动(move)赋值时),unique_lock对象需要传递一个Mutex对象作为它的参数,新创建的unique_lock对象负责传入一个Mutex对象的上锁和解锁操作。使用以上类型互斥量实例化unique_lock的对象时,自动调用构造函数上锁,unique_lock对象销毁自动调用析构函数对锁进行解锁。可以很方便防止死锁问题。

       与lock_gard不同的是,unique_lock更加的灵活,提供了更多的成员函数:

        1.上锁/解锁操作:lock、try_lock,try_lock_for,try_lock_until和unlock。

        2.修改操作:移动赋值,交换(swap:与另一个unique_lock对象互换所管理的互斥量所有权),释放(release:返回它所管理的互斥量对象的指针,并释放所有权)

        3.获取属性:owns_lock(返回当前对象是否上锁),operator bool()(与owns_lock()功能相同),mutex(返回当前unique_lock所管理的互斥量的指针)。

5.例子 

        支持两个线程交替打印,一个打印奇数,一个打印偶数。

        主要是依靠条件变量加互斥量来实现的。 

#include
int main()
{
	int n = 1000;
	//创建两个锁和两个条件变量
	mutex mu1;
	mutex mu2;
	condition_variable cv1;
	condition_variable cv2;

	thread t1([&]() {for (int i = 0; i < n; i+=2)
		{
		unique_lockmut1(mu1);
			cout << i << endl;
        //打印之后通知另一个进程,然后自己也等待通知
		cv2.notify_one();
		cv1.wait(mut1);
		}
	});
	thread t2([&]() {for (int i = 1; i < n; i += 2)
		{
		unique_lockmut2(mu2);
        //等待进程的通知
		cv2.wait(mut2);
			cout << i << endl;
        //打印完后通知另一个进程
		cv1.notify_one();
		}
	});

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

你可能感兴趣的:(c++,开发语言)