多线程
C++多线程相关笔记
在传统的C++(C++11之前) 中并没有引入线程这个概念,在C++11出来之前,如果想要在C++中实现多线程,需要借助操作系统平台提供的API,比如Linux的<pthread.h>
,或者Windows下的<windows.h>
C++11提供了语言层面上的多线程,包含在头文件<thread>
中。其解决了跨平台的问题,提供了管理线程、保护共享数据、线程间同步操作、原子操作等类。C++11
引入了5个头文件来支持多线程编程
头文件 | 作用 |
---|---|
<thread> | 提供多线程编程所需的类和函数,包括创建、启动、等待和管理线程 |
<mutex> | 提供互斥锁和其他同步原语的类和函数,用于保护共享资源,防止竞态条件 |
<atomic> | 提供原子操作库,用于执行线程安全的原子操作 |
<future> | 异步编程相关,用于异步执行函数和获取结果 |
<condition_variable> | 提供条件变量类,用于线程之间的协调和通信 |
std::thread
std::thread
常用函数
- 构造&析构函数
函数 | 类别 | 作用 |
---|---|---|
thread() noexcept | 默认构造函数 | 创建一个线程 |
template <class Fn, class… Args> explicit thread(Fn&& fn, Args&&… args) | 初始化构造函数 | 创建一个线程 以 args 为参数执行 fn 函数 |
thread(const thread&) = delete | 复制构造函数 | 禁用std::thread 类的拷贝构造函数 |
thread(thread&& x) noexcept | 移动构造函数 | 构造一个与x 相同的对象会破坏 x 对象 |
~thread() | 析构函数 | 析构对象 |
- 成员函数
函数 | 作用 |
---|---|
void join() | 等待线程结束并清理资源(会阻塞) |
bool joinable() | 返回线程是否可以执行join 函数 |
void detach() | 启动的线程自主在后台运行 必须在线程创建时立即调用,且调用此函数会使其不能被 join |
std::thread::id get_id() | 获取线程id |
thread& operator=(std::thread &&rhs) | 见移动构造函数 (如果对象是 joinable ,会调用std::terminate() 结束程序) |
例一:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
#include <thread>
void thread1()
{
std::cout << "Hello," << std::endl;
}
void thread2()
{
std::cout << "World!" << std::endl;
}
int main()
{
std::thread a(thread1);
std::thread b(thread2);
a.join();
b.join();
return 0;
}
输出结果:
1
Hello, World!
或者
1
2
World!
Hello,
例二:
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 <iostream>
#include <thread>
void countnumber(int id, unsigned int n)
{
for (unsigned int i = 1; i <= n; i++)
{
;
}
std::cout << "Thread " << id << " finished!" << std::endl;
}
int main()
{
std::thread th[10];
for (int i = 0; i < 10; i++)
{
th[i] = thread(countnumber, i, 100000000);
}
for (int i = 0; i < 10; i++)
{
th[i].join();
}
return 0;
}
输出结果之一:
1
2
3
4
5
6
7
8
9
10
Thread 4 finished!
Thread 8 finished!
Thread 6 finished!
Thread 3 finished!
Thread 1 finished!
Thread 7 finished!
Thread 9 finished!
Thread 5 finished!
Thread 2 finished!
Thread 0 finished!
例三:
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
#include <iostream>
#include <thread>
template <class T>
void changevalue(T &x, T val)
{
x = val;
}
int main()
{
std::thread th[100];
int nums[100];
for (int i = 0; i < 100; i++)
{
th[i] = std::thread(changevalue<int>, nums[i], i + 1);
}
for (int i = 0; i < 100; i++)
{
th[i].join();
std::cout << nums[i] << std::endl;
}
return 0;
}
正常编译这个程序,编译器一定会报错。
原因是thread
在传递参数时,是以右值传递(Args&&… args)的:
1
2
template <class Fn, class... Args>
explicit thread(Fn&& fn, Args&&... args)
如果要传递左值,std::ref
和std::cref
解决了这个问题。 std::ref
用于创建对对象的可修改引用(左值引用) std::cref
用于创建对对象的常量引用(const 左值引用)
修改代码为
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
#include <iostream>
#include <thread>
template <class T>
void changevalue(T &x, T val)
{
x = val;
}
int main()
{
std::thread th[100];
int nums[100];
for (int i = 0; i < 100; i++)
{
th[i] = std::thread(changevalue<int>, ref(nums[i]), i + 1);
}
for (int i = 0; i < 100; i++)
{
th[i].join();
std::cout << nums[i] << std::endl;
}
return 0;
}
输出结果:
1
2
3
4
5
6
7
1
2
3
4
...
99
100
注意事项:
- 线程是在
thread
对象被定义的时候开始执行的,而不是在调用join
函数时才执行的,调用join
函数只是阻塞等待线程结束并回收资源 - 分离的线程(执行过
detach
的线程)会在调用它的线程结束或自己结束时释放资源 - 线程会在函数运行完毕后自动释放,不推荐利用其他方法强制结束线程,可能会因资源未释放而导致内存泄漏
- 没有执行
join
或detach
的线程在程序结束时会引发异常
std::this_thread
在<thread>
头文件中,不仅有std::thread
这个类,而且还有一个std::this_thread
命名空间,其可以很方便地让线程对自己进行控制
std::this_thread
常用函数
函数 | 作用 |
---|---|
std::thread::id get_id() noexcept | 获取当前线程id |
template<class Rep, class Period> void sleep_for( const std::chrono::duration<Rep, Period>& sleep_duration ) | 等待sleep_duration |
void yield() noexcept | 暂时放弃线程的执行,将主动权交给其他线程 |
例四:
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
#include <iostream>
#include <thread>
#include <atomic>
std::atomic_bool ready;
void sleep(uintmax_t ms)
{
std::this_thread::sleep_for(std::chrono::milliseconds(ms));
}
void count()
{
while (!ready)
std::this_thread::yield();
for (int i = 0; i <= 20'0000'0000; i++)
;
std::cout << "Thread " << std::this_thread::get_id() << " finished!" << std::endl;
return;
}
int main()
{
ready = 0;
std::thread th[10];
for (int i = 0; i < 10; i++)
{
th[i] = std::thread(::count);
}
sleep(5000);
ready = true;
std::cout << "Start!" << std::endl;
for (int i = 0; i < 10; i++)
{
th[i].join();
}
return 0;
}
输出结果:
1
2
3
4
5
6
7
8
9
10
11
Start!
Thread 139621413664512 finished!
Thread 139621396879104 finished!
Thread 139621371700992 finished!
Thread 139621354915584 finished!
Thread 139621422057216 finished!
Thread 139621380093696 finished!
Thread 139621405271808 finished!
Thread 139621363308288 finished!
Thread 139621346522880 finished!
Thread 139621388486400 finished!
std::mutex和std::atomic
std::mutex
std::mutex
是C++中最基本的互斥量,一个线程将mutex
锁住时,其它的线程就不能操作mutex
,直到这个线程将mutex
解锁。
例五:
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
#include <iostream>
#include <thread>
#include <mutex>
int n = 0;
std::mutex mtx;
void count10000()
{
for (int i = 1; i <= 10000; i++)
{
mtx.lock();
n++;
mtx.unlock();
}
}
int main()
{
std::thread th[100];
for (std::thread &x : th)
{
x = std::thread(count10000);
}
for (std::thread &x : th)
{
x.join();
}
std::cout << n << std::endl;
return 0;
}
输出结果为1000000,这里如果不使用锁,实际输出结果比1000000小,并且输出结果不定
std::mutex
互斥类型
类型 | 说明 |
---|---|
std::mutex | 基本Mutex 类 |
std::recursive_mutex | 递归Mutex 类 |
std::time_mutex | 定时Mutex 类 |
std::recursive_timed_mutex | 定时递归Mutex 类 |
std::mutex
常用成员函数(mutex代指对象)
函数 | 作用 |
---|---|
void lock() | 将mutex 上锁如果 mutex 已经被其它线程上锁,那么会阻塞,直到解锁;如果 mutex 已经被同一个线程锁住,会产生死锁 |
void unlock() | 解锁mutex ,释放其所有权如果有线程因为调用 lock() 而被阻塞,随机将mutex 的控制权交给其中一个线程。如果当前线程未锁定 mutex ,会引发未定义的异常 |
bool try_lock() | 尝试将mutex 上锁如果 mutex 未被上锁,则将其上锁并返回true ,如果mutex 已被锁则返回false |
std::lock_guard
创建std::lock_guard
对象时,它将尝试获取提供给它的互斥锁的所有权。当控制流离开std::lock_guard
对象的作用域时,std::lock_guard
析构并释放互斥量。
std::lock_guard
特点:
- 创建即加锁,作用域结束自动析构并解锁,无需手工解锁
- 不能中途解锁,必须等作用域结束才解锁
- 不能复制
std::unique_lock
std::unique_lock
具有std::lock_guard
的所有功能,同时又具有其他很多方法,使用起来更加灵活方便,能够应对更复杂的锁定需要。
std::unique_lock
的特点:
- 创建时可以不锁定(通过指定第二个参数为
std::defer_lock
),而在需要时再锁定 - 可以随时加锁解锁
- 作用域规则同
std::lock_guard
,析构时自动释放锁 - 不可复制,可移动
- 条件变量需要该类型的锁作为参数(此时必须使用
std::unique_lock
)
std::atomic
std::mutex
很好地解决了多线程资源争抢的问题,但加锁和解锁都需要进行额外的工作,还有最常见的死锁问题
例六:
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
#include <iostream>
#include <thread>
#include <atomic>
std::atomic<int> n;
void count10000()
{
for (int i = 1; i <= 10000; i++)
{
n++;
}
}
int main()
{
n = 0;
std::thread th[100];
for (std::thread &x : th)
{
x = std::thread(count10000);
}
for (std::thread &x : th)
{
x.join();
}
std::cout << n << std::endl;
return 0;
}
原子操作是最小的且不可并行化的操作
即使是多线程,也要像同步进行一样同步操作std::atomic
对象,从而省去了std::mutex
上锁、解锁的时间消耗
注意事项:
原子变量不能使用拷贝构造,初始之后才可以赋值
std::atomic
常用成员函数
函数 | 类型 | 作用 |
---|---|---|
atomic() noexcept = default | 默认构造函数 | 构造std::atomic 对象(可通过 atomic_init 进行初始化) |
constexpr atomic(T val) noexcept | 初始化构造函数 | 构造一个std::atomic 对象用 val 的值来初始化 |
atomic(const atomic&) = delete | 复制构造函数 | 删除std::atomic 类型的拷贝构造函数 |
std::async
注:std::async定义在future头文件中
std::async
参数 不同于std::thread
,std::async
是一个函数,所以没有成员函数。
重载版本 | 作用 |
---|---|
template <class Fn, class… Args> future<typename result_of<Fn(Args…)>::type> async (Fn&& fn, Args&&… args) | 异步或同步(根据操作系统而定) 以 args 为参数执行fn 传递引用参数需要 std::ref 或std::cref |
template <class Fn, class… Args> future<typename result_of<Fn(Args…)>::type> async (launch policy, Fn&& fn, Args&&… args); | 异步或同步(根据policy参数而定) 以 args 为参数执行fn 传递引用参数需要 std::ref 或std::cref |
std::launch
强枚举类(enum class)
std::launch
有2个枚举值和1个特殊值
标识符 | 实际值 | 作用 |
---|---|---|
枚举值launch::async | 0x1(1) | 异步启动 |
枚举值launch::deferred | 0x2(2) | 调用future::get future::wait 时同步启动 |
特殊值launch::async launch::defereed | 0x3(3) | 同步或异步,根据操作系统而定 |
例七:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
#include <thread>
#include <future>
int main()
{
std::async(
std::launch::async, [](const char *message)
{ std::cout << message << std::flush; },
"Hello, ");
std::cout << "World!" << std::endl;
return 0;
}
输出结果:
1
Hello, World!
std::future
例八:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
#include <future>
template <class... Args>
decltype(auto) sum(Args &&...args)
{
return (0 + ... + args); // "0 +"避免空参数包错误
}
int main()
{
std::future<int> val = std::async(std::launch::async, sum<int, int, int>, 1, 10, 100); // 必须带模板参数
std::cout << val.get() << std::endl;
return 0;
}
std::future
常用成员函数
- 构造&析构函数
函数 | 类型 | 作用 |
---|---|---|
future() noexcept | 默认构造函数 | 构造一个空的、无效的future 对象可以移动分配到另一个 future 对象 |
future(const future&) = delete | 复制构造函数 | 删除std::future 类型的拷贝构造函数 |
future (future&& x) noexcept | 移动构造函数 | 构造一个与x 相同的对象并破坏x |
~future() | 析构函数 | 析构对象 |
- 常用成员函数
函数 | 作用 |
---|---|
一般:T get() 当类型为引用: R& future<R&>::get() 当类型为void: void future::get() | 阻塞等待线程结束并获取返回值 若类型为void,则与future::wait()相同 只能调用一次 |
void wait() const | 阻塞等待线程结束 |
template <class Rep, class Period> future_status wait_for(const chrono::duration<Rep,Period>& rel_time) const; | 阻塞等待rel_time 若在这段时间内线程结束则返回 future_status::ready 若没结束则返回 future_status::timeout 若 async 是以launch::deferred 启动的则不会阻塞并立即返回 future_status::deferred |
std::future_status
强枚举类
见上文std::future::wait_for
解释
例九:
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
#include <iostream>
#include <future>
void count_big_number()
{
for (int i = 0; i <= 10'0000'0000; i++)
{
;
}
}
int main()
{
std::future<void> fut = std::async(std::launch::async, count_big_number);
std::cout << "Please wait" << std::flush;
while (fut.wait_for(std::chrono::seconds(1)) != std::future_status::ready) // 每次等待1秒
{
std::cout << '.' << std::flush;
}
std::cout << std::endl
<< "Finished!" << std::endl;
return 0;
}
std::promise
在使用std::thread
而不是std::async
时,直接使用std::future<int>
会报错,需要通过传递引用的方式来获取std::thread
的返回值
例十:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
constexpr long double PI = 3.14159265358979323846264338327950288419716939937510582097494459230781640628;
// 求圆的直径、周长及面积
void get_circle_info(double r, double &d, double &c, double &s)
{
d = r * 2;
c = PI * d;
s = PI * r * r;
}
int main()
{
double r;
std::cin >> r;
double d, c, s;
get_circle_info(r, d, c, s);
std::cout << d << ' ' << c << ' ' << s << std::endl;
return 0;
}
std::promise
实际上是std::future
的一个包装。
因为std::future
的值不能被改变,但可以通过std::promise
来创建一个拥有特定值的std::future
std::promise
常用成员函数
- 构造&析构函数
函数 | 类型 | 作用 |
---|---|---|
promise() | 默认构造函数 | 构造空的std::promise 对象 |
template <class Alloc> promise(allocator_arg_t aa, const Alloc& alloc) | 构造函数 | 与默认构造函数相同 但使用特定的内存分配器alloc构造对象 |
promise (const promise&) = delete | 复制构造函数 | 删除std::promise 类型的拷贝构造函数 |
promise (promise&& x) noexcept | 移动构造函数 | 构造一个与x 相同的对象并破坏x |
~promise() | 析构函数 | 析构对象 |
- 常用成员函数
函数 | 作用 |
---|---|
一般:void set_value (const T& val) void set_value (T&& val) 类型为引用: void promise<R&>::set_value (R& val) 类型为void: void promise::set_value (void) | 设置promise 的值并将共享状态设为ready (将 future_status 设为ready )void 特化:只将共享状态设为ready |
future get_future() | 构造一个future 对象,其值和status 与promise相同 |
例十一: 以例八中的代码为基础加以修改
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
#include <iostream>
#include <thread>
#include <future>
template <class... Args>
decltype(auto) sum(Args &&...args)
{
return (0 + ... + args);
}
template <class... Args>
void sum_thread(std::promise<long long> &val, Args &&...args)
{
val.set_value(sum(args...));
}
int main()
{
std::promise<long long> sum_value;
std::thread get_sum(sum_thread<int, int, int>, ref(sum_value), 1, 10, 100);
std::cout << sum_value.get_future().get() << std::endl;
get_sum.join();
return 0;
}
condition_variable
<condition_variable>
头文件主要包含了与条件变量相关的类和函数。
包括:
- 类
std::condition_variable
std::condition_variable_any
- 枚举类型
std::cv_status。
- 函数
std::notify_all_at_thread_exit()
std::condition_variable
必须结合std::unique_lock
使用,std::condition_variable_any
可以使用任何的锁。
std::condition_variable
常用成员函数
函数 | 作用 |
---|---|
wait(lock) | 使当前线程等待,直到其他线程通知或唤醒 需要一个已经上锁的 std::unique_lock 对象作为参数 |
wait(lock, pred) | 额外提供一个条件函数 只有当 pred 返回true 时,才会真正等待,否则会继续等待或返回 |
notify_one() | 通知等待在条件变量上的一个线程,使其从等待状态唤醒 |
notify_all() | 通知等待在条件变量上的所有线程,使它们从等待状态唤醒 |