C++标准模版库中线程的使用

news/2024/5/19 4:05:58 标签: c++, 线程, thread, mutex, 线程同步

文章目录


线程是程序开发中必须使用到的概念,但是也是相当难掌握的概念。因为在单线程的程序中,所有的逻辑都是线性发生的,出现问题定位的时候只需要一步一步调试就可以了。但是在多线程的环境中,各种莫名其妙的情况都会出现。
我这里记录下自己在开发过程中总结出来的一些线程的基本逻辑和碰到过的坑。
我的环境信息如下:

  • 语言是C++;
  • 使用的线程类是最基本的std标准库的thread类;
  • 操作系统为macOS。
  • 编译器为g++

线程的基本使用

最基础的使用方法

这里话不多说,直接上基本的demo代码:

void t1()
{
    int cnt = 1;
    printf("thread start\n", cnt);

    return;
}

int main()
{
    for(int i=0; i<20; i++){
        thread * th1 = new thread(t1);
        
        th1->join();
    }

    return 1;
}

上面就是通过一个thread的指针,new了一个thread类,这个类接受一个函数指针作为线程的运行对象。这样就初始化了并运行了一个线程

这个输出结果就是:

thread start
thread start
thread start
thread start
thread start
...

重点说一下的是join函数(后面还会提到)。我刚接触到这个函数的时候也有点不理解,为啥叫join,是加入一个什么东西。
后来才想到,实际上就是告诉操作系统的线程调用模块,这个线程加入到线程调用的队列中去,也就是进入线程调度系统去排队,详细的demo后面有介绍。

按照上面的代码,就是创建一个线程,就把这个线程排上队。那么这20个线程就是一个一个的来执行。

上面的代码是无法看出来是不是顺序执行的,那么我们改一点点东西,标记一下函数的执行顺序。

在创建线程时传参

void t1(int i)
{
    printf("thread start, thread number is %d\n", i);

    return;
}

int main()
{
    for(int i=0; i<20; i++){
        thread * th1 = new thread(t1, i);
        
        th1->join();
    }

    return 1;
}

这样输出就会变成:

thread start, thread number is 0
thread start, thread number is 1
thread start, thread number is 2
thread start, thread number is 3
thread start, thread number is 4
thread start, thread number is 5
...

这样就达到了上面的效果,看到了所有的线程,在join函数的作用下,一个一个排队执行。大家可以试着去掉join函数,那样,这个顺序就是乱的了,随机的,看操作系统的心情了。

但是,这里有一个小坑,使用mac系统的坑。mac系统中的g++使用的编译器是clang,而在mac中的clang默认不支持c++11的多线程,必须在编译时指定参数:
g++ muti_thread.cpp -std=c++11

否则会编译不通过(我这里全是手动编译,如果是工程化的话,需要配置到编译器配置中去)。

另外,在线程的传参中:

  • 如果是基本类型的的参数,默认时以值传递的方式进行。也就是两个变量之间没什么关系,子线程中是自己的资源,如果做引用传递,编译器会报错。
  • 如果参数是指针,指针本身是值传递,但是会出现指向同一地址的问题。
  • 如果参数是类,逻辑上也是值传递。
  • 多个参数的传递就是依次往后就行:
      void t1(int i, int j)
      
      thread * th1 = new thread(t1, i, j);
    

另外,线程和调用函数不是一个概念,不要通过线程的函数去返回什么值,那个做不到的。

再看看join

join在很多地方叫做联结。我理解除了排队这个联结就是说新建的这个线程和创建他的这个线程有没有关系,如果调用了join函数,那么主线程(或者说创建线程线程)就会等待子线程(被创建的线程)结束后再执行,如果没有,那么主线程创建子线程后就会自己继续执行。
我们在上面的代码里再修改一下看看,在join后增加了一个输出:

void t1(int i)
{
    printf("thread start, thread number is %d\n", i);

    return;
}

int main()
{
    for(int i=0; i<20; i++){
        thread * th1 = new thread(t1, i);
        
        th1->join();
        std::cout << "continue" << std::endl;
    }

    return 1;
}

输出结果就变成了:

thread start, thread number is 0
continue
thread start, thread number is 1
continue
thread start, thread number is 2
continue
thread start, thread number is 3
continue
thread start, thread number is 4
continue
thread start, thread number is 5
continue

如果没有join这个函数的话:

int main()
{
    for(int i=0; i<20; i++){
        thread * th1 = new thread(t1, i);

        std::cout << "continue" << std::endl;
    }

    return 1;
}

输出就是比较随机的(就看操作系统怎么调度了,而且这个0-1-2-3-4-5的顺序也不一定能保证):

continue
thread start, thread number is 0
continue
thread start, thread number is 1
continue
thread start, thread number is 2
thread start, thread number is 3
continue
continue
thread start, thread number is 4
continue
thread start, thread number is 5
continue

