• C++并发编程


    一、并发编程相关的基础概念

    1、操作系统(Linux)

    (1)内核和应用
      在Linux程序开发中,涉及到了许多的并发操作,根据程序的上下层关系可分为两部分,一部分是在内核中,即操作系统本身的在运行过程中就包含有大量的并发操作,如硬件驱动程序和内存管理以及进程调度模块就在同时运行着;另一部分就是我们经常说的应用程序,运行在linux系统之上的程序,如一个服务器应用程序,同时存在两个线程在运行,一个负责监听客户端,一个负责处理客户端的请求;

    (2)进程和线程
      进程是系统进行资源分配的基本单位,线程是CPU进行调度的基本单位。一个进程中包含多个线程。有关于进程线程更多的区别和细节请自行百度,这里不再赘述。也可阅读我的另一个专栏《Linux IO编程和网络编程入门》;

    (3)并行和串行,宏观和微观
      并行简而言之就是可以同一时间干很多件事情;串行就是有先后顺序,一件事情结束以后才能去做另一件事情。软件开发中的并行和串行也是如此,将一件事情替换为一个程序或者一个进程又或一个线程,能否同时运行;

      多个线程只有在多核处理器才能真正的同时运行,在单个处理器上只能做到假并行,看起来是在同时运行,实际上是调度器以时间片为单位切换调度执行多个线程,某一时刻只有一个程序在运行;

    (4)系统调用,POSIX API,函数库、框架库

    已经有人讲的很清楚了,我就不班门弄斧、制造垃圾了:
    https://dandelioncloud.cn/article/details/1555512222984392706
    
    • 1
    • 2

    (5)阻塞和非阻塞
      阻塞和非阻塞指的是调用者(程序)在等待返回结果(或输入)时的状态。阻塞时,在调用结果返回前,当前线程会被挂起,并在得到结果之后返回。非阻塞时,如果不能立刻得到结果,则该调用者不会阻塞当前线程。因此对应非阻塞的情况,调用者需要定时轮询查看处理状态。
    ---------------来源于百度百科

    2、任务和通信

    (1)进程间通信IPC与线程间通信

    进程间通信的方式:无名管道( pipe )、高级管道(popen)、有名管道(named pipe)、
    消息队列( message queue )、信号量( semophore ) 、信号 ( sinal ) 、共享内存、
    ( shared memory ) 、套接字( socket )
    
    线程间通信的方式:互斥锁、读写锁、自旋锁、条件变量、信号机制、信号量机制
    
    • 1
    • 2
    • 3
    • 4
    • 5

    (2)同步和异步
      同步就相当于是 当客户端发送请求给服务端,在等待服务端响应的请求时,客户端不做其他的事情。当服务端做完了才返回到客户端。这样的话客户端需要一直等待。用户使用起来会有不友好。

      异步就是,当客户端发送给服务端请求时,在等待服务端响应的时候,客户端可以做其他的事情,这样节约了时间,提高了效率。

      也可以认为,同步就是双方约定了一个固定的频率进行某件事情,如每天两点见面,则在两点的时候就不能去做其他事情了;而异步则是双方见面的时间不固定,想见了就见,没有固定约定的时间。

    3、多进程和多线程

    (1)如何选择使用多进程还是多线程

    https://www.cnblogs.com/mude918/p/11750350.html
    
    • 1

    (2)单核和对称多核SMP下的多线程
      只有在多核下,才可以实现多个线程同时运行,在单核上只能实现伪并行;

    4、C++中的多线程发展史

    (1)C++98中没有并发支持,因为C中也没有并发支持,早期C++认为这不是语言该管的事儿

    (2)POSIX OS的pthread被广泛用于C/C++的多线程编程

    (3)这造成很大问题是:很多C++程序员根本没有并发编程的意识,需要时也只能盲目胡乱找资源

    (4)Java在语言层面源生支持并发,取得了很大的成功和很好的反响

    (5)C++11中开始引入并发编程机制std::thread

    二、pthread线程使用讲解和实战

      对于二、三部分请阅读我之前写的一篇博客《linux线程全解
    》,在本篇就不详述了,避免重复性工作。

      pthread与操作系统和编程语言无关,是符合posix标准的操作系统都具有的API

    1、pthread基本使用

    (1)ubuntu系统中进行 man 手册安装:
    sudo apt-get install glibc-doc manpages-posix manpages-posix-dev
    
    (2)头文件:#include <pthread.h>
    
    (3)链接时添加:-lpthread
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    2、线程的分离

    (1)线程有2中状态:JOINABLE或者DETACHED,默认是JOINABLE

    (2)JOINABLE的线程必须在创建它的线程中使用pthread_join回收,否则会有资源未释放

    (3)DETACHED的线程可以在终止时释放资源,这样创建它的线程就不用通过pthread_join来等待接收

    (4)线程转为DETACHED有2种方法:第一种是线程函数内自己调用pthread_detach(pthread_self());

    3、线程属性

    (1)pthread_attr_t attr;//声明一个参数
    (2)pthread_attr_init(&attr);//对参数进行初始化
    (3)pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);//设置线程为可连接的
    (4)pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);//设置线程为可分离的
    (5)pthread_attr_destroy(&attr)//销毁属性,防止内存泄漏
    (6)int pthread_attr_getdetachstate(pthread_attr_t *attr,int *detachstate);获取线程状态
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    4、关于线程的几个值得注意的点

    (1)main所在的线程称之为“初始线程”,从main返回的时候,整个进程都被终止了;

    (2)在任意线程内调用exit函数会让该线程所在的进程整个退出。所以主动退出线程的时候一定要使用pthread_exit函数,而不是exit;

    (3)当主线程调用pthread_exit函数仅仅只是终止主线程,其他线程仍将继续存在;

    三、线程的同步之互斥锁、读写锁、非阻塞式锁和条件变量

    1、线程同步的必要性

      当我们有多个线程要同时访问一个变量或对象时,如果这些线程中既有读又有写操作时,就会导致变量值或对象的状态出现混乱,从而导致程序异常。

      举个例子,如果一个银行账户同时被两个线程操作,一个取100块,一个存钱100块。假设账户原本有0块,如果取钱线程和存钱线程同时发生,会出现什么结果呢?
      (1)账户余额是0,取钱不成功;(2)账户余额是100,取钱成功了。那到底是哪个呢?很难说清楚。因此多线程同步就是要解决这个问题,使得在同一时刻只有一个动作可以作用于这个对象身上;

    2、互斥锁mutex

    (1)互斥锁静态初始化:pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
    (2)互斥锁动态初始化:pthread_mutex_init(&mutex,NULL);
    (3)上锁和解锁:pthread_mutex_lock(&mutex);	pthread_mutex_unlock(&mutex);
    (4)互斥锁销毁:pthread_mutex_destroy(&mutex);
    
    • 1
    • 2
    • 3
    • 4

    3、读写锁

    pthread_rwlock_t
    初始化:int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
    销毁:int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
    进行读操作上锁:int pthread_rwlock_rdlock(pthread_rwlock_t  *rwlock);
    进行写操作上锁:int pthread_rwlock_wrlock(pthread_rwlock_t  *rwlock);
    解锁:int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    4、非阻塞式锁

    (1)互斥锁非阻塞式上锁:pthread_mutex_trylock(&mutex);
    (2)读写锁非阻塞式上锁:
    	int pthread_rwlock_trywrlock(pthread_rwlock_t  *rwlock);
    	int pthread_rwlock_tryrdlock(pthread_rwlock_t  *rwlock);
    
    • 1
    • 2
    • 3
    • 4

    5、条件变量

    (1)条件变量的核心功能:A线程等待条件时阻塞wait,B线程必要时signal唤醒A
    (2)条件变量实现了多个线程之间的同步
    (3)条件变量常用于所谓的“生产者与消费者模型”
    (4)条件变量相关API

    pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;/*初始化互斥锁*/
    pthread_cond_t cond = PTHREAD_COND_INITIALIZER;/*初始化条件变量*/
    pthread_mutex_lock(&mutex);/*锁住互斥量*/
    pthread_cond_signal(&cond);//发送信号量 跟wait函数不在同一个线程中
    pthread_cond_wait(&cond,&mutex);//阻塞线程,等待条件变量,同时解锁互斥量
    pthread_mutex_unlock(&mutex);//解锁互斥量
    pthread_mutex_destroy(&mutex);//销毁互斥锁
    pthread_cond_destroy(&cond);//销毁条件变量
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    (5)条件变量的实例

    参考:https://blog.csdn.net/chengonghao/article/details/51779279
    
    • 1

    四、标准库的thread基本使用

    1、标准库中线程支持

    (1)参考:https://zh.cppreference.com/w/cpp/thread
    (2)C++11的实现是主体,C++20只是增加了扩展

    2、std::thread的使用和案例分析

    构造函数和传参:https://zh.cppreference.com/w/cpp/thread/thread/thread

    类 thread 表示单个执行线程。线程允许多个函数同时执行。在头文件 <thread> 定义;
    
    线程在构造关联的线程对象时立即开始执行(等待任何OS调度延迟),从提供给作为构造函数
    参数的顶层函数开始。顶层函数的返回值将被忽略,而且若它以抛异常终止,则调用
     std::terminate 。顶层函数可以通过 std::promise 或通过修改共享变量(可能需要同
     步,见 std::mutex 与 std::atomic )将其返回值或异常传递给调用方。
    
    std::thread 对象也可能处于不表示任何线程的状态(默认构造、被移动、 detach 或 
    join 后),并且执行线程可能与任何 thread 对象无关( detach 后)。
    
    没有两个 std::thread 对象会表示同一执行线程; std::thread 不是可复制构造 
    (CopyConstructible) 或可复制赋值 (CopyAssignable) 的,尽管它可移动构造 
    (MoveConstructible) 且可移动赋值 (MoveAssignable)
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    在这里插入图片描述

    // 左值引用
    int num = 10;
    int &b = num;     // 正确
    int &c = 10;      // 错误
     
    int num = 10;
    const int &b = num;   // 正确
    const int &c = 10;    // 正确
     
     
    // 右值引用
    int num = 10;
    //int && a = num;    // 错误,右值引用不能初始化为左值
    int && a = 10;       // 正确
     
    a = 100;
    cout << a << endl;   // 输出为100,右值引用可以修改值
     
     
    // 右值引用的使用
    // 如 thread argv 的传入
    template<typename _Callable, typename... _Args>
    explicit thread(_Callable&& __f, _Args&&... __args) { 
    //.... 
    }
    // Args&&... args 是对函数参数的类型 Args&& 进行展开
    // args... 是对函数参数 args 进行展开
    // explicit 只对构造函数起作用,用来抑制隐式转换
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    在这里插入图片描述

    #include 
    #include 
    #include 
    #include 
     
    void f1(int n)
    {
        for (int i = 0; i < 5; ++i) {
            std::cout << "Thread 1 executing\n";
            ++n;
            std::this_thread::sleep_for(std::chrono::milliseconds(10));
        }
    }
     
    void f2(int& n)
    {
        for (int i = 0; i < 5; ++i) {
            std::cout << "Thread 2 executing\n";
            ++n;
            std::this_thread::sleep_for(std::chrono::milliseconds(10));
        }
    }
     
    class foo
    {
    public:
        void bar()
        {
            for (int i = 0; i < 5; ++i) {
                std::cout << "Thread 3 executing\n";
                ++n;
                std::this_thread::sleep_for(std::chrono::milliseconds(10));
            }
        }
        int n = 0;
    };
     
    class baz
    {
    public:
        void operator()()
        {
            for (int i = 0; i < 5; ++i) {
                std::cout << "Thread 4 executing\n";
                ++n;
                std::this_thread::sleep_for(std::chrono::milliseconds(10));
            }
        }
        int n = 0;
    };
     
    int main()
    {
        int n = 0;
        foo f;
        baz b;
        std::thread t1; // t1 不是线程
        std::thread t2(f1, n + 1); // 按值传递
        std::thread t3(f2, std::ref(n)); // 按引用传递
        std::thread t4(std::move(t3)); // t4 现在运行 f2() 。 t3 不再是线程
        std::thread t5(&foo::bar, &f); // t5 在对象 f 上运行 foo::bar()
        std::thread t6(b); // t6 在对象 b 的副本上运行 baz::operator()
        t2.join();
        t4.join();
        t5.join();
        t6.join();
        std::cout << "Final value of n is " << n << '\n';
        std::cout << "Final value of f.n (foo::n) is " << f.n << '\n';
        std::cout << "Final value of b.n (baz::n) is " << b.n << '\n';
    }
    可能的输出:
    
    Thread 1 executing
    Thread 2 executing
    Thread 3 executing
    Thread 4 executing
    Thread 3 executing
    Thread 1 executing
    Thread 2 executing
    Thread 4 executing
    Thread 2 executing
    Thread 3 executing
    Thread 1 executing
    Thread 4 executing
    Thread 3 executing
    Thread 2 executing
    Thread 1 executing
    Thread 4 executing
    Thread 3 executing
    Thread 1 executing
    Thread 2 executing
    Thread 4 executing
    Final value of n is 5
    Final value of f.n (foo::n) is 5
    Final value of b.n (baz::n) is 0
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95

    3、管理当前线程的函数

    定义于命名空间 this_thread
    	yield(C++11)建议实现重新调度各执行线程(函数)
    	get_id (C++11)返回当前线程的线程 id(函数)
    	sleep_for (C++11)使当前线程的执行停止指定的时间段(函数)
    	sleep_until (C++11)使当前线程的执行停止直到指定的时间点(函数)
    
    • 1
    • 2
    • 3
    • 4
    • 5

    五、thread的线程同步

    1、mutex

    RAII风格:https://zh.cppreference.com/w/cpp/language/raii
    
    	资源获取即初始化(Resource Acquisition Is Initialization),或称 RAII,是
    一种 C++ 编程技术,它将必须在使用前请求的资源(分配的堆内存、执行线程、打开
    的套接字、打开的文件、锁定的互斥体、磁盘空间、数据库连接等——任何存在受限供给中的事
    物)的生命周期绑定与一个对象的生存期相绑定。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    在这里插入图片描述

    2、condition_variable

    在这里插入图片描述
    在这里插入图片描述

    #include 
    #include 
    #include 
    #include 
    #include 
     
    std::mutex m;
    std::condition_variable cv;
    std::string data;
    bool ready = false;
    bool processed = false;
     
    void worker_thread()
    {
        // 等待直至 main() 发送数据
        std::unique_lock<std::mutex> lk(m);
        cv.wait(lk, []{return ready;});
     
        // 等待后,我们占有锁。
        std::cout << "Worker thread is processing data\n";
        data += " after processing";
     
        // 发送数据回 main()
        processed = true;
        std::cout << "Worker thread signals data processing completed\n";
     
        // 通知前完成手动解锁,以避免等待线程才被唤醒就阻塞(细节见 notify_one )
        lk.unlock();
        cv.notify_one();
    }
     
    int main()
    {
        std::thread worker(worker_thread);
     
        data = "Example data";
        // 发送数据到 worker 线程
        {
            std::lock_guard<std::mutex> lk(m);
            ready = true;
            std::cout << "main() signals data ready for processing\n";
        }
        cv.notify_one();
     
        // 等候 worker
        {
            std::unique_lock<std::mutex> lk(m);
            cv.wait(lk, []{return processed;});
        }
        std::cout << "Back in main(), data = " << data << '\n';
     
        worker.join();
    }
    输出:
    
    main() signals data ready for processing
    Worker thread is processing data
    Worker thread signals data processing completed
    Back in main(), data = Example data after processing
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59

    六、thread的异步机制future

    参考阅读:https://zh.cppreference.com/w/cpp/thread/future
    
    • 1

    在这里插入图片描述

    七、C++20新引入的jthread

    阅读参考:https://www.zhihu.com/question/364140779/answer/959369984
    
    • 1
    1)std::jthread与std::thread的区别是什么?
    	据我所知,特性上,std::jthread相比std::thread主要增加了以下两个功能:
    	1.std::jthread对象被destruct时,会自动调用join,等待其所表示的执行流结束。
    	2.支持外部请求中止(通过get_stop_source、get_stop_token和request_stop)。
    
    (2)为什么不是选择往std::thread添加新接口,而是引入了一个新的标准库?
    因为std::jthread为了实现上述新功能,带来了额外的性能开销(主要是多了一个成员变量)
    。而根据C++一直以来“不为不使用的功能付费”的设计哲学,他们自然就把这些新功能拆出来
    新做了一个类。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    关于《C++并发编程》后续还会增加章节的,这篇文章讲的很简略,更多的是让大家知道C++关于并发编程的一些库,避免后续工作或者学习中看到相关代码不知道来自于那里。本篇文章类似于预习课文的意思吧

    注:本文章参考了《朱老师物联网大讲堂》课程笔记,并结合了自己的实际开发经历、百度百科以及网上他人的技术文章,综合整理得到。如有侵权,联系删除!水平有限,欢迎各位在评论区交流。

  • 相关阅读:
    代码版的《本草纲目》毽子舞!如何本地整一个
    【数据库】存储引擎InnoDB、MyISAM、关系型数据库和非关系型数据库、如何执行一条SQL等重点知识汇总
    c语言程序范例
    springmvc-day01
    零零信安-D&D数据泄露报警日报【第37期】
    初识操作系统
    SQL Server查看端口号
    软考程序员考试大纲(2023)
    【LeetCode-中等】221. 最大正方形(详解)
    67-94-hive-函数-开窗函数-常用函数-udf自定义函数
  • 原文地址:https://blog.csdn.net/weixin_45842280/article/details/127896204