多线程是实现并发(双核的真正并行或者单核机器的任务切换都叫并发)的一种手段,多线程并发即多个线程同时执行,一般而言,多线程并发就是把一个任务拆分为多个子任务,然后交由不同线程处理不同子任务,使得这多个子任务同时执行。
标准库提供了std::thread类来创建和管理线程,std::future类模板来获取异步操作的结果。

1 创建线程

首先要引入头文件#include<thread>,C++11中管理线程的函数和类在该头文件中声明,其中包括std::thread类。

使用C++线程库启动线程,可以归结为构造std::thread对象。

  • 语句thread th1(proc1);创建了一个名为th1的线程,并且线程th1开始执行函数proc1。
  • 语句th1 = thread(proc1)功能同上。
    下面是创建线程数组的示例:

实例化thread类对象时,至少需要传递函数名作为参数。如果函数为有参函数,如void proc2(int a,int b),那么实例化thread类对象时,则需要传递更多参数,参数顺序依次为函数名、该函数的第一个参数、该函数的第二个参数,···,如thread th2(proc2,a,b);。这里的传参,后续章节还会有详解与提升。

只要创建了线程对象(前提是,实例化std::thread对象时传递了“函数名/可调用对象”作为参数),线程就开始执行。

2 线程阻塞

线程启动后,一定要在和线程相关联的std::thread对象销毁前,对线程运用join()或者detach()方法。

join()与detach()都是std::thread类的成员函数,是两种线程阻塞方法,两者的区别是是否等待子线程执行结束。

2.1 join

所谓join,是等待调用线程运行结束后当前线程再继续运行,例如,主函数中有一条语句th1.join(),那么执行到这里,主函数阻塞,直到线程th1运行结束,主函数再继续运行。

TarArchiveOutputStream多线程 多线程 strdup_互斥量

2.2 detach

称为分离线程函数,使用detach()函数会让线程在后台运行,即说明主线程不会等待子线程运行结束才结束
通常称分离线程为守护线程(daemon threads),UNIX中守护线程是指,没有任何显式的用户接口,并在后台运行的线程。这种线程的特点就是长时间运行;线程的生命周期可能会从某一个应用起始到结束,可能会在后台监视文件系统,还有可能对缓存进行清理,亦或对数据结构进行优化。

TarArchiveOutputStream多线程 多线程 strdup_实例化_02

3 互斥量

3.1 基本用法:lock+unlock

互斥量是为了解决数据共享过程中可能存在的访问冲突的问题。首先需要#include<mutex>;,然后需要实例化mutex对象;最后需要在进入临界区之前对互斥量加锁,退出临界区时对互斥量解锁;至此,互斥量走完了它的一生。

#include<iostream>
#include<thread>
#include<mutex>
using namespace std;
mutex m;//实例化m对象,不要理解为定义变量
void proc1(int a)
{
    m.lock();
    cout << "proc1函数正在改写a" << endl;
    cout << "原始a为" << a << endl;
    cout << "现在a为" << a + 2 << endl;
    m.unlock();
}

void proc2(int a)
{
    m.lock();
    cout << "proc2函数正在改写a" << endl;
    cout << "原始a为" << a << endl;
    cout << "现在a为" << a + 1 << endl;
    m.unlock();
}
int main()
{
    int a = 0;
    thread p1(proc1, a);
    thread p2(proc2, a);
    p1.join();
    p2.join();
    return 0;
}

不推荐实直接去调用成员函数lock(),因为如果忘记unlock(),将导致锁无法释放,使用lock_guard或者unique_lock则能避免忘记解锁带来的问题。

3.2 lock_guard类

std::lock_guard()会在其构造函数中进行加锁,在其析构函数中进行解锁。最终的结果就是:创建即加锁,作用域结束自动解锁,从而使用lock_guard()就可以替代lock()与unlock()。
通过设定作用域,使得lock_guard在合适的地方被析构(在互斥量锁定到互斥量解锁之间的代码叫做临界区(需要互斥访问共享资源的那段代码称为临界区),临界区范围应该尽可能的小,即lock互斥量后应该尽早unlock),通过使用{}来调整作用域范围,可使得互斥量m在合适的地方被解锁。
下面是例子:

#include<iostream>
#include<thread>
#include<mutex>
using namespace std;
mutex m;//实例化m对象,不要理解为定义变量
void proc1(int a)
{
    lock_guard<mutex> g1(m);//用此语句替换了m.lock();lock_guard传入一个参数时,该参数为互斥量,此时调用了lock_guard的构造函数,申请锁定m
    cout << "proc1函数正在改写a" << endl;
    cout << "原始a为" << a << endl;
    cout << "现在a为" << a + 2 << endl;
}//此时不需要写m.unlock(),g1出了作用域被释放,自动调用析构函数,于是m被解锁