相对应的,还有一个解除链接的函数:detach,解除主线程与子线程之间的这个等待关系。
因为上面提到的join的逻辑,只要在主线程里调用了join函数,主线程相当于就别挂起了,所以直接join后调用detach是没用的:

int main()
{
    for(int i=0; i<20; i++){
        thread * th1 = new thread(t1, i);

        th1->join();
        std::cout << "continue" << std::endl;
        th1->detach();
    }

    return 1;
}

这样会出现段错误,因为在主线程执行到detach函数的时候,实际上子线程已经退出了。
实际上,我理解线程被new出来之后,本身与主线程之间就是独立的,只有调用了join之后,detach才会有意义,不要被阻塞的太长了。

线程线程之间的同步

上面的内容是比较基础的,如果每个线程都是处理相对独立的任务,互不相关,那么还是比较简单的。但很多情况下是多线程会访问到同样的内容,这样,多线程的同步就是至关重要的问题了,没有协调好的话,就是会出现很多意想不到的情况。

设置这样一个场景:
有一个统一的变量,在上面的20个线程中,每个线程都给这个变量进行赋值,赋值为自己的线程编号,然后输出,在输出的过程中会处理一点别的事情,这是需要花一点时间的,所以我们用一个sleep语句来模拟这个线程所费的时间,demo如下:

void t1(int i)
{
    // m.lock();
    cnt = i;

    int a = rand();
    // 休眠随机的时间
    std::this_thread::sleep_for(std::chrono::milliseconds(a%100));
    // m.unlock();
    printf("thread start, thread number is %d, count is: %d\n", i, cnt);
    
    return;
}

int main()
{
    for(int i=0; i<20; i++){
        thread * th1 = new thread(t1, i);

        std::this_thread::sleep_for(std::chrono::milliseconds(10));
    }

    std::cout<<"main quit, count is: " << cnt << endl;

    return 1;
}

输出却是,很明显是乱的:

thread start, thread number is 0, count is: 0
thread start, thread number is 1, count is: 5
thread start, thread number is 4, count is: 6
thread start, thread number is 2, count is: 7
thread start, thread number is 3, count is: 8
thread start, thread number is 6, count is: 9
thread start, thread number is 9, count is: 9
thread start, thread number is 8, count is: 10
thread start, thread number is 5, count is: 11
thread start, thread number is 10, count is: 13
thread start, thread number is 7, count is: 13
thread start, thread number is 15, count is: 15
thread start, thread number is 13, count is: 16
thread start, thread number is 11, count is: 16
thread start, thread number is 16, count is: 18
thread start, thread number is 17, count is: 19
thread start, thread number is 12, count is: 19
main quit, count is: 19

这里发现整个乱的,因为每个线程都是独立运行的,很有可能在你的线程里去做别的事情的时候,被另外的线程把这个值改掉了。
还有就是发现输出都没有19个,这是因为主线程已经结束了,所以后面的线程都没等到输出就已经整个进程退出了。

根据上面讲到的内容,很容易想到,加上join就可以了,确实,加上join后的输出就是:

thread start, thread number is 0, count is: 0
thread start, thread number is 1, count is: 1
thread start, thread number is 2, count is: 2
thread start, thread number is 3, count is: 3
thread start, thread number is 4, count is: 4
thread start, thread number is 5, count is: 5
thread start, thread number is 6, count is: 6
thread start, thread number is 7, count is: 7
thread start, thread number is 8, count is: 8
thread start, thread number is 9, count is: 9
thread start, thread number is 10, count is: 10
thread start, thread number is 11, count is: 11
thread start, thread number is 12, count is: 12
thread start, thread number is 13, count is: 13
thread start, thread number is 14, count is: 14
thread start, thread number is 15, count is: 15
thread start, thread number is 16, count is: 16
thread start, thread number is 17, count is: 17
thread start, thread number is 18, count is: 18
thread start, thread number is 19, count is: 19
main quit, count is: 19

但是这里最关键的问题在于,每次主线程都要等待前一个线程结束之后再继续执行,也就是等待前一个线程结束之后再启动下一个线程,这就是一个串行的程序,就失去了多线程的意义了。

那么在这种情况下就只能使用线程同步的一个关键概念了:锁。
在std库中,就有一个基本的锁对象:mutex
使用
include <metux>就可以使用
demo代码:

int cnt = 0;
mutex m;

void t1(int i)
{
    m.lock();
    cnt = i;

    int a = rand();
    std::this_thread::sleep_for(std::chrono::milliseconds(a%100));
    m.unlock();
    printf("thread start, thread number is %d, count is: %d\n", i, cnt);
    
    return;
}

