C++11 多线程thread

C++11通过标准库引入了对多线程的支持 。

 #include <thread>头文件,使用thread类

一.创建线程及一些线程相关函数的使用

1.创建线程:使用std::thread 类创建线程。构造thread类对象时,传入参数,构造完成后,新的线程直接被创建,同时执行该线程。在该对象销毁前,必须指定detach()或join()

例:

#include <qDebug>

#include <thread>

void Thread1()

{

  qDebug()<< "Thread1 created";

}

void Thread2(int a, int &b)

{

  qDebug()<< "thread2 created";

}

int main(int argc, char *argv[])

{

  std::thread threadA(Thread1); //线程1创建,并执行。Thread1为将要在线程中执行的函数(不带参数)

  int a = 1;

  int b = 2;

  std::thread threadB(Thread2, a, std::ref(b));//线程2创建并执行。Thread2为函数,a为Thread2的第一个参数,b为Thread2的第二个参数。Thread2第二个参数为引用,这里需要用std::ref(b)的方式才是传引用

  return 1;

}

2.创建一个thread对象,在线程结束后对资源的回收:thread库有提供两个方法:1.thread::join   2.thread::detach。必须在thread对象销毁前调用

join: 以阻塞的方式开启新线程。调用线程在新线程结束返回后才继续向下执行(只会阻塞调用线程,不会影响到其它线程)。新线程结束后join()会清理相关资源,然后返回, 此时join()清理了新线程的资源,thread对象与已经被销毁的线程没关联,调用thread::joinable返回false

例:

void Thread1(int* num, int time)

{

  for (int i = 0; i < time; i++)

  {

    ++ *num;

  }

}

int main(int argc, char *argv[])

{

  int num = 0;

  int time = 1000;

  std::thread mThread1(Thread1, &num, time);  //线程1被创建并执行

  std::thread mThread2(Thread1, &num, time);  //线程2被创建并执行

  mThread1.join(); //阻塞主线程,直到mThread1执行完后跑mThread2.join()。同时mThread2线程也在运行

  mThread2.join();

  qDebug()<< "num= "<< num;    //num的结果在1000-2000之间 但是实际跑出来的结果为什么就是2000???

  num = 0;

  std::thread mThread3(Thread1, &num, time);  //线程3被创建并执行

  mThread3.join(); //阻塞主线程,直到mThread3执行完后跑下一句

  std::thread mThread4(Thread1, &num, time);  //线程4被创建并执行

  mThread4.join();

  qDebug()<< "num= "<< num;    //num的结果为2000

}

detach: 非阻塞方式开启新线程。新线程与调用线程分离,且调用线程不能与新线程交互,新线程会在后台运行。线程的所有权,控制权和线程结束后其相关资源的回收都由C++运行库保证。调用thread::joinable返回false

3.std::thread没有拷贝构造函数和拷贝赋值操作符,因此不支持复制操作(但是可以move),也就是说,没有两个 std::thread对象会表示同一执行线程;

4.以下几种情况下,std::thread对象是不关联任何线程的(对这种对象调用join或detach接口会抛异常):

默认构造的thread对象;

被移动后的thread对象;

detach 或 join 后的thread对象;

5.使用std::thread的默认构造函数构造的对象不关联任何线程;判断一个thread对象是否关联任何线程,使用std::thread::joinable接口,如果返回true,表示此thread对象有关联到某个线程(即使该线程已经执行结束)

二.多线程数据共享安全问题

多线程最主要的问题就是共享数据带来的问题。如果数据都是只读,多个线程同时访问不会有问题,但是,一个变量有多个线程会同时去修改,就会出现问题。

对于这类问题,C++11标准提供互斥类解决。互斥类定义在mutex头文件中  #include <mutex>。通过对临界区域进行加锁提供对共享数据的保护。mutex.h中提供了4个互斥类:

(1)、std::mutex:该类表示普通的互斥锁, 不能递归使用。

(2)、std::timed_mutex:该类表示定时互斥锁,不能递归使用。std::time_mutex比std::mutex多了两个成员函数:

A、try_lock_for():函数参数表示一个时间范围,在这一段时间范围之内线程如果没有获得锁则保持阻塞;如果在此期间其他线程释放了锁,则该线程可获得该互斥锁;如果超时(指定时间范围内没有获得锁),则函数调用返回false。

