在C++11之前,C++没有对线程提供语言级别的支持,各种操作系统和编译器实现线程的方法不一样。
C++11增加了线程以及线程相关的类,统一编程风格、简单易用、跨平台。
创建线程
头文件:#include <thread>
线程类:std::thread
构造函数
1)thread() noexcept;
默认构造函,构造一个线程对象,不执行任何任务(不会创建/启动子线程)。
2)template< class Function, class... Args >
explicit thread(Function&& fx, Args&&... args );
创建线程对象,在线程中执行任务函数fx中的代码,args是要传递给任务函数fx的参数。
任务函数fx可以是 普通函数、类的非静态成员函数、类的静态成员函数、lambda函数、仿函数
下面附上部分thread的源码

删除了赋值函数和拷贝构造函数,但是可以把左值转换为右值就可以赋值成功。
3)thread(const thread& ) = delete;
删除拷贝构造函数,不允许线程对象之间的拷贝。
4)thread(thread&& other ) noexcept;
移动构造函数,将线程other的资源所有权转移给新创建的线程对象。
赋值函数
赋值函数:
thread& operator= (thread&& other) noexcept;
thread& operator= (const other&) = delete;
线程中的资源不能被复制,如果other是右值,会进行资源所有权的转移,如果other是左值,禁止拷贝。
但是这里注意,因为是把医院所有权进行了转移,所以相当于原来的那个对象线程已经丢了,所以不能进行join()操作。
下面给出示例

注意
先创建的子线程不一定跑得最快(程序运行的速度有很大的偶然性)。
线程的任务函数返回后,子线程将终止。
如果主程序(主线程)退出(不论是正常退出还是意外终止),全部的子线程将强行被终止。
线程资源的回收
虽然同一个进程的多个线程共享进程的栈空间,但是,每个子线程在这个栈中拥有自己私有的栈空间。所以,线程结束时需要回收资源。
回收子线程的资源有两种方法:
在这先附上一段源码,在这我们目前只需要简单的能看懂就行,不必深究
joinable()判断子线程的分离状态,返回值为布尔类型,

1)在主程序中,调用join()成员函数等待子线程退出,回收它的资源。如果子线程已退出,join()函数立即返回,否则会阻塞等待,直到子线程退出。
2)在主程序中,调用detach()成员函数分离子线程,子线程退出时,系统将自动回收资源。分离后的子线程不可join()。
用joinable()成员函数可以判断子线程的分离状态,函数返回布尔类型。
注意
已经detach()的对象不能在最后调用join(),否则运行会报错。
this_thread的全局函数

get_id():该函数用于获取线程ID,thread类也有同名的成员函数;
yield():该函数让线程主动让出自己已经抢到的CPU时间片;
sleep_for(参数):该函数让线程休眠一段时间。参数:windows下以毫秒为单位,linux下以秒为单位
sleep_until():该函数让线程休眠至指定时间点。(可实现定时任务)
其他函数
void swap(std::thread& other); // 交换两个线程对象。
static unsigned hardware_concurrency() noexcept; // 返回硬件线程上下文的数量。
示例:
void func()
{
cout << "我是子线程:" << this_thread::get_id() << endl;
}
int main()
{
thread thread_1(func);
thread thread_2(func);
cout << "子线程A的id=" << thread_1.get_id() << endl;
cout << "子线程B的id=" << thread_2.get_id() << endl;
thread_1.swap(thread_2);
cout << "子线程A的id=" << thread_1.get_id() << endl;
cout << "子线程B的id=" << thread_2.get_id() << endl;
thread_1.detach();
thread thread_3(move(thread_1)); //正确,将thread左值转换为右值
thread_3.join();
return 0;
}

