目录
- 一、并发编程相关的基础概念
- 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) 。
// 左值引用
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 只对构造函数起作用,用来抑制隐式转换
#include <iostream>
#include <utility>
#include <thread>
#include <chrono>
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++ 编程技术,它将必须在使用前请求的资源(分配的堆内存、执行线程、打开
的套接字、打开的文件、锁定的互斥体、磁盘空间、数据库连接等——任何存在受限供给中的事
物)的生命周期绑定与一个对象的生存期相绑定。
2、condition_variable
#include <iostream>
#include <string>
#include <thread>
#include <mutex>
#include <condition_variable>
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++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++关于并发编程的一些库,避免后续工作或者学习中看到相关代码不知道来自于那里。本篇文章类似于预习课文的意思吧。
注:本文章参考了《朱老师物联网大讲堂》课程笔记,并结合了自己的实际开发经历、百度百科以及网上他人的技术文章,综合整理得到。如有侵权,联系删除!水平有限,欢迎各位在评论区交流。