void proc2(int a)
{
    {
        lock_guard<mutex> g2(m);
        cout << "proc2函数正在改写a" << endl;
        cout << "原始a为" << a << endl;
        cout << "现在a为" << a + 1 << endl;
    }//通过使用{}来调整作用域范围,可使得m在合适的地方被解锁
    cout << "作用域外的内容3" << endl;
    cout << "作用域外的内容4" << endl;
    cout << "作用域外的内容5" << endl;
}
int main()
{
    int a = 0;
    thread proc1(proc1, a);
    thread proc2(proc2, a);
    proc1.join();
    proc2.join();
    return 0;
}

lock_gurad也可以传入两个参数,第一个参数为adopt_lock标识时,表示构造函数中不再进行互斥量锁定,因此此时需要提前手动锁定。

3.3 unique_lock类

std::unique_lock类似于lock_guard,只是std::unique_lock用法更加丰富,同时支持std::lock_guard()的原有功能。 使用std::lock_guard后不能手动lock()与手动unlock();使用std::unique_lock后可以手动lock()与手动unlock(); std::unique_lock的第二个参数,除了可以是adopt_lock,还可以是try_to_lock与defer_lock;

try_to_lock: 尝试去锁定,得保证锁处于unlock的状态,然后尝试现在能不能获得锁;尝试用mutx的lock()去锁定这个mutex,但如果没有锁定成功,会立即返回,不会阻塞在那里,并继续往下执行;

defer_lock: 始化了一个没有加锁的mutex;

#include<iostream>
#include<thread>
#include<mutex>
using namespace std;
mutex m;
void proc1(int a)
{
    unique_lock<mutex> g1(m, defer_lock);//始化了一个没有加锁的mutex
    cout << "xxxxxxxx" << endl;
    g1.lock();//手动加锁,注意,不是m.lock()! 注意,不是m.lock()! m已经被g1接管了;
    cout << "proc1函数正在改写a" << endl;
    cout << "原始a为" << a << endl;
    cout << "现在a为" << a + 2 << endl;
    g1.unlock();//临时解锁
    cout << "xxxxx"  << endl;
    g1.lock();
    cout << "xxxxxx" << endl;
}//自动解锁

void proc2(int a)
{
    unique_lock<mutex> g2(m,try_to_lock);//尝试加锁一次,但如果没有锁定成功,会立即返回,不会阻塞在那里,且不会再次尝试锁操作。
    if(g2.owns_lock){//锁成功
        cout << "proc2函数正在改写a" << endl;
        cout << "原始a为" << a << endl;
        cout << "现在a为" << a + 1 << endl;
    }else{//锁失败则执行这段语句
        cout <<""<<endl;
    }
}//自动解锁

int main()
{
    int a = 0;
    thread proc1(proc1, a);
    thread proc2(proc2, a);
    proc1.join();
    proc2.join();
    return 0;
}

mutex对象的所有权不需要手动转移给unique_lock , unique_lock对象实例化后会直接接管mutex。

mutex m;
{  
    unique_lock<mutex> g2(m,defer_lock);
    unique_lock<mutex> g3(move(g2));//所有权转移,此时由g3来管理互斥量m
    g3.lock();
    g3.unlock();
    g3.lock();
}

4 异步线程future

主线程从调用get方法开始进行堵塞。需要#include<future> async是一个函数模板,用来启动一个异步任务,它返回一个future类模板对象,future对象起到了占位的作用。
位是什么意思?就是说该变量现在无值,但将来会有值。刚实例化的future是没有储存值的,但在调用future对象的get()成员函数时,主线程会被阻塞直到异步线程执行结束,并把返回结果传递给future
相当于你去办政府办业务(主线程),把资料交给了前台,前台安排了人员去给你办理(std::async创建子线程),前台给了你一个单据(std::future对象),说你的业务正在给你办(子线程正在运行),等段时间你再过来凭这个单据取结果。过了段时间,你去前台取结果(调用get()),但是结果还没出来(子线程还没return),你就在前台等着(阻塞),直到你拿到结果(子线程return),你才离开(不再阻塞)。

#include <iostream>
#include <future>
#include <unistd.h>
using namespace std;
double t1(const double a, const double b)
{
 double c = a + b;
 sleep(3);//假设t1函数是个复杂的计算过程,需要消耗3秒
 return c;
}

int main() 
{
 double a = 2.3;
 double b = 6.7;
 future<double> fu = async(t1, a, b);//创建异步线程线程,并将线程的执行结果用fu占位;
 cout << "正在进行计算" << endl;
 cout << "计算结果马上就准备好,请您耐心等待" << endl;
 cout << "计算结果:" << fu.get() << endl;//阻塞主线程,直至异步线程return
        //cout << "计算结果:" << fu.get() << endl;//取消该语句注释后运行会报错,因为future对象的get()方法只能调用一次。
 return 0;
}