call_once函数
在多线程环境中,某些函数只能被调用一次,例如:初始化某个对象,而这个对象只能被初始化一次。
在线程的任务函数中,可以用std::call_once()来保证某个函数只被调用一次
头文件:#include <mutex>
template< class callable, class... Args >
void call_once( std::once_flag& flag, Function&& fx, Args&&... args );
第一个参数是std::once_flag,用于标记函数fx是否已经被执行过。
第二个参数是需要执行的函数fx。
后面的可变参数是传递给函数fx的参数。
下面给出示例
#include <iostream>
#include <thread>
#include <mutex> // std::once_flag和std::call_once()函数需要包含这个头文件。
using namespace std;
//once_flag onceflag; // once_flag全局变量。本质是取值为0和1的锁。
// 在线程中,打算只调用一次的函数。
void once_func(const int bh, const string& str) {
cout << "once_func() bh= " << bh << ", str=" << str << endl;
}
// 普通函数。
void func(int bh) {
once_func(1, "我只能被调用一次哦");
//call_once(onceflag, once_func, 0, "我只能调用一次哦");
for (int ii = 1; ii <= 3; ii++)
{
cout << "第" << ii << "秒" << endl;
this_thread::sleep_for(chrono::seconds(1)); // 休眠1秒。
}
}
int main()
{
// 用普通函数创建线程。
thread t1(func, 3);
thread t2(func, 8);
t1.join(); // 回收线程t1的资源。
t2.join(); // 回收线程t2的资源。
}
运行结果我们发现once_func()被调用了两次,每次创建线程都会调用,那如果我们只想让他被调用一次,怎么办?我们可以用函数call_once();

代码更改为下面
#include <iostream>
#include <thread>
#include <mutex> // std::once_flag和std::call_once()函数需要包含这个头文件。
using namespace std;
once_flag onceflag; // once_flag全局变量。本质是取值为0和1的锁。
// 在线程中,打算只调用一次的函数。
void once_func(const int bh, const string& str) {
cout << "once_func() bh= " << bh << ", str=" << str << endl;
}
// 普通函数。
void func(int bh) {
//once_func(1, "我只能被调用一次哦");
call_once(onceflag, once_func, 0, "我只能调用一次哦");
for (int ii = 1; ii <= 3; ii++)
{
cout << "第" << ii << "秒" << endl;
this_thread::sleep_for(chrono::seconds(1)); // 休眠1秒。
}
}
int main()
{
// 用普通函数创建线程。
thread t1(func, 3);
thread t2(func, 8);
t1.join(); // 回收线程t1的资源。
t2.join(); // 回收线程t2的资源。
}
这是发现只被调用了一次。
到这里创建线程的操作基本就完了,但是有没有发现我们测试debug时打印的内容很乱,这是为什么呢??
我们先
谈一下线程安全问题
线程安全
#include <iostream>
#include <thread> // 线程类头文件。
using namespace std;
int aa = 0;
void func()
{
//把a加十万次
for (int i = 1; i <= 100000; i++)
aa++;
}
int main()
{
thread t1(func);
thread t2(func);
t1.join();
t2.join();
cout << "aa=" << aa << endl; // 显示全局变量aa的值。
}


我们发现结果都不一样,这是为什么呢??
引入一点线程安全的知识
同一个进程中的多个线程共享该进程中的全部系统资源
多个线程访问同一共享资源的时候会产生冲突
顺序性、可见性、原子性
原子性
原子性的操作是不可被中断的一个或一系列操作。个人理解和MySQL中的事务那块的知识有点类似。
严格定义:严格的原子性的操作,其他线程获取操作的变量时,只能获取操作前的变量值和操作后的变量值,不能获取到操作过程中的中间值,在操作过程中其他操作需要获取变量值,需要进入阻塞状态等待操作结束。
顺序性
为了优化程序性能,编译器、处理器和运行时会对代码指令进行重排,重排过程中会遵循as-if-serial语义,即不影响单线程的运行结果。
举个例子,当编译器看到下面代码时会进行怎么处理
int a=10;
a++;
a-=10;
a=20;
此时代码会被优化为int a=20;
CPU为了提高程序整体执行效率,可能会对到吗顺序进行优化,虽然不保证完全按照代码顺序执行,但是可以保证结果和按代码顺序执行时一致。
可见性
线程操作共享变量时,会将该变量从内存加载到CPU缓存中,修改该变量后,CPU会立即更新缓存,但不一定会立即将它写回内存。这时候如果其它线程访问该变量,从内存中读到的是旧数据,而非第一个线程操作后的数据, 多个线程并发访问共享变量时,一个线程对共享变量的修改,其他线程立刻可以看到
所以上面的问题就可以知道出在哪里了,a++操作不是原子操作,两个线程可能执行时获得的时旧数据.
如何保证线程安全?
volatile关键字
作用
1. 保证内存变量的可见性
2. 进制代码优化(重排序)
但这里将a用volatile修饰是不解决问题的,因为他只解决了可见性问题,所以还需要进行线程同步
线程同步
加锁和解锁,确保同一时间只有一个线程访问共享资源
访问共享资源之前加锁,访问完成后释放锁
如果某线程持有锁,其它的线程形成等待队列
C++11提供了四种互斥锁(避免多个线程同时访问共享资源。避免数据竞争,并提供线程间的同步支持)