int main()
{
    for(int i=0; i<20; i++){
        thread * th1 = new thread(t1, i);
        
        std::this_thread::sleep_for(std::chrono::milliseconds(10));
        
    }

    getchar();
    std::cout<<"main quit, count is: " << cnt << endl;

    return 1;
}
  • getchar()是防止主线程退出导致后面的线程没有输出

  • m.lock和m.unlock中间的代码可以理解为原子执行(实际上不是)

  • 这个锁实际上理解为一把钥匙更好,就是说一个线程通过m.lock去获得这个关键区代码的运行权限(钥匙),如果钥匙在别人手里,lock函数就会阻塞这个线程直到获得这把钥匙。

  • m.unlock函数就是用完钥匙了,把钥匙还给管理员(操作系统),管理员就可以把这把钥匙给别人了。

  • 在使用和别人共享的资源的时候,最好是增加一把锁。(当然也未必,因为锁是需要等待,会降低程序的性能),比如下面的程序就可以不使用锁:

    void t1(int i)
      {
          // m.lock();
          cnt = i;
    
          int a = rand();
          std::this_thread::sleep_for(std::chrono::milliseconds(a%100));
          // m.unlock();
          printf("thread start, thread number is %d, count is: %d\n", i, cnt);
          
          return;
      }
    
      int main()
      {
          for(int i=0; i<20; i++){
              thread * th1 = new thread(t1, i);
              
              std::this_thread::sleep_for(std::chrono::milliseconds(200));
              
          }
    
          getchar();
          std::cout<<"main quit, count is: " << cnt << endl;
    
          return 1;
      }
    

    因为在这个程序里,每个线程的处理时间不会超过100毫秒,下一个线程创建的时间都在200毫秒了,肯定不会出现别的线程来捣乱的情况,所以不需要使用锁。
    这个例子不一定非常准确,只是想说明,在c++程序里,性能为第一要务,如果从业务和逻辑上分析,没有必要加锁,就不要去加锁。加锁不好的话反而会发生死锁这样的麻烦事。

这一篇先说这么多,后面还想从操作系统的角度来说一说更底层的线程原理,还有就是结合内存使用一起,说说线程安全和可重入的问题。


http://www.niftyadmin.cn/n/30433.html

相关文章

硬盘损坏数据恢复怎么操作?恢复数据的常用方法

硬盘一般固定在电脑里面的存储装置&#xff0c;里面保存着我们大量的数据。随着电脑的使用越加广泛&#xff0c;有时不免出现一些问题&#xff0c;比如硬盘在使用过程中出现数据错误&#xff0c;或者是硬盘的内部零件出现故障。出现这些问题&#xff0c;硬盘损坏数据恢复怎么操…

3轴数字罗盘IC HMC5883L介绍

3轴数字罗盘IC HMC5883L简介霍尼韦尔 HMC5883L 是一种表面贴装的高集成模块&#xff0c;并带有数字接口的弱磁传感器芯片&#xff0c;应用于低成本罗盘和磁场检测领域。HMC5883L 包括最先进的高分辨率HMC118X 系列磁阻传感器&#xff0c;并附带霍尼韦尔专利的集成电路包括放大器…

模拟实现list / list迭代器

前言&#xff1a;学习C的STL&#xff0c;我们不仅仅要求自己能够熟练地使用各种接口&#xff0c;我们还必须要求自己了解一下其底层的实现方法&#xff0c;这样可以帮助我们写出比较高效的代码程序&#xff01; ⭐在本篇文章中&#xff0c;list的迭代器是重点&#xff0c;它不…

植物大战 vector——C++

剖析STL库中的vector源代码&#xff0c;对其模仿进行简单实现。 猛戳订阅&#x1f341;&#x1f341; &#x1f449; [C详解专栏] &#x1f448; &#x1f341;&#x1f341; 这里是目录标题vector的使用vector的遍历reserve增容vector模拟实现匿名对象析构函数迭代器区间构造拷…

JAVA练习33

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 目录 前言 一、题目-有效的字母异位词 1.题目描述 2.思路与代码 2.1 思路 2.2 代码 总结 前言 提示&#xff1a;这里可以添加本文要记录的大概内容&#xff1a; 1月2…

Qt扫盲-QObject对象和线程

QObject对象和线程一、概述二、QObjectReentrant性三、每个线程事件的循环四、从其他线程访问QObject的子类五、跨线程的信号和槽函数一、概述 QThread继承QObject。QThread它发出信号来指示线程开始或结束执行&#xff0c;还提供了一些任务槽。 Qobject可以在多个线程中使用…

Python cv2不显示代码提示的解决

安装opencv-python 4.5.5.64版本推荐使用豆瓣源方法一&#xff1a;pip install 安装包名字 -i http://pypi.doubanio.com/simple/ --trusted-host pypi.doubanio.com //豆瓣镜像网站方法二&#xff1a;pip install 安装包名字 -i http://pypi.douban.com/simple/ --trusted-hos…

【Linux】进程间通信——管道

文章目录进程间通信1.1进程间通信介绍1.2进程间通信目的1.3进程间通信分类管道2.1管道介绍2.2匿名管道pipe读写特征管道特征2.3命名管道mkfifo创建管道文件删除管道文件通信总结进程间通信 1.1进程间通信介绍 什么是进程间通信&#xff1f; 答&#xff1a;进程具有独立性&…