线程
在编程中,线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。线程自身不拥有系统资源,只拥有一点在运行中必不可少的资源,但它可以与同属进程的其他线程共享进程所拥有的全部资源。
使用线程,可以实现一段程序不同的地方并发的执行,极大的提高运行效率
在c++11之前,线程库主要由操作系统或第三方软件提供,linux下为pthread库,windows下为Win32 API ,这样的代码可移植性较差,c++11引入了线程库thread ,这样做可以把可移植性问题交给不同平台的编译器,减少了出问题的概率.
创建线程与线程赋值
线程不支持拷贝构造和拷贝赋值
#include <thread>
void fun(string str)
{
cout << str << endl;
}
int main() {
thread t1(fun, "helloworld");
thread t2;
// t2 = t1; 错误,不支持赋值重载
// t2 = move(t1); 不报错,但不应该这么写
t2 = thread(fun, "helloworld"); // 正确,支持移动构造和移动赋值
t1.join();
t2.join();
system("pause");
return 0;
}
常用类方法
- join() 线程等待,一般用于主线程(如果主线程先于其他线程退出,此时其他线程的资源没人可以回收,就会报错)
- get_id() 返回线程id,类型为thread::id
- joinable() 返回一个布尔值,如果该线程没有被回收返回true,否则为false
- detach() 将本线程与主线程分离开,二者不再有联系(慎用!!!)
线程函数参数为引用时
由于线程不支持拷贝构造与拷贝赋值,所以线程在构造时使用的是参数的拷贝,如果函数的一个参数为引用,将无法实现传递,这时就需要一个神奇的函数ref
void fun(int& a)
{
++a;
cout << a << endl;
}
int main() {
int a = 0;
thread t[5];
for (int i = 0; i < 5; i++) {
// t[i] = thread(fun, a); // 错误,引用无法直接传递
t[i] = thread(fun, ref(a)); //引用需要调用 ref
}
for (int i = 0; i < 5; i++) {
t[i].join();
}
cout << a << endl;
system("pause");
return 0;
}
原子性操作
多线程最主要的问题是共享数据带来的问题( 即线程安全 ) 。如果共享数据都是只读的,那么没问题,因为只读操作不会影响到数据,更不会涉及对数据的修改,所以所有线程都会获得同样的数据。但是,当一个或多个线程要修改共享数据时,就会产生很多潜在的麻烦 。传统的解决方案是通过加锁保证同一时间内只允许一个线程进行修改和操作数据,不过这种方式也存在一些问题,比如当代码不是很长而加锁部分占据代码的很大一部分时,将会非常影响效率在c++11中引入了一种新的解决方案,即原子操作,原子操作是指不可被中断的一个或一系列操作,适当的使用原子操作会比锁的效率更高需包含头文件 <atomin>
对于内置类型
也可自定义类型
atmoic < T > t ; // 声明一个类型为 T 的原子类型变量 t
注意:原子类型通常属于 " 资源型 " 数据,多个线程只能访问单个原子类型的拷贝,因此 在 C++11 中,原子类型只能从其模板参数中进行构造,不允许原子类型进行拷贝构造、移动构造以及 operator= 等,为了防止意外,标准库已经将 atmoic 模板类中的拷贝构造、移动构造、赋值运算符重载默认删除掉了。
#include <atomic>
int main()
{
atomic<int> a1(0);
//atomic<int> a2(a1); // 编译失败
atomic<int> a2(0);
//a2 = a1; // 编译失败
return 0; }
锁
需包含头文件 mutex
头文件mutex包含4种锁
1.std::mutex
C++11 提供的最基本的互斥量,该类的对象之间不能拷贝,也不能进行移动。 mutex 最常用
的三个函数:
- lock 上锁
- unlock 解锁
- try_lock 尝试去锁
线程函数调用 lock() 时,可能会发生以下三种情况:
- 如果该互斥量当前没有被锁住,则调用线程将该互斥量锁住,直到调用 unlock之前,该线程一直拥有该锁
- 如果当前互斥量被其他线程锁住,则当前的调用线程被阻塞住
- 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)
线程函数调用 try_lock() 时,可能会发生以下三种情况:
- 如果当前互斥量没有被其他线程占有,则该线程锁住互斥量,直到该线程调用 unlock释放互斥量
- 如果当前互斥量被其他线程锁住,则当前调用线程返回 false,而并不会被阻塞掉
- 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)
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
相当于2,3的粘合
c++11避免死锁
锁控制不好时,可能会造成死锁,最常见的比如在锁中间代码返回,或者在锁的范围内抛异常。因此:C++11采用RAII的方式对锁进行了封装,即lock_guard和unique_lock。简单点来说就是用类来封装锁,然后通过控制类的构造与析构来避免死锁问题(RAII)
lock_ground<锁的类型> lg(锁)在lock_ground的构造处进行上锁,析构处进行解锁,从而实现加锁缺陷,功能过少
unique_lock<锁的类型> ul(锁)同理在构造处进行上锁,析构处进行解锁,从而实现加锁unique_lock还提供了许多类方法来更好的管理锁上锁 / 解锁操作 : lock 、 try_lock 、 try_lock_for 、 try_lock_until 和 unlock修改操作 :移动赋值、交换 (swap :与另一个 unique_lock 对象互换所管理的互斥量所有权) 、释放 (release :返回它所管理的互斥量对象的指针,并释放所有权 )获取属性 : owns_lock( 返回当前对象是否上了锁 ) 、 operator bool()( 与 owns_lock() 的功能相 同) 、 mutex( 返回当前 unique_lock 所管理的互斥量的指针 ) 。