mutex类
mutex类的成员函数
1)加锁lock()
互斥锁有锁定和未锁定两种状态。
如果互斥锁是未锁定状态,调用lock()成员函数的线程会得到互斥锁的所有权,并将其上锁。
如果互斥锁是锁定状态,调用lock()成员函数的线程就会阻塞等待,直到互斥锁变成未锁定状态。
2)解锁unlock()
只有持有锁的线程才能解锁。
3)尝试加锁try_lock()
如果互斥锁是未锁定状态,则加锁成功,函数返回true。
如果互斥锁是锁定状态,则加锁失败,函数立即返回false。(线程不会阻塞等待)
这时我们只需要进行如下改动就可以输出正确结果
#include <iostream>
#include <thread> // 线程类头文件。
#include<mutex> //互斥锁头文件
using namespace std;
int aa = 0;
mutex mtx; //创建互斥锁
void func()
{
//把a加十万次
for (int i = 1; i <= 100000; i++)
{
mtx.lock(); //申请加锁
aa++;
mtx.unlock(); //解锁
}
}
int main()
{
thread t1(func);
thread t2(func);
t1.join();
t2.join();
cout << "aa=" << aa << endl; // 显示全局变量aa的值。
}
再举一个例子,现在计数,三个线程,统计1-100
#include<iostream>
#include<thread>
#include<mutex>
using namespace std;
volatile int Count = 0;
mutex mtx;
void Add(string pthread_name)
{
while (Count < 100)
{
mtx.lock();
if (Count < 100)
{
++Count;
cout << pthread_name << "执行了Add():" << Count << endl;
}
mtx.unlock();
this_thread::sleep_for(chrono::milliseconds(100));
}
}
int main()
{
thread t1(Add, "线程1");
thread t2(Add, "线程2");
thread t3(Add, "线程3");
t1.join();
t2.join();
t3.join();
return 0;
}

timed_mutex类
成员函数
1)加锁lock()
2)解锁unlock()
3)尝试加锁try_lock()
4) bool try_lock_for(时间长度);
5) bool try_lock_until(时间点);
recursive_mutex类
递归互斥锁允许同一线程多次获得互斥锁,可以解决同一线程多次加锁造成的死锁问题。
死锁问题
死锁是指两个或多个事务在同一资源上相互占用,并请求锁定对方的资源,从而导致恶性循环的现象。
当多个进程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些进程都将无法向前推进,这种情况就是死锁, 如果没有外力的作用,那么死锁涉及到的各个进程都将永远处于封锁状态。
看案例
#include<iostream>
#include<mutex>
using namespace std;
class Object
{
mutex m_mutex;
public:
void func1()
{
m_mutex.lock();
cout << "func1()" << endl;
m_mutex.unlock();
}
void func2()
{
m_mutex.lock();
cout << "func2()" << endl;
func1();
m_mutex.unlock();
}
};
int main()
{
Object obj;
//obj.func1(); //没问题
obj.func2(); //出现错误,
return 0;
}
这里出现错误,原因是在func2中调用了func1,此时func2持有锁,而func1申请加锁,但是func1肯定申请不到,所以出现了死锁,这里只需要将类中的成员变量属性改为递归锁:recursive_mutex就行了.
lock_guard类
lock_guard是模板类,可以简化互斥锁的使用,也更安全。
lock_guard的定义如下:
template<class Mutex>
class lock_guard
{
explicit lock_guard(Mutex& mtx); //这里的Mutex是上面四种锁的一个
}
lock_guard在构造函数中加锁,在析构函数中解锁。
lock_guard采用了 RAII思想(在类构造函数中分配资源,在析构函数中释放资源,保证资源在离开作用域时自动释放)。
RALL思想是什么?
RAII全称为(Resource Acquisition Is Initialization),即“资源获取就是初始化”,是C++语言的一种管理资源、避免泄漏的惯用法。
C++标准保证任何情况下,已构造的对象最终会销毁,即它的析构函数最终会被调用。利用这个特性就可以很好的管理创建好的资源。比如 锁、信号、指针这类成套操作的资源,即有创建有销毁。
RALL机制或者RALL思想,简单的说, 把需要创建的资源封装到类里面,在类的构造函数完成对资源的初始化,在类的析构函数中完成对资源的释放。
后续会更新一篇文章细说RALL思想.