C++ 多线程

 

本文介绍C++多线程相关内容。

1 多线程相关概念

  • 线程

    线程是进程中的一个实体,是被系统独立分配和调度的基本单位。也就是说线程是CPU可执行调度的最小单位。引入线程之后将进程的两个基本属性分开了,线程作为调度和分配的基本单位,进程作为独立分配资源的单位。线程基本上不拥有资源,只拥有一点运行中必不可少的资源,在同一个进程中的所有线程都共享地址空间,线程间的大部分数据可以共享并且相比多进程来说,多线程间的通信开销小、启动速度快、占用资源少。

  • 多线程并发

    多线程是实现并发(双核的真正并行或者单核机器的任务切换都叫并发)的一种手段,多线程并发即多个线程同时执行,一般而言,多线程并发就是把一个任务拆分为多个子任务,然后交由不同线程处理不同子任务,使得这多个子任务同时执行。

    为什么要进行多线程并发呢?主要是用于任务拆分和提高性能:将程序划分成不同的任务,每个线程执行一个或多个任务,可以将整个程序的逻辑变的更加清晰;任务之间各自并发降低运算时间。

    并发与并行有什么区别呢?首先串行很容易理解就是说所有任务都按先后顺序执行。而并行和并发的区别在于多个任务是否同时执行。并发是指在同一时间段内,能够处理多个事件,在并发程序中可以同时拥有两个或者多个线程。这意味着,如果程序在单核处理器上运行,那么这两个线程将交替地换入或者换出内存。这些线程是同时“存在”的——每个线程都处于执行过程中的某个状态。 并行是指在同一时刻上,能够处理多个事件。如果程序能够并行执行,那么就一定是运行在多核处理器上。此时程序中的每个线程都将分配到一个独立的处理器核上,因此可以同时运行。

  • 多线程同步与互斥

    多个线程同时执行任务肯定存在线程间的同步和互斥:

    • 线程同步:同步指维护任务片段的先后顺序,指线程之间所具有的一种制约关系,一个线程的执行依赖另外一个线程的消息,当它没有得到另一个线程的消息时应等待,直到消息到达时才被唤醒;

    • 线程互斥: 指对于共享的进程系统资源,每个线程访问时的排他性。互斥就是保证资源同一时刻只能被一个进程使用;互斥是为了保证数据的一致性。当有若干个线程都要使用某一个共享资源时,任何时刻最多只允许一个线程去使用,其他线程必须等待,知道占用占用资源者释放该资源。线程互斥可以看成是一种特殊的线程同步。

    线程间的同步方法大体可以分为两类:

    1. 用户模式(使用时不需要切换内核态,只在用户态完成操作):

      临界区:通过对多线程的串行化来访问公共资源或一段代码,适合一个进程内的多线程访问公共区域或代码段时使用

    2. 内核模式(利用系统内核对象的单一性来进行同步,使用时需要切换内核态与用户态):

      事件:用来通知线程有一些事件已发生,从而启动后继任务的开始。通过线程间触发事件实现同步互斥

      互斥量:互斥量跟临界区很相似,只有拥有互斥对象的线程才具有访问资源的权限,由于互斥对象只有一个,因此就决定了任何情况下此共享资源都不会同时被多个线程所访问。适合不同进程内多线程访问公共区域或代码段时使用,与临界区相似

      信号量:与临界区和互斥量不同,可以实现多个线程同时访问公共区域数据,原理与操作系统中PV操作类似,先设置一个访问公共区域的线程最大连接数,每有一个线程访问共享区资源数就减一,直到资源数小于等于零。

  • C++多线程并发

    简单情况下,实现C++多线程并发程序的思路如下:将任务的不同功能交由多个函数分别实现,创建多个线程,每个线程执行一个函数,一个任务就这样同时分由不同线程执行了。

