首页 > 编程语言 >C++多线程并发(一)--- 线程创建与管理

C++多线程并发(一)--- 线程创建与管理

时间:2024-01-13 22:35:13浏览次数:37  
标签:std thread C++ --- 并发 线程 多线程

目录

C++的多线程可以充分利用计算机资源,提高代码运行效率。在这里总结了一些多线程应用过程中的基本概念和用法。   

进程和线程的区别

  • 进程是一个应用程序被操作系统拉起来加载到内存之后从开始执行到执行结束的这样一个过程。简单来说,进程是程序(应用程序,可执行文件)的一次执行。比如双击打开一个桌面应用软件就是开启了一个进程。
  • 线程是进程中的一个实体,是被系统独立分配和调度的基本单位。也有说,线程是CPU可执行调度的最小单位。也就是说,进程本身并不能获取CPU时间,只有它的线程才可以。

引入线程之后,将传统进程的两个基本属性分开了,线程作为调度和分配的基本单位,进程作为独立分配资源的单位。
我对这句话的理解是:线程参与操作系统的调度,参与CPU的竞争,得到分配的时间片,获得处理机(CPU)运行。而进程负责获取操作系统分配的资源,如内存。

何为并发?

简单来说,并发指的是两个或多个独立的活动在同一时段内发生
与并发相近的另一个概念是并行。它们两者存在很大的差别,图示如下:

  • 并发:同一时间段内可以交替处理多个操作,强调同一时段内交替发生。

  • 并行:同一时刻内同时处理多个操作,强调同一时刻点同时发生。

在传统的单核CPU中,CPU通过极快的速度不停的切换不同应用程序的命令,而让我们看起来感觉计算机在同时执行很多个应用程序。比如,一边听歌,一边聊天,还能同时打游戏,我们误以为这是并发,其实只是一种伪并发的假象

在出现多核处理器以后,使得并发真正的实现。C++中采用多线程实现并发。

C++11线程基本操作

一个多线程C++程序是什么样子的?它看上去和其他所有C++程序一样,通常是变量、类以及函数的组合。唯一真正的区别在于某些函数可以并发运行,所以你需要确保共享数据的并发访问是安全的。当然,为了并发地运行函数,必须使用特定的函数以及对象来管理各个线程。

C++11新标准多线程支持库

C++标准并没有提供对多进程并发的原生支持,所以C++的多进程并发要靠其他API(比如用CreateThread进行创建)
C++11可以通过多线程实现并发,这是一种比较底层、传统的实现方式。C++11引入了5个头文件来支持多线程编程,是<atomic><thread><mutex><condition_variable><future>

  • < thread > : 提供线程创建及管理的函数或类接口;
  • < mutex > : C++11 互斥量Mutex。在多线程环境中,有多个线程竞争同一个公共资源,就很容易引发线程安全的问题
  • < condition_variable > : 允许一定量的线程等待(可以定时)被另一线程唤醒,然后再继续执行;
  • < future > : 提供了一些工具来获取异步任务(即在单独的线程中启动的函数)的返回值,并捕捉其所抛出的异常
  • < atomic >:为细粒度的原子操作(不能被处理器拆分处理的操作)提供组件,允许无锁并发编程。

std::thread类成员函数

  • get_id:获取线程ID,返回一个类型为std::thread::id的对象。
  • joinable:检查线程是否可被join。检查thread对象是否标识一个活动(active)的可行性线程。缺省构造的thread对象、已经完成jointhread对象、已经detachthread对象都不是joinable
  • join:通过join()函数关联并阻塞线程,等待该线程执行完毕后继续;
  • detach:通过detach()函数解除关联使线程可以与主线程并发执行,但若主线程执行完毕退出后,detach()接触关联的线程即便没有执行完毕,也将自动退出。
  • native_handle:该函数返回与std::thread具体实现相关的线程句柄。
  • swap:交换两个线程对象所代表的底层句柄。
  • operator:移动线程对象

有时候我们需要在线程执行代码里面对当前调用者线程进行操作,针对这种情况,C++11里面专门定义了一个命名空间this_thread,此命名空间也声明在<thread>头文件中,其中包括:

  • get_id()函数用来获取当前调用者线程的ID;
  • yield()函数可以用来将调用者线程跳出运行状态,重新交给操作系统进行调度,即当前线程放弃执行,操作系统调度另一线程继续执行;
  • sleep_until()函数是将线程休眠至某个指定的时刻(time point),该线程才被重新唤醒;
  • sleep_for()函数是将线程休眠某个指定的时间片(time span),该线程才被重新唤醒,不过由于线程调度等原因,实际休眠实际可能比sleep_duration所表示的时间片更长。

