[C++] 多线程 / 异步编程 简易笔记
前言
在 C++ 11 之前,C++
标准库并没有提供内置的多线程支持,实现多线程编程通常需要引入外部第三方库,Linux
上常用 pthread.h
(POSIX Threads),而 Windows 上则需要调用
Windows API,可以使用 pthreads-win32
库或
Boost.Thread
库来实现,跨平台非常麻烦。在 C++ 11
中引入了官方的多线程库
<thread>
,大大方便了代码的编写。
不过如果在 Windows 上使用 MinGW-GCC 的话,若根据网上的旧教程使用
MinGW 8.1.0 版本的话需要注意安装时选择 Posix 而非 Win32,否则
<thread>
不完整,推荐去 Github
下载最新版。
std::thread
在 C++ 11 中使用 std::thread
创建线程,头文件
<thread>
先来看个简单的例子:
1 |
|
std::thread
第一个参数接受一个函数,作为新线程的入口,之后依次跟着该函数的参数,注意引用需要使用
std::ref
或
std::cref
(const ref
)(头文件为
<functional>
)
线程创建后会立即开始,可以调用 join()
等待完成或
detach()
分离线程。线程创建后必须调用其中一个函数,否则在主线程结束后将引发异常。
std::thread
也可以结合 std::function
和
std::bind
使用,这里不展开讲诉。
此外还有一些其它的成员函数,如:
.get_id()
获取此线程 ID.joinable()
此线程是否可join()
std::this_thread::get_id()
获取当前线程 IDstd::this_thread::yield()
切换线程std::this_thread::sleep_for
搭配std::chrono
实现休眠指定时间std::this_thread::sleep_until
同上
std::mutex
(互斥锁)及其它锁
关于为什么需要使用 std::mutex
这里不进行详细展开,可以去操作系统课或多线程编程相关资料了解一下,大概就是多个线程同时对一个资源操作可能引发异常。这时候就需要能够保护变量不被其它线程使用的方法,即锁。
首先是 std::mutex
互斥锁,头文件
<mutex>
。
使用非常简单,见下面的代码:
1 |
|
成员函数:
.lock()
尝试获取锁,不成功则阻塞线程直到获取锁.unlock()
解锁,若不是此线程上锁则会引发异常.try_lock()
尝试获取锁,成功则上锁并返回true
但通常不直接使用
std::mutex
,因为使用不当可能产生死锁(如上方代码忘记
.unlock()
),根据 C++ 的 RAII 思想,标准库封装了
std::lock_guard
,std::unique_lock
,std::shared_lock
(C++
14) 及 std::scoped_lock
(C++ 17)等类型来管理锁。以
std::lock_guard
为例:
1 |
|
不同于 std::lock_guard
,std::unique_lock
比较灵活,但效率上会差一点,通常较少使用。而
std::unique_lock
可以带第二个参数:
std::adopt_lock
表示在声明std::unique_lock
之前已经 lock 了目标锁,因此std::unique_lock
不会再尝试 lock,仅负责 unlock。std::lock_guard
也可以携带这个参数。std::try_to_lock
尝试 lock,但不一定成功,不会阻塞线程。此时可以调用成员函数.owns_lock()
判断是否拥有控制权。std::defer_lock
使用这个参数的前提是目标没有上锁,否则会引发异常。表示不主动上锁,需要自己调用成员函数上锁,好处是可以中途手动控制解锁一段时间。部分成员函数:
.lock()
上锁.unlock()
解锁(可以不调用,在std::unique_lock
销毁时会自动调用).try_lock()
尝试上锁并返回是否成功.release()
释放该std::mutex
的所有权,返回std::mutex*
传递 std::unique_lock
时可以使用
std::move
。
此外,C++ 还有如
std::timed_mutex
(允许获取锁时的时间限制),std::recursive_mutex
(允许同一线程递归),std::recursive_timed_mutex
,std::shared_timed_mutex
(C++
14)等锁类型,感兴趣的话可以查查资料。
std::atomic
为了减轻多线程使用锁的负担,C++ 标准库提供了一些原子操作(不会在中途被打断)以方便使用。
最简单的例子,std::atomic_int
(即
std::atomic<int>
),头文件
<atomic>
:
1 |
|
其它类型及成员函数见 cplusplus,其实大多数时候就当成普通变量使用,注意
std::atomic
不支持浮点数。
std::condition_variable
C++11 中的 std::condition_variable
(头文件
<condition_variable>
)主要用于在多线程环境中实现同步和通信,需要和
std::unique_lock<std::mutex>
联合使用。
简单例子:
1 |
|
接下来看看相关的成员函数:
.wait()
两种形式:.wait(std::unique_lock<std::mutex> &__lock)
调用后释放互斥锁、等待在条件变量上、在其它线程调用.notify()
时再次获取互斥锁并向下执行.wait(std::unique_lock<std::mutex> &__lock, _Predicate __p)
调用.notify()
时会检测第二个参数(通常是个函数)是否成立,不成立则继续等待
.wait_for()
有时间限制的.wait()
.wait_until()
同上.notify_one()
随机提醒一个正在等待的线程.notify_all()
提醒所有正在等待的线程
std::async
/ std::future
/ std::promise
异步编程
所属头文件:<future>
std::async
(这里指的是 C++ 11,而非 C++ 20
<coroutine>
协程中的 async
)是
std::thread
的进一步封装。
不同于 std::thread
,std::async
实际是一个函数,返回一个 std::future
类。比起说明,直接看一个例子更为直观:
1 |
|
而 std::async
具体是异步还是同步根据操作系统而定,在部分操作系统会在调用
std::future.get()
之类的函数时才执行。
也可以通过新增一个参数手动指定 std::async
的方式:std::async(std::launch::async, ...)
其中第一个参数有以下几种值:
std::launch::async (0x1)
立即异步启动std::launch::deferred (0x2)
实际调用(如std::future.get()
)时启动std::launch::async | std::launch::deferred (0x3)
由操作系统决定
与 std::thread
相同,如果函数带参数的话就需要依次加在后面,引用需要
std::ref
或 std::cref
,也可以结合
std::bind
使用:
1 |
|
而回到 std::future<>
类,也有几个简单的成员函数:
.get()
阻塞当前线程,获取结果(只能调用一次,若需多次调用需要std::shared_future
).wait()
阻塞当前线程,等待结果.wait_for()
等待指定时间,根据目标线程是否结束返回std::future_status::ready
或std::future_status::timeout
若std::async
启动方式为std::launch::deferred
则直接返回std::future_status::deferred
.wait_until()
同理.share()
产生一份类型为std::shared_future
的拷贝
讲完了 std::async
和 std::future
,那么
std::promise
是干什么用的呢?
在特定场景下,我们可能需要获取 std::thread
的返回值,那么怎么做呢?显然的方法是传递引用或者通过全局变量获取返回值,但这样看起来就很不高端,这时
std::promise
就派上了用场。
std::promise
可以理解成一个 std::future
的包装,通过 .get_future()
成员函数可以获得,并且内部有一个值,可以通过接口修改,具体见例子:
1 |
|
以上只是一种用法,也可以以引用形式传入线程。
具体的成员函数:
.set_value()
设置std::promise
的值,并且future_status
变为std::future_status::ready
.get_future()
获取std::future
对象.set_exception()
设置异常,std::future.get()
将会引发异常.set_value_at_thread_exit()
在当前线程结束后设置值
话说回来,至于到底应该用 std::thread
还是
std::async
,实际使用中两者的性能区别并不会太大,个人更推荐使用
std::async
,方便快捷,不容易出错,容易获取线程返回值,并且并不会比
std::thread
性能差很多。
std::thread
在资源紧张时可能创建失败,导致程序崩溃。std::async
在资源紧张时会推迟执行,最终在调用了 future::get()
的线程上执行。
std::jthread
/
<coroutine>
这一部分是 C++ 20 的内容,因为 C++ 20
比较超前,暂时不进行探讨,或许之后会回来填坑?