2 线程创建

  • C++11中提供了thread库管理线程、保护共享数据、线程间同步等功能。头文件是#include<thread>。一个进程至少要有一个线程,在C++中可以认为main函数就是我们的主线程。而在创建thread对象的时候,就是在这个线程之外创建了一个独立的子线程。只要创建了这个子线程并且开始运行了( 线程创建即运行 ),主线程就完全和它没有关系了,不知道CPU会什么时候调度它运行,什么时候结束运行。

  • 线程的创建方式

    1 调用thread类去创建线程对象

      头文件与子线程处理函数如下,所有代码一致:
    
      #include <iostream>
      #include <thread>
    
      using namespace std;
    
      void print()
      {
          Sleep(3000);
          cout<<"====== start new thread ======"
      }
    

    1.1 使用join()函数加入,汇合线程、阻塞主线程,等待子线程结束,才会回到主线程,继续执行主线程的内容。

      int main()
      {
          thread t1(print);
          t1.join();  // 阻塞主进程,
          // 只有当t1子线程执行完毕后,才会继续执行主线程
          cout<<"====== main thread is running ======"<<endl;
      }
    

    运行结果如下:

      【程序启动3s之后】
      ====== start new thread ======
      ====== main thread is running ======
    

    如果创建一个线程而不做处理,会调用abort函数中止程序。一个线程也只能join一次,否则也会abort

    1.2 使用detach()函数,打破依赖关系,把子线程驻留后台

    detach用于主线程和当前线程分离,主线程可以先执行结束,如果主线程执行完了,子线程会在C++后台运行,一旦使用detach,与这个子线程关联的对象会失去对这个主线程的关联,此时这个子线程会驻留在C++后台运行,当主线程执行完毕结束,子线程会移交给C++运行时库管理,这个运行时库会清理与这个线程相关的资源(守护线程),detach会使子线程失去进程的控制;

      int main()
      {
          thread t1(print);
          t1.detach();  // 主线程子线程分离
          sleep(4);
          cout<<"====== main thread is running ======"<<endl;
      }
    

    运行结果如下:

      【程序启动3s之后】
      ====== start new thread ======
      【程序启动4s之后】
      ====== main thread is running ======
    

    1.3 使用joinable()函数判断当前线程是否可以做join或者detach操作,若可以,返回true。

      int main()
      {
          thread t1(print);
          t1.detach();  // 主线程子线程分离
          cout<<"====== main thread is running ======"<<endl;
          if( t1.joinable() )
              t1.join();
          else
              cout<<"thread t1 is running"<<endl;
      }
    

    输出如下:

      ====== main thread is running ======
      thread t1 is running
    

    2 通过类和对象创建线程

      class Test
      {
      public:
          // STL 仿函数
          void operator()()
          {
              sleep(3); // unistd.h
              cout << "====== start new thread ======" << endl;
          }
      };
    
      int main()
      {
          //正常写法:对象充当线程处理函数
          Test test;
          thread t1(test);
          t1.join(); 
            
          //Test();
          thread t2( (Test() )); //这里如果不多写一个括号,
          // 编译器就会把t2解析成一个函数,Test()解析成一个参数,从而出错
          t2.join();
      }
    

    3 带参的方式创建线程

      void print(int & num)
      {
          cout<<"thread get params value is: "<< num << endl;
      }
    
      int main()
      {
          int num = 10;
          //ref 用于包装 “引用传递值”
          // 此处必须要用 std::ref
          thread t1( print, ref(num) );
          t1.join();
          cout<<"main thread end"<<endl;
      }
    

    4 Lambda表达式创建线程

      int main()
      {
          thread t1( [](){cout<<"new thread has joint<<endl"<<endl;} );
          t1.join();
          cout<<"main thread end"<<endl;
      }
    

    5 以智能指针为参数创建线程

      void print(unique_ptr<int> ptr)
      {
          cout<<"thread get params value is: "<< *ptr << endl;
      }
    
      int main()
      {
          int * p = new int(10);
          unique_ptr<int> ptr(p);
          thread t1( print, move(ptr) ); //move移动语义,
          //通俗的说就是把ptr移动到子线程中了,
          //这样的话,主线程中的ptr就没了 
          t1.join();
          cout<<"main thread end"<<endl;
      }
    

    6 以类的成员函数充当线程处理函数来创建线程

      class Test
      {
      public:
          void print(int & num)
          {
              cout << "thread get params value is: " << num << endl;
          }
      };
    
      int main()
      {
          int num = 10;
          Test test;
          //需要说明 是哪个对象
          thread t1(&Test::print, test, ref(num));
          t1.join();
          cout << "main thread end" << endl;
      }
    

