前言
在c++11之前涉及多线程的问题都是和平台相关的,比如windows和linux都有一套自己的接口,这使得代码的可移植性变差。C++11中最重要的特性就是对线程进行了支持,使得C++在编程时不再依赖第三方库,而且原子操作中还引入了原子类的概念,要使用标准库的线程,必须包含<thread>头文件,让我们一起来了解一下吧。
目录
1.thread类的简单介绍
2.线程函数参数
3.原子性操作库
4.lock_guard和unique_lock
4.1mutex的种类
4.2lock_guard
4.3unique_lock
5.例子
1.thread类的简单介绍
线程类
函数名 | 功能 |
thread() | 构建一个线程对象,没有关联任何线程函数,即没有启动任何线程 |
thread(fn,args1,args2...) | 构建一个线程对象,并关联线程函数fn,args1 ,args2,...为线程函数的参数 |
get_id() | 获取线程的id |
jionable() | 线程是否还在执行,jionable代表一个正在执行中的线程 |
jion() | 该函数调用后会阻塞住线程,该线程执行结束后,主线程继续执行。 |
detach() | 在创建线程对象后马上调用,用于把被创建的线程与线程对象分离,分离后的线程变为后台线程,创建的线程“死活”就与主线程无关了 |
注意:
1.线程是操作系统的一个概念,线程对象可以关联一个线程,用于控制线程以及获取线程的状态。
2.当创建一个线程对象后,没有提供线程函数,该对象实际没有对应任何线程。
#include<thread>
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<thread>
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<thread>
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<thread>
#include<iostream>
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<thread>
#include<iostream>
#include<mutex>
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引入原子操作类型,使得线程间的数据同步变得非常高效。
注意:使用以上的原子操作变量的时候,必须添加头文件。<atomic>
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;//声明一个类型为T的原子变量类型
注意:原子类型通常属于“资源型数据”,多个线程只允许访问单个原子类型的拷贝,因此在C++11中,原子类型只能从模版参数中进行构造,不允许原子类型进行拷贝构造,移动构造以及operator=等,为了防止意外,标准库已经将atomic模版类中拷贝构造,赋值运算符重载,移动构造全都进行了删除。
int main()
{
atomic<int> a1;
//会报错
atomic<int> a2(a1);
atomic<int> 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_guard和unique_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 Mutex>
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<condition_variable> 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_lock<mutex>mut1(mu1); cout << i << endl; //打印之后通知另一个进程,然后自己也等待通知 cv2.notify_one(); cv1.wait(mut1); } }); thread t2([&]() {for (int i = 1; i < n; i += 2) { unique_lock<mutex>mut2(mu2); //等待进程的通知 cv2.wait(mut2); cout << i << endl; //打印完后通知另一个进程 cv1.notify_one(); } }); t1.join(); t2.join(); return 0; }