文章目录
0 前言
1 线程库的基本使用
进程是进行中的程序,一个操作系统里可以由多个进程(资源调度的最小单位)
线程是进程中的进程,一个进程里可以有多个线程(CPU调度的基本单位)
函数
首先需要有一个函数用于创建线程
如果线程的函数有参数,则
主线程不等待子线程运行完,就继续执行
join是需要主线程回收子线程资源,detach是主线程与子线程分离,子线程退出后的资源由其他进程回收、
join是阻塞的
2. 线程函数中的数据未定义错误
2.1 传递临时变量
因为临时变量在主线程结束后临时变量会被直接释放,所以出错
std::ref 传递引用类型 使用右值引用
2.2 传递指针或引用指向局部变量问题
因为a是局部变量,只在test中有效,
解决问题:把a放在全局区
2.3 传递指针或引用指向已释放的内存的问题
使用智能指针
2.4 入口函数为类的私有函数
使用友元类
3 互斥量解决多线程数据共享问题
使用互斥锁
4.互斥量死锁
假如有两个线程T1,T2,需要对两个互斥量mtx1喝mtx2进行访问,并且需要按照以下顺序获得互斥量的所有权
T1先获取mtx1的所有权,再获得mtx2的所有权
T2先获取mtx2的所有权,在获取mtx1的所有权
T1使用完mtx1后想要获得mtx2的所有权后再mtx1释放所有权,但mtx2需要mtx1释放mtx1的所有权后再释放mtx2的所有权.就造成了互斥量死锁
解决方案:
T1获取完所有的mtx,再给T2获取所有的mtx
5 lock_guard 和C++11 的std::unique_lock
5.1 lock_guard
lock_guard 是一种互斥量的封装
当构造函数调用时,会自动锁定
当析构函数调用时,该互斥量会被自动解锁
std::lock_guard 对象不能复制或者移动,只能在局部作用域中使用
5.2 std::unique_lock
也是多线程中对于互斥量进行加锁解锁的操作。但是比lock_guard有更多的功能,比如延迟加锁、条件变量、超时等。
这样写会和lock_guard一样
5.1 更多的操作
此时需要手动加锁
此时加锁时,会有更多的操作
在5秒内进行阻塞并尝试加锁,如果超时,则直接返回
这里函数的意思是:如果在5秒内没有获得资源并且锁到他,则直接返回
try_lock_until 意思是等到什么时间,一般传系统时间
支持右值引用,支持
6 call_once与其使用场景
在单例模式下使用该场景
call_once
如果多个线程同时调用单例模式,需要保证这个函数在多个线程中只被执行一次
call_once只能在线程中使用,在main函数中使用会报错
7 condition_variable
7.1 生产者和消费者模型
生产者加任务,消费者取任务。这时候可能出现生产者正在加任务,但消费者正在取任务。所以需要加锁
#include<iostream>
#include<thread>
#include<string>
#include<memory>
#include<mutex>
#include<condition_variable>
#include<queue>
std::queue<int> g_queue;
std::condition_variable g_cv;
std::mutex mtx;
void Producer() {
for (int i = 0; i < 10; i++) {
{
// 这里使用两个大括号,是为了限制锁的作用域
std::unique_lock<std::mutex> lock(mtx);
g_queue.push(i);
// 通知消费者取
g_cv.notify_one();
std::cout << "task:" << i << std::endl;
}
std::this_thread::sleep_for(std::chrono::microseconds(100));
}
}
void Consumer() {
while (1) {
std::unique_lock<std::mutex> lock(mtx);
// 等待,如果队列为空,需要等待
g_cv.wait(lock, []() {return !g_queue.empty(); });
int value = g_queue.front();
g_queue.pop();
std::cout << "Consumer: " << value << std::endl;
}
}
int main()
{
std::thread t1(Producer);
std::thread t2(Consumer);
t1.join();
t2.join();
return 0;
}
8 C++ 跨平台线程池
提前开辟好一堆线程
#include<iostream>
#include<thread>
#include<string>
#include<memory>
#include<mutex>
#include<condition_variable>
#include<queue>
#include<functional>
class ThreadPool {
public:
ThreadPool(int numThreads) :stop(false)
{
for (int i = 0; i < numThreads; i++) {
// pushback会使用拷贝构造函数,thread不支持拷贝构造函数,所以使用emplace
threads.emplace_back([this] {
while (1) {
std::unique_lock<std::mutex> lock(mtx);
// 等待任务来
condition.wait(lock, [this] {
return !tasks.empty() || stop;
});
if (stop && tasks.empty()) {
return;
}
// 取任务,取任务列表最左边的任务
std::function<void()> task(std::move(tasks.front()));
tasks.pop();
lock.unlock();
task();
}
});
}
}
~ThreadPool()
{
{
// 线程池结束了
std::unique_lock<std::mutex> lock(mtx);
stop = true;
}
// 通知所有线程完成任务
condition.notify_all();
for (auto& t : threads) {
t.join();
}
}
template<class F,class ... Args>
// 万能引用 给用户提供的接口
void enqueue(F&& f, Args&&...args) {
// 把函数和参数绑定在一起
std::function<void()> task = std::bind(std::forward<F>(f), std::forward<Args>(args)...);
{
// 先锁队列
std::unique_lock<std::mutex> lock(mtx);
// 把函数加入队列
tasks.emplace(std::move(task));
}
condition.notify_one();
}
private:
// 线程数组
std::vector<std::thread> threads;
// 任务队列
std::queue<std::function<void()>> tasks;
// 互斥锁
std::mutex mtx;
// 通知消费者进行取任务的变量
std::condition_variable condition;
bool stop;
};
int main()
{
ThreadPool pool(4);
for (int i = 0; i < 10; i++) {
pool.enqueue([i] {
std::cout << "task :" << i << "task is running" << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(2));
std::cout << "task :" << i << "task is done" << std::endl;
});
}
return 0;
}
线程池需要维护的两个东西 1.线程池2.任务数组
9 异步并发 async future packaged_task promise
9.1 async、future
是C++引入的函数模板,用于异步执行一个函数,并返回一个std::future对象,表示异步操作的结果,使用std::async可以方便的进行异步编程
比如一个函数要执行两次
可以使用打包 当传入async传入func时,就已经开始执行函数了,函数执行的结果存储在future_result中,可以使用get获取。如果get的时候没有执行完 ,会等待执行完再继续往下走
9.2 packaged_task
将函数对象封装成一个std::future对象,可以方便的将函数转化为异步操作,供其他线程使用
9.3 promise
在C++中,promise用于在一个线程中产生一个值,在另一个线程中获得。(也可以使用函数传递,或者通过共享变量)
10 原子操作 std::atomic
用于实现多线程环境下的原子操作。提供了一种线程安全的方式来访问和修改共享变量。
和自己通过锁mutex一样