3 互斥锁的使用

  • mutex

    mutex是一个基本的互斥锁,其可以通过lock()进行加锁,unlock()进行解锁。示例如下;

      #include<iostream>
      #include<thread>
      #include<mutex>
    
      int a = 0;
      mutex _mutex;
      void foo()
      {
          for (int i = 0; i < 10000000; ++i)
          {
              _mutex.lock();
              a += 1;
              _mutex.unlock();
          }
      }
      int main()
      {
          thread t1(foo);
          thread t2(foo);
          t1.join();
          t2.join();
          return 0;
      }
    

    在C++中,通过构造std::mutex的实例创建互斥元,调用成员函数lock()来锁定它,调用unlock()来解锁,这能满足我们的需求。不过一般不推荐这种做法,lock()和unlock()是成对出现的,需要手动解锁,若在手动释放锁前程序异常,没有调用unlock(),这个资源会一直被锁着,没发释放,会导致其他异常。

  • lock_guard

    标准C++库提供了std::lock_guard类模板,实现了互斥元的RAII惯用语法。std::mutexstd::lock _ guard。都声明在< mutex >头文件中。

    注意:析构时自动释放,不能手动调用unlock(),否则析构时再次调用,会报异常。

    在作用域内创建lock_guard对象时,会尝试获得锁(mutex::lock()),没有获得就像其他锁一样阻塞在原地。在lock_guard的析构函数内会释放锁(mutex::unlock()),不需要我们手动释放。

    示例如下:

      #include <iostream>
      #include <thread>
      #include <mutex>
    
      using namespace std;
    
      mutex m_lock;
    
      void print(int && num)
      {
          lock_guard<mutex> lock(m_lock); // 创建lock_guard,并加锁
          cout<<"thread id is :"<<num<<endl;
          // 此处无需手动解锁,当超过此作用域,lock自动析构,进行解锁
      }
    
      int main()
      {
          thread t1(print, 1 );
          thread t2(print, 2);
          t1.join();
          t2.join();
          cout << "main thread end" << endl;
          return 0;
      }
    
  • unique_lock

    unique_lock是对mutex的一种RAII使用手法,unique_lock是lock_guard的加强版,它具有 lock_guard 的所有功能,同时具有如下特点:

    • 创建时可以不锁定(通过指定第二个参数为std::defer_lock)

    • 可以随时加锁解锁(通过lock,try_lock, unlock)

    • 允许延迟锁定(通过try_lock_for),限时锁定(通过 try_lock_until)

    • 不可复制,可移动(通过移动构造函数或移动赋值函数转移所有权)

    • 可以主动释放所有权(通过release)

    • 作用域规则同 lock_grard,析构时自动释放锁

    • 条件变量需要该类型的锁作为参数

    unique_lock也是一个类,如unique_lock<mutex> lck(m),就是创建了一个unique_lock对象lck,并将其与互斥量m绑定,同时对其上锁, 创建对象默认加锁。

    与lock_guard不同的是,unique_lock可以进行临时解锁和再上锁,如在构造对象之后使用lck.unlock()就可以进行解锁,lck.lock()进行上锁,而不必等到析构时自动解锁。unique_lock在析构时也会自动解锁。

    除此之外,unique_lock还接受第二个参数来进行构造。两个参数构造的形式有以下几种:

    • unique_lock lck(m,adopt_lock):用互斥量来初始化unique_lock对象,但是构造时不会自动lock();

    • unique_lock lck(m,defer_lock):仅仅是将lck与m绑定,不会自动进行lock()和unlock();

    • unique_lock lck(m,try_to_lock):将lck与m绑定,并且尝试对其进行加锁,如果加锁失败也不会阻塞,加锁是否成功可以根据lck.owns_lock()来判断是否加锁成功;

    使用示例:

      #include <iostream>
      #include <thread>
      #include <mutex>
    
      using namespace std;
    
      mutex m_lock;
    
      void print(int && num)
      {
          unique_lock<mutex> lock(m_lock); // 创建unique_lock,并加锁
          cout<<"thread id is :"<<num<<endl;
          // 此处无需手动解锁,当超过此作用域,lock自动析构,进行解锁
      }
    
      int main()
      {
          thread t1(print, 1 );
          thread t2(print, 2);
          t1.join();
          t2.join();
          cout << "main thread end" << endl;
          return 0;
      }
    

4 线程同步