std::thread的关键总结

  1. C++ 11中创建线程非常简单,使用std::thread类就可以,thread类定义于thread头文件,构造thread对象时传入一个可调用对象作为参数(如果可调用对象有参数,把参数同时传入),这样构造完成后,新的线程马上被创建,同时执行该可调用对象
  2. std::thread默认的构造函数构造的对象不关联任何线程;判断一个thread对象是否关联某个线程,使用joinable()接口,如果返回true,表明该对象关联着某个线程(即使该线程已经执行结束);
  3. "joinable"的对象析构前,必须调用join()接口等待线程结束,或者调用detach()接口解除与线程的关联,否则会抛异常;
  4. 正在执行的线程从关联的对象detach后会自主执行直至结束,对应的对象变成不关联任何线程的对象,joinable()将返回false;
  5. std::thread没有拷贝构造函数和拷贝赋值操作符,因此不支持复制操作(但是可以move),也就是说,没有两个std::thread对象会表示同一执行线程;
  6. 容易知道,如下几种情况下,std::thread对象是不关联任何线程的(对这种对象调用joindetach接口会抛异常):
    • 默认构造的thread对象;
    • 被移动后的thread对象;
    • detachjoin 后的thread对象;

C++中多线程创建

  1. 简单使用
    #include <iostream>
    #include <thread>
    using namespace std;
    
    void f() {
        cout << "thread 1 is running" << endl;
        this_thread::sleep_for(chrono::seconds(1));
    }
    
    int main() {
        thread t1(f);   // 创建线程,一旦创建完毕,马上开始运行
        t1.join();
        return 0;
    }
    
    这里创建传入的函数f,实际上其构造函数需要的是可调用(callable)类型,只要是有函数调用类型的实例都是可以的。所有除了传递函数外,还可以使用:
  • lambda表达式
    for (int i = 0; i < 4; i++)
    {
        thread t([i]{
            cout << i << endl;
        });
        t.detach();
    }
    
  • 重载了()运算符的类的实例
    #include <iostream>
    #include <thread>
    using namespace std;
    class Task
    {
    public :
    void operator()(int i)  //()重载
        {
            cout << i << endl;
        }
    };
    
    int main()
    {
        for (int i = 0; i < 4; i++)
        {
            Task task;
            thread t(task, i);
            t.detach();
        }
    }
    
    当线程启动后,一定要在和线程相关联的thread销毁前,确定以何种方式等待线程执行结束。C++11有两种方式来等待线程结束
    1. detach方式,启动的线程自主在后台运行,当前的代码继续往下执行,不等待新线程结束。前面代码所使用的就是这种方式。因此执行的结果就多种多样了。
      这就涉及到多线程编程最核心的问题了资源竞争。CPU有4核,可以同时执行4个线程是没有问题的。但是控制台(资源)却只有一个,同时只能有一个线程拥有这个唯一的控制台,将数字输出。
    2. join方式,等待启动的线程完成,才会继续往下执行。假如前面的代码使用这种方式,其输出就会0,1,2,3,因为每次都是前一个线程输出完成了才会进行下一个循环,启动下一个新线程。
  1. 带函数参数的线程
    当需要向线程函数传递参数时,直接在创建线程时,同时也把参数作为入参传递给线程函数。
    注意当调用函数的参数为引用参数时,线程调用需要加上ref关键字表示引用。并且线程函数会改变引用的变量值。

    #include <iostream>
    #include <thread>
    using namespace std;
    void f1(int n)
    {
        n++;
        cout<<"n = "<< n <<endl;
    }
    
    void f2(int &n)//引用参数
    {
        n++;
        cout<<"n = "<<n<<endl;
    }
    
    int main()
    {
        int n = 0;
        
        thread t1(f1, n);
        t1.join();
        cout<<"n = "<<n<<endl;
        
        thread t2(f2, ref(n));
        t2.join();
        cout<<"n = "<<n<<endl;
    }
    
    /*
    n = 1
    n = 0
    n = 1
    n = 0
    */
    
  2. 转移线程的所有权
    thread是可移动的(movable)的,但不可复制(copyable)。可以通过move来改变线程的所有权,灵活的决定线程在什么时候join或者detach

    #include <iostream>
    #include <thread>
    using namespace std;
    
    void f2(int &n) {
        n++;
        cout << "n = " << n << endl;
    }
    
    int main() {
        int n = 0;
    
        thread t3(f2, ref(n));
        thread t4(move(t3));
        t4.join();  //此时t4正在运行f2(),t3不再是一个线程了。
        return 0;
    }
    /*
    n = 1
    */
    

    将线程从t3转移给t4,这时候t3就不再拥有线程的所有权,调用t3.joint3.detach会出现异常,要使用t4来管理线程。这也就意味着thread可以作为函数的返回类型,或者作为参数传递给函数,能够更为方便的管理线程。

  3. 线程暂停
    如果让线程从外部暂停会引发很多并发问题,这也是为什么std::thread没有直接提供pause函数的原因。
    如果线程在运行过程中,确实需要停顿,就可以用this_thread::sleep_for

    this_thread::sleep_for(chrono::seconds(3)); //此处线程停顿3秒。
    
  4. 获取当前线程号
    线程的标识类型为std::thread::id,有两种方式获得到线程的id。

    • 通过thread的实例调用get_id()直接获取
    • 在当前线程上调用this_thread::get_id()获取
    thread::id main_threadId =  this_thread::get_id();
    

