多线程学习笔记
前言:这周学习学习了多线程并发的相关知识,写一个读书笔记以作记录。学习的教程是网易云课堂的:https://study.163.com/course/courseMain.htm?courseId=1006067356&trace_c_p_k2=217aa888da5741698cfb97e1e70009cd
更新:在最近的项目中使用到了多线程技术,发现有些知识遗漏或是不准确,已对下文进行更正和重新排版。2020/11/3
一、进程、线程、并发
- 进程:简而言之就是一个运行的程序,如点开一个exe文件就打开了一个进程;
线程:进程中的不同执行路径,即在一个进程中运行了多个功能;(每一个进程都至少包含有一个线程,即主线程,主线程与进程的关系是相互依存)
并发:分为多线程和多进程,顾名思义就是多进程同时运行和多线程同时运行,如同时打开两个QQ客户端就是多进程,VStudio中多个窗口线程就是多线程并发;
二、thread库
以前由于系统环境的不同,如windows、linux,就需要选择不同的线程库进行代码编写,可移植性不高。
C++11之后有了标准的线程库:std::thread。
除此之外,C++还有另外三个库支持多线程编程,分别是**,,和**,之后我会对其一一介绍,现在先来看看Thread库。
看看thread类,它是thread库多线程实现的基础。
构造函数
从构造函数可以窥见“多线程”的一些实现思想。
thread()默认构造函数,创建一个空的 std::thread 执行对象(在线程池的实现中就需要提前创建一定数量的线程对象);
thread(Fn&& fn, Args&&…)初始化构造函数,创建一个 std::thread 对象,该 std::thread 对象可被 joinable,新产生的线程会调用 fn 函数(即可调用对象),该函数的参数由 args 给出。
thread(const thread&) = delete拷贝构造函数(被禁用),意味着 std::thread 对象不可拷贝构造,这也可以理解,“线程”这个概念在同一时刻仅能由一个线程对象运行,所以不存在拷贝赋值之类的;
**thread(thread&& x)**转移/移动构造函数,,调用成功之后 x 不代表任何 std::thread 执行对象,相当于将“线程”的所有权转给了另外的线程对象。
下面这个例子就展示了如何创建线程:
//参考并稍加修改自博客: https://blog.csdn.net/coolwriter/article/details/79883253 //example 1_1 #include <iostream> // std::cout #include <thread> // std::thread void func1() { for (int i = 0; i != 10; ++i) { std::cout << "thread 1 print " << i << std::endl; } } void func2(int n) { std::cout << "thread 2 print " << n << std::endl; } int main() { std::thread t1(func1); std::thread t2(func2, 123); std::cout << "main, foo and bar now execute concurrently...\n"; // synchronize threads: t1.join(); // pauses until first finishes t2.join(); // pauses until second finishes std::cout << "thread 1 and htread 2 completed.\n"; system("pause"); return 0; }
结果:
上述代码中,使用两个重要成员函数。
重要函数
join,它可以阻塞主线程直到子线程执行完毕,即必须等待A.join()执行完毕,才会接着执行之后的代码;
detach,表示该线程和主线程分离,该线程被运行时库给接管,若是在linux环境下运行,会发现即便Ctrl+C退出了主线程,但是子线程依旧还在运行。
一旦线程执行完毕,它所分配的资源将会被释放。
另外,调用 detach 函数之后this不再代表任何的线程执行实例。
PS:值得一提的是,多线程的调度机制可能会造成先创建的线程还未执行,而后面的线程就已经开始执行了,比如上述例子在代码中的顺序应该是thread1 thread2 而后打印出main,但实际效果却并非如此,因此在多线程程序的编写要注意变量的互斥性,以及代码的鲁棒性。
至于其他的成员函数:
get_id:获取线程 ID,返回一个类型为 std::thread::id 的对象。
joinable():检查线程是否可被 join。检查当前的线程对象是否表示了一个活动的执行线程,由默认构造函数创建的线程是不能被 join 的。
swap():Swap 线程,交换两个线程对象所代表的底层句柄即ID等资源,见下:
#include <iostream> #include <thread> #include <chrono> void foo() { std::this_thread::sleep_for(std::chrono::seconds(1)); } void bar() { std::this_thread::sleep_for(std::chrono::seconds(1)); } int main() { std::thread t1(foo); std::thread t2(bar); std::cout << "thread 1 id: " << t1.get_id() << std::endl; std::cout << "thread 2 id: " << t2.get_id() << std::endl; std::swap(t1, t2); std::cout << "after std::swap(t1, t2):" << std::endl; std::cout << "thread 1 id: " << t1.get_id() << std::endl; std::cout << "thread 2 id: " << t2.get_id() << std::endl; t1.swap(t2); std::cout << "after t1.swap(t2):" << std::endl; std::cout << "thread 1 id: " << t1.get_id() << std::endl; std::cout << "thread 2 id: " << t2.get_id() << std::endl; t1.join(); t2.join(); }
结果如下:
yield: 当前线程放弃执行,操作系统调度另一线程继续执行。
sleep_until: 线程休眠至某个指定的时刻(time point),该线程才被重新唤醒,详见连接。
sleep_for: 线程休眠某个指定的时间片(time span),该线程才被重新唤醒,不过由于线程调度等原因,实际休眠时间可能比 sleep_duration 所表示的时间片更长。
#include <iostream> #include <chrono> #include <thread> int main() { std::cout << "Hello waiter" << std::endl; std::chrono::milliseconds dura( 2000 ); std::this_thread::sleep_for( dura ); std::cout << "Waited 2000 ms\n"; }
三、mutex库
互斥
在介绍mutex之间,先简要介绍一下何为互斥。
互斥是指散布在不同任务之间的若干程序片断,当某个任务运行其中一个程序片段时,其它任务就不能运行它们之中的任一程序片段,只能等到该任务运行完这个程序片段后才可以运行。
最基本的场景就是:一个公共资源同一时刻只能被一个进程或线程使用,多个进程或线程不能同时使用公共资源。
mutex主要是对临界区进行互斥操作,防止多线程同时访问该资源,造成系统错误。
常用函数
该库中所要使用的类及函数有以下这些:
mutex类4种 std::mutex,最基本的 Mutex 类。 std::recursive_mutex,支持递归的 Mutex类。 std::time_mutex,定时 Mutex 类。 std::recursive_timed_mutex,定时递归Mutex 类。 Lock 类(两种) std::lock_guard,方便线程对互斥量上锁; std::unique_lock,比unique更灵活; std::try_to_lock_t ,尝试同时对多个互斥量上锁。 std::lock,可以同时对多个互斥量上锁。 std::call_once,如果多个线程需要同时调用某个函数,可以保证多个线程对该函数只调用一次。
在介绍这些函数之前,先提一个概念——“锁”。
什么是锁?
——简单的理解就是,谁拿到了锁,谁就资源去拿屋子的东西!
体现在多线程之中则是:只有某个线程拿到锁时,它才能访问和修改锁对应的资源,其他的任何线程都只有等待!
mutex类
std::mutex 是最基本的互斥量,不支持递归地对 std::mutex 对象上锁,而 std::recursive_lock 则可以。
mutxe的成员函数:
1、构造函数,std::mutex不允许拷贝构造,不允许 move 拷贝,开始产生的 mutex 对象是处于 unlocked 状态的。
2、lock(),该线程将锁住互斥量。会发生下面 3 种情况:
- 如果该互斥量不被锁住,则调用线程将该互斥量锁住,直到调用 unlock之前,该线程一直拥有该锁。
- 如果当前互斥量被其他线程锁住,则当前的调用线程被阻塞。
- 如果当前互斥量被当前调用线程锁住,则会产生死锁。
PS:什么是死锁?
多个线程各占据了一部分资源的锁,但是需要所有资源才可以继续运行,但是大家都不放所,如此相持的局面。——就好比江湖人士各有一片神功秘籍残片,但是人人都不愿意交给其他人,所有人都死盯着对方放手。
3、unlock(), 解锁,释放对互斥量的所有权。
4、try_lock(),尝试锁住互斥量,如果互斥量被其他线程占有,**则当前线程也不会被阻塞。**线程调用该函数也会出现下面 3 种情况:
- 如果当前互斥量没有被其他线程占有,则该线程锁住互斥量,直到该线程调用 unlock 释放互斥量。
- 如果当前互斥量被其他线程锁住,则当前调用线程返回 false,而并不会被阻塞掉。
- 如果当前互斥量被当前调用线程锁住,则会产生死锁。
std::recursive_mutex 与 std::mutex 一样,也是一种可以被上锁的对象,但是和 std::mutex 不同的是,recursive_mutex 允许同一个线程对互斥量多次上锁(即递归上锁),来获得对互斥量对象的**多层所有权,**std::recursive_mutex 释放互斥量时需要调用与该锁层次深度相同次数的 unlock(),可理解为 lock() 次数和 unlock() 次数相同,除此之外,std::recursive_mutex 的特性和 std::mutex 大致相同。
std::time_mutex 比 std::mutex 多了两个成员函数, try_lock_for(),try_lock_until()。
try_lock_for 函数接受一个时间范围,表示在这一段时间范围之内线程如果没有获得锁则被阻塞住(与 std::mutex 的 try_lock() 不同,try_lock 如果被调用时没有获得锁则直接返回 false),如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指定时间内还是没有获得锁),则返回 false。
try_lock_until 函数则接受一个时间点作为参数,在指定时间点未到来之前线程如果没有获得锁则被阻塞住,如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指定时间内还是没有获得锁),则返回 false。
C++多线程编程(下):https://developer.aliyun.com/article/1508312