B、try_lock_until():函数参数表示一个时刻,在这一时刻之前线程如果没有获得锁则保持阻塞;如果在此时刻前其他线程释放了锁,则该线程可获得该互斥锁;如果超过指定时刻没有获得锁,则函数调用返回false。
(3)、std::recursive_mutex:该类表示递归互斥锁。递归互斥锁可以被同一个线程多次加锁,以获得对互斥锁对象的多层所有权。例如,同一个线程多个函数访问临界区时都可以各自加锁,执行后各自解锁。std::recursive_mutex释放互斥量时需要调用与该锁层次深度相同次数的unlock(),即lock()次数和unlock()次数相同。可见,线程申请递归互斥锁时,如果该递归互斥锁已经被当前调用线程锁住,则不会产生死锁。此外,std::recursive_mutex的功能与 std::mutex大致相同。

(4)、std::recursive_timed_mutex:带定时的递归互斥锁。

互斥类的最重要成员函数是lock()和unlock()。在进入临界区时,执行lock()加锁操作,如果这时已经被其它线程锁住,则当前线程在此排队等待。退出临界区时,执行unlock()解锁操作。

   (1)、构造函数:互斥类不支持copy和move操作,最初的互斥对象是处于unlocked状态。

        (2)、lock函数:互斥锁被锁定。线程申请该互斥锁,如果未能获得该互斥锁,则调用线程将阻塞(block)在该互斥锁上;如果成功获得该互诉锁,该线程一直拥有该互斥锁直到调用unlock解锁;如果该互斥锁已经被当前调用线程锁住,则产生死锁(deadlock)。

        (3)、unlock函数:解锁,释放调用线程对该互斥锁的所有权。

        (4)、try_lock:尝试锁定互斥锁。如果互斥锁被其他线程占有,则当前调用线程也不会被阻塞,而是由该函数调用返回false;如果该互斥锁已经被当前调用线程锁住,则会产生死锁。其中std::mutex就是lock、unlock。std::lock_guard与std::mutex配合使用,把锁放

              到lock_guard中时,mutex自动上锁,lock_guard析构时,同时把mutex解锁。
         (5)、native_handle:返回当前句柄。

例:

#include <threa>

#include <mutex>

std::mutex mMutex

void Thread1(int* num, int time)

{

  for (int i = 0; i < time; i++)

  {

    mMutex.lock();

    ++ *num;

    mMutex.unlock();

  }

}

int main(int argc, char *argv[])

{

  int num = 0;

  int time = 1000;

  std::thread mThread1(Thread1, &num, time);  //线程1被创建并执行

  std::thread mThread2(Thread1, &num, time);  //线程2被创建并执行

  mThread1.join(); //阻塞主线程,直到mThread1执行完后跑mThread2.join()。同时mThread2线程也在运行

  mThread2.join();

  qDebug()<< "num= "<< num;    //num的结果是2000

}

mutex对++ *num的保护,同一时刻,只能有一个线程对num变量进行++操作,因此,这段程序的输出必然是20000。

ps:使用互斥锁一定要注意死锁的问题。

但是实际跑出来的结果为什么就是2000???问题的猜测

1.编译器对代码进行了优化,在循环中,变量num有可能直接放在寄存器中,不会每次++都从内存重新读取。循环结束后在将num写回内存。(但是后面验证使用了volatile关键字,结果也是2000)

2.num是一个int的基本变量,num++的操作是一个原子操作,所以不用加锁。https://www.zhihu.com/question/27026846    //更有可能

在该对象销毁前,必须指定detach()或join():

if ( 1 )

{

  thread jjj = thread(Func, param );

  jjj.detach();

}

如果在出if前没有指定jjj.detach(), 就会crash。

对选用锁的类型的选择:

开发过程中,对于多线程的情况下,单个基础数据类型的数据共享安全,尽量使用原子操作代替锁机制. 当需要对代码块进行数据安全保护的时候,就需要选择使用锁机制或者自旋锁了。

参考:https://www.cnblogs.com/god-of-death/p/7843191.html

 
原文地址:https://www.cnblogs.com/linxisuo/p/13300930.html