C++并发编程

目录

  • 一、并发编程相关的基础概念
    • 1、操作系统(Linux)
    • 2、任务和通信
    • 3、多进程和多线程
    • 4、C++中的多线程发展史
  • 二、pthread线程使用讲解和实战
    • 1、pthread基本使用
    • 2、线程的分离
    • 3、线程属性
    • 4、关于线程的几个值得注意的点
  • 三、线程的同步之互斥锁、读写锁、非阻塞式锁和条件变量
    • 1、线程同步的必要性
    • 2、互斥锁mutex
    • 3、读写锁
    • 4、非阻塞式锁
    • 5、条件变量
  • 四、标准库的thread基本使用
    • 1、标准库中线程支持
    • 2、std::thread的使用和案例分析
    • 3、管理当前线程的函数
  • 五、thread的线程同步
    • 1、mutex
    • 2、condition_variable
  • 六、thread的异步机制future
  • 七、C++20新引入的jthread

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

1、操作系统(Linux)

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

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

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

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

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

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

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

2、任务和通信

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

进程间通信的方式:无名管道( pipe )、高级管道(popen)、有名管道(named pipe)、
消息队列( message queue )、信号量( semophore ) 、信号 ( sinal ) 、共享内存、
( shared memory ) 、套接字( socket )

线程间通信的方式:互斥锁、读写锁、自旋锁、条件变量、信号机制、信号量机制

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

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

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

3、多进程和多线程

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

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

(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

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);获取线程状态

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);

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);

4、非阻塞式锁

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

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);//销毁条件变量

(5)条件变量的实例

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

四、标准库的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)

C++并发编程_第1张图片

// 左值引用
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 只对构造函数起作用,用来抑制隐式转换

C++并发编程_第2张图片

#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

3、管理当前线程的函数

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

五、thread的线程同步

1、mutex

RAII风格:https://zh.cppreference.com/w/cpp/language/raii

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

C++并发编程_第3张图片

2、condition_variable

C++并发编程_第4张图片
C++并发编程_第5张图片

#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

六、thread的异步机制future

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

C++并发编程_第6张图片

七、C++20新引入的jthread

阅读参考:https://www.zhihu.com/question/364140779/answer/959369984
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++一直以来“不为不使用的功能付费”的设计哲学,他们自然就把这些新功能拆出来
新做了一个类。

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

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

你可能感兴趣的:(从C高级到征服C++,c++,linux,并发,多线程)