小结:
本结主要介绍了C++11引入的标准多线程库的一些基本操作。有以下内容:

  • 线程的创建
  • 线程的执行方式,join或者detach
  • 向线程函数传递参数,需要注意的是线程默认是以拷贝的方式传递参数的,当期望传入一个引用时,要使用std::ref进行转换
  • 线程是movable的,可以在函数内部或者外部进行传递
  • 线程在运行过程中,如果需要停顿,可以用this_thread::sleep_for实现。
  • 每个线程都一个标识,可以调用get_id获取。

参考文章

标签:std,thread,C++,---,并发,线程,多线程
From: https://www.cnblogs.com/yubo-guan/p/17963125

相关文章

  • 无涯教程-LISP - 循环语句
    在某些情况下,您需要执行一段代码次数,循环语句使我们可以多次执行一个语句或一组语句,以下是大多数编程语言中循环语句的一般形式。LISP提供以下类型的构造来处理循环需求。单击以下链接以查看其详细信息。Sr.No.Construct&描述1loop以最简单的形式,它允许您重复执行某些语......
  • CF414B - Mashmokh and ACM
    思路dp。dp[i][j]表示第i位填j时的方案数ac代码#include<bits/stdc++.h>usingnamespacestd;usingi64=longlong;consti64inf=8e18;typedefpair<int,int>pii;constintmod=1e9+7;constintN=2e3+5;intdp[N][N];vector<int>g[N];voi......
  • openpyxl模块---------------------------------------------提取身份证信息及计算年
     上代码:fromopenpyxlimportload_workbookfromdatetimeimportdatetimedefcreate_time():now_year=datetime.now().yearwb=load_workbook('C:/Users/admin/Desktop/11.xlsx')sh=wb.activemax_column=sh.max_columnfori,cellin......
  • 初中英语优秀范文100篇-057My Favourite Story-我最喜欢的故事
    初中英语优秀范文100篇-057MyFavouriteStory-我最喜欢的故事PDF格式公众号回复关键字:SHCZFW057记忆树1MyfavoritestoryisTheNightoftheHorse.翻译我最喜欢的故事是《马之夜》简化记忆故事句子结构主语:Myfavoritestory主语是一个名词短语,由形容词"fav......
  • CF-613-D
    613-D题目大意给定一颗\(n\)个节点的树。\(q\)组询问,每组询问给定\(k\)个点,问至少要删除树中多少个点才能使这\(k\)个点两两不连通,无解则输出\(-1\)。这里\(\sum{k_i}\)的规模大致和\(n\)相当。Solution虚树模板题。暴力的做法是每组询问都对整棵树进行遍树形DP,复杂度为\(......
  • C++实现文件内查找字符串
    实现概要:读取放入buf后查找匹配的第一个字符然后使用seek()移动文件指针,peek()查看剩余的字符是否匹配如果剩余的字符匹配把该字符串在文件中的位置push进一个vector<int>中再继续查看剩余的文件内容//str2.cpp--capacity()andreserve()#include<iostream>......
  • CF1201C - Maximum Median
    思路二分答案。对于一个mid,查询中位数要是为mid的话至少要做多少次操作,最小操作次数就是排序后从中位数开始计算max(0,mid-v[i])的和ac代码#include<bits/stdc++.h>usingnamespacestd;usingi64=longlong;consti64inf=8e18;typedefpair<int,int>pii;cons......
  • P5321 [BJOI2019] 送别 题解--zhengjun
    由于大家的做法需要大量分类讨论和代码量,这里提供一种不怎么分类的,容易实现的做法。首先,由于墙体会随时变化,所以直接对墙体本身维护不是很方便。我们可以牺牲一点常数,对\((i,j)\)建立四个点\(UL_{i,j},UR_{i,j},DL_{i,j},DR_{i,j}\)分别表示\((i-\varepsilon,j-\varepsilo......
  • 无涯教程-LISP - 条件判断
    以下是大多数编程语言中常见的典型决策结构的一般形式-LISP提供以下类型的决策构造,单击以下链接以查看其详细信息。Sr.No.Construct&描述1cond此构造用于检查多个判断操作子句。2ifif构造具有多种形式。3when如果test子句的判断输出为true,则执行test操作,否则,对后......
  • python-爬元气桌面壁纸
    一、利用xpath来抓取图片url地址是:唯美壁纸-唯美手机壁纸-唯美手机动态壁纸-元气壁纸(cheetahfun.com)数据解析方式xpath二、分析在浏览器中打开网页链接后,F12找到元素,可以看到图片的的内容可以在源代码中找到,分析发现,每一个图片分别对应着一个li的标签【在<licla......