c++11 知识点汇总
文章目录
- c++11 知识点汇总
- 一、C++11常用关键知识点梳理
- 1. 关键字和语法
- 2. 绑定器和函数对象
- 3. 智能指针
- 4. 容器
- 二、C++语言级别支持的多线程编程 -- 本节重点
- 1. 通过thread类编写C++多线程程序
- 2. 线程间互斥锁与死锁 -- 对应linux多线程互斥锁
- 代码:
- **竞态条件**
- 线程互斥mutex---- 跟linux使用非常像
- 死锁问题
- lock_guard-保证所有线程都能释放锁
- unique_lock
- 3. 线程间同步通信-生产者消费者模型
- **生产者-消费者**线程模型
- 错误案例
- 正确案例-- 使用条件变量
- 2.4 再谈lock_guard和unique_lock
- 2.5 基于CAS操作的atomic原子类型
- 2.6额外补充:CAS的成员方法---课里没讲
- **基本语法和使用示例**
- **CAS 操作解释:**
- **总结**:
一、C++11常用关键知识点梳理
1. 关键字和语法
-
auto
:可以根据右值,推导出右值的类型,然后左边变量的类型就已知了 -
nullptr
:给指针专用(能够和整数进行区别);之前的NULL
是一个宏定义#define NULL 0
,在代码上无法区分整数和指针地址 -
foreach
语句:可以遍历数组(底层是指针遍历),容器(底层是迭代器遍历)等
for(Type val : container) => 底层就是通过指针或者迭代器来实现的cout<<val<<" ";
-
右值引用:
move
移动语义函数和forward
类型完美转发函数 -
模板的一个新特性:
typename... A
表示可变参(类型参数)
2. 绑定器和函数对象
function
:函数对象bind
:绑定器bind1st
和bind2nd
只能结合 二元函数对象=>一元函数对象 – 非常有限lambda
表达式
3. 智能指针
shared_ptr
和weak_ptr
----带引用计数, ---- 分强弱智能指针, weak_ptr指针.lock()
可以提升为强智能指针
不带引用计数虽然有好几个, 但是 推荐使用 unique_ptr
4. 容器
set和map
:红黑树,增删查O(logn) – 以前c++标准库就有unorder_set和unorder_map
:哈希表,增删查O(1) ---- c++11 新增array
:数组,固定大小,不可扩容。区别于vector
---- c++11 新增, 需要确保数量已知forward_list
:前向链表,单链表。list
是双向链表 ---- c++11 新增
推荐使用vector和list, 更灵活, 具体情况具体看待
二、C++语言级别支持的多线程编程 – 本节重点
linux下的 线程函数, 在c++里并不适用
C++语言级别的多线程编程 =>代码可以跨平台:windows/linux/mac
主要内容: thread/mutex/condition_variable–线程, 互斥, 条件变量
锁: lock_quard, unique_lock
原子类型: atomic
睡眠: sleep_for
C++语言层面 thread--可以根据系统, 使用不同的底层(底层用的还是下面平台的方法) windows linux:| |
createThread pthread_create
需要包含的头文件:#include <thread>
-----linux是 pthread
1. 通过thread类编写C++多线程程序
-
主要函数:
std::thread: 创建线程对象, ---- 类似于 pthread_create
std::this_thread::sleep_for(std::chrono::seconds(2)) : 线程睡眠2s – 类似于 sleep(2)
线程对象.join : 回收等待子线程, 在c++里 不分离线程的化, 必须回收等待, 这个与 linux不同----非常严格
线程对象.detach: 分离线程
-
怎么创建启动一个线程?
std::thread
定义一个线程对象,传入线程所需要的线程函数和参数,线程自动开启 -
子线程如何结束?
子线程函数运行完成,线程就结束了 -
主线程如何处理子线程?
t.join()
:等待t
线程结束,当前线程继续往下运行
t.detach()
:把t
线程设置为分离线程,主线程结束,整个进程结束,所有子线程都自动结束,类似于守护线程
#include <iostream>
#include <thread>
using namespace std;void threadHandle1(int time)
{//让子线程睡眠time秒std::this_thread::sleep_for(std::chrono::seconds(time));cout << "hello threadHandle1!" << endl;
}void threadHandle2(int time)
{//让子线程睡眠time秒std::this_thread::sleep_for(std::chrono::seconds(time));cout << "hello threadHandle2!" << endl;
}int main()
{// 创建了一个线程对象,传入一个线程函数(作为线程入口函数), 新线程就开始运行,没有先后顺序,随着CPU的调度算法执行std::thread t1(threadHandle1, 1);std::thread t2(threadHandle2, 2);// 主线程运行到这里,等待子线程结束,主线程继续往下运行t1.join();t2.join();// 把子线程设置为分离线程//t1.detach();//t2.detach();cout << "main thread done!" << endl;/*主线程运行完成时,会查看当前进程是否还有未运行完成的子线程如果有未运行完成的子线程,那么进程就会异常终止*/return 0;
}
2. 线程间互斥锁与死锁 – 对应linux多线程互斥锁
c++ thread 模拟车站三个窗口买票的 程序
代码:
– 不加互斥锁, 数据是乱的, 会同时同一时间 访问 某个量
#include <iostream>
#include <thread>
#include <list>
using namespace std;/*
c++ thread 模拟车站三个窗口买票的 程序
*/int ticketCount = 10; // 车站有10张车票,由三个窗口一起卖票// 模拟卖票的线程函数
void sellTicket(int index)
{while (ticketCount > 0){cout << "窗口:" << index << "卖出第:" << 11-ticketCount << "张票!" << endl;ticketCount--;std::this_thread::sleep_for(std::chrono::seconds(1));}
}int main()
{list<std::thread> tlist; // 使用双向链表for (int i = 1; i <= 3; ++i){tlist.push_back(std::thread(sellTicket, i));}for (auto& t : tlist){t.join();}cout << "所有窗口卖票结束!" << endl;return 0;
}
竞态条件
指的是多个线程或进程同时访问共享资源时,程序的执行结果依赖于线程或进程的执行顺序,从而导致不可预测的行为或错误。
线程互斥mutex---- 跟linux使用非常像
要对线程安全进行保障,这就需要线程间的互斥,使用互斥锁,需要包含头文件#include <mutex>
.lock()
.unlock()
注意 : 加锁和解锁的位置! 非常影响打印的效果
#include <iostream>
#include <thread>
#include <list>
#include <mutex>
using namespace std;/*
c++ thread 模拟车站三个窗口买票的 程序
*/int ticketCount = 10; // 车站有10张车票,由三个窗口一起卖票std::mutex mtx; // 全局的一把互斥锁// 模拟卖票的线程函数
void sellTicket(int index)
{//mtx.lock();//这里是不行的, 导致 一个线程 把票全卖了while (ticketCount > 0){mtx.lock();if (ticketCount > 0) // 必须再次判断, 因为是先进来循环,才等锁, 会引发另一个卖完了, 这个还在循环里, 还会卖{cout << "窗口:" << index << "卖出第:" << 11 - ticketCount << "张票!" << endl;ticketCount--;}mtx.unlock();std::this_thread::sleep_for(std::chrono::milliseconds(100));}}int main()
{list<std::thread> tlist; // 使用双向链表for (int i = 1; i <= 3; ++i){tlist.push_back(std::thread(sellTicket, i));}for (auto& t : tlist){t.join();}cout << "所有窗口卖票结束!" << endl;return 0;
}
临界区(Critical Section) 是多线程编程中的一个重要概念,指的是访问共享资源(如变量、数据结构、文件等)的一段代码。临界区中的代码需要被保护,以确保同一时间只有一个线程可以执行这段代码,从而避免竞态条件(Race Condition)和数据不一致的问题。
要保证临界区代码段 原子操作
死锁问题
程序如果在在中间出现问题, unlock
就不会执行到了,会被阻塞加锁那里, 会导致死锁.
c++11 提供了lock_guard
与unique_lock
解决死锁问题
lock_guard-保证所有线程都能释放锁
lock_guard
:lock_guard<std::mutex> lock(mutex锁名);
构造会自动上锁,析构会自动释放。拷贝构造与赋值重载函数被删除掉了,类比智能指针scoped_ptr
#include <iostream>
#include <thread>
#include <list>
#include <mutex>
using namespace std;/*
c++ thread 模拟车站三个窗口买票的 程序
*/int ticketCount = 10; // 车站有10张车票,由三个窗口一起卖票std::mutex mtx; // 全局的一把互斥锁// 模拟卖票的线程函数
void sellTicket(int index)
{//mtx.lock();//这里是不行的, 导致 一个线程 把票全卖了while (ticketCount > 0){//mtx.lock();{lock_guard<std::mutex> lock(mtx); // 出作用域自动析构解锁if (ticketCount > 0){cout << "窗口:" << index << "卖出第:" << 11 - ticketCount << "张票!" << endl;ticketCount--;}}//mtx.unlock();std::this_thread::sleep_for(std::chrono::milliseconds(100));}}int main()
{list<std::thread> tlist; // 使用双向链表for (int i = 1; i <= 3; ++i){tlist.push_back(std::thread(sellTicket, i));}for (auto& t : tlist){t.join();}cout << "所有窗口卖票结束!" << endl;return 0;
}
unique_lock
unique_lock
:构造会自动上锁,析构会自动释放。拷贝构造与赋值重载函数被删除掉了,提供了带右值引用版本的,类比智能指针unique_ptr
#include <iostream>
#include <thread>
#include <list>
#include <mutex>
using namespace std;/*
c++ thread 模拟车站三个窗口买票的 程序
*/int ticketCount = 10; // 车站有10张车票,由三个窗口一起卖票std::mutex mtx; // 全局的一把互斥锁// 模拟卖票的线程函数
void sellTicket(int index)
{//mtx.lock();//这里是不行的, 导致 一个线程 把票全卖了while (ticketCount > 0){//mtx.lock();{//lock_guard<std::mutex> lock(mtx); // 出作用域自动析构解锁unique_lock<std::mutex> lock(mtx); // 出作用域自动析构解锁if (ticketCount > 0){cout << "窗口:" << index << "卖出第:" << 11 - ticketCount << "张票!" << endl;ticketCount--;}}//mtx.unlock();std::this_thread::sleep_for(std::chrono::milliseconds(100));}}int main()
{list<std::thread> tlist; // 使用双向链表for (int i = 1; i <= 3; ++i){tlist.push_back(std::thread(sellTicket, i));}for (auto& t : tlist){t.join();}cout << "所有窗口卖票结束!" << endl;return 0;
}
3. 线程间同步通信-生产者消费者模型
多线程编程两个问题:
-
线程间的互斥
竞态条件导致=>对临界区代码段=>其原子操作=>添加互斥锁
mutex
、轻量级的无锁实现(CAS)Linux下
strace ./a.out
(程序启动的跟踪打印的命令)会发现 c++代码底层使用的还是 linux的pthread_mutex_t
-
线程间的同步通信
线程间不通信的话,每个线程受CPU的调度,没有任何执行上的顺序可言,线程1和线程2是根据CPU调度算法来的,两个线程都有可能先运行,是不确定的,线程间的运行顺序是不确定的
通信就是:
- 线程1和线程2一起运行,线程2要做的事情必须先依赖于线程1完成部分的事情,然后线程1告诉线程2这部分东西做好了,线程2就可以继续向下执行了
- 或者是线程1接下来要做某些操作,这些操作需要线程2把另外一部分事情做完,然后通知一下线程1它做完了,然后线程1才能做这些操作。
生产者-消费者线程模型
注意: C++ STL中所有的容器都不是线程安全的,都需要进行封装。在这个例子中把queue
封装成了Queue
错误案例
先看一个非常简便的 例子 : 这个例子 问题很多:生产者空,消费者还要消费
生产者和消费者 不交流
#include <iostream>
#include <thread>
#include <list>
#include <mutex>
#include <queue>
using namespace std;std::mutex mtx;// 生产者生产一个物品,通知消费者消费一个;消费完了,消费者再通知生产者继续生产物品
class Queue
{
public://生产物品void put(int val){lock_guard<std::mutex> suo(mtx);que.push(val);cout << "生产者 生产:" << val <<"号物品" << endl;}// 消费物品int get(){lock_guard<std::mutex> suo(mtx);int val = que.front();que.pop();cout << "消费者 消费:" << val <<"号物品" << endl;return val;}private:queue<int> que;};void producer(Queue* que) // 生产者线程
{for (int i = 1; i <= 10; ++i){que->put(i);std::this_thread::sleep_for(std::chrono::milliseconds(100));}
}void consumer(Queue* que) // 消费者线程
{for (int i = 1; i <= 10; ++i){que->get();std::this_thread::sleep_for(std::chrono::milliseconds(100));}
}int main()
{Queue que; // 两个线程共享的队列std::thread t1(producer, &que);std::thread t2(consumer, &que);t1.join();t2.join();return 0;
}
正确案例-- 使用条件变量
信号量 (这是指c中的信号量)虽然可以做, 但是 条件变量(c里有, 但是c++11提供了更好的封装)更好用
条件变量: – 有两种
-
功能:条件变量允许线程等待某个条件为真时才继续执行,通常与互斥锁结合使用。
-
使用条件变量需要包含头文件
#include <condition_variable>
-
.wait() 必须传入 unique_lock 类型的 加锁, 不能是别的类型
-
std::condition_variable
:- 必须与
std::unique_lock<std::mutex>
配合使用。 - 性能更高,但灵活性较低。
- 必须与
-
std::condition_variable_any
:- 可以与任何满足基本要求的锁类型配合使用。
- 灵活性更高,但性能较低。
-
核心操作:
条件变量的核心操作 wait():使当前线程进入等待状态,直到被通知。通常与谓词(Predicate)一起使用,以避免虚假唤醒。notify_one():唤醒一个等待的线程。notify_all():唤醒所有等待的线程。
-
条件变量, 虽然没有明确的 状态术语, 不过 一般来说 :
wait()
使线程进入等待状态,在此期间它会释放关联的互斥锁并挂起执行,直到收到notify_one()
或notify_all()
的通知后被唤醒,并尝试重新获取锁;而阻塞状态指的是线程在尝试获取锁时发现锁已被其他线程持有,因此无法继续执行,必须等待锁释放后才能继续运行。
通知后, 其它线程得到该通知,就会从等待状态(条件达成)=>阻塞状态,之后获取互斥锁继续执行----有点乱, 大概明白就行
#include <iostream>
#include <thread>
#include <list>
#include <mutex>
#include <queue>
#include <condition_variable>
using namespace std;std::mutex mtx; // 定义互斥锁, 线程间的 同步操作
std::condition_variable cv; // 定义条件变量 线程间的 通信操作// 生产者生产一个物品,通知消费者消费一个;消费完了,消费者再通知生产者继续生产物品
class Queue
{
public://生产物品void put(int val){/*lock_guard<std::mutex> guard(mtx);*/// que不为空,生产者应该通知消费者去消费unique_lock<std::mutex> lck(mtx);while (!que.empty()) {// que不为空,生产者应该通知消费者去消费, 消费者消费完了,生产者再继续生产// 生产者线程进入#1等待状态,并且#2把mtx互斥锁释放掉cv.wait(lck); //传入一个互斥锁,当前线程挂起,处于等待状态,并且释放当前锁}que.push(val);cv.notify_all(); // 通知其他线程 , 生产了物品, 可以消费//其它线程得到该通知,就会从等待状态变为阻塞状态,之后获取互斥锁继续执行cout << "生产者 生产:" << val <<"号物品" << endl;}// 消费物品int get(){/*lock_guard<std::mutex> guard(mtx);*/unique_lock<std::mutex> lck(mtx);//消费者线程发现que是空的,通知生产者线程先生产物品//消费者线程进入等待状态,并且把mtx互斥锁释放掉while (que.empty()){cv.wait(lck); // 循环等待, 并释放互斥锁}int val = que.front();que.pop();cv.notify_all();cout << "消费者 消费:" << val <<"号物品" << endl;return val;}private:queue<int> que;
};void producer(Queue* que) // 生产者线程
{for (int i = 1; i <= 10; ++i){que->put(i);std::this_thread::sleep_for(std::chrono::milliseconds(100));}
}void consumer(Queue* que) // 消费者线程
{for (int i = 1; i <= 10; ++i){que->get();std::this_thread::sleep_for(std::chrono::milliseconds(100));}
}int main()
{Queue que; // 两个线程共享的队列std::thread t1(producer, &que);std::thread t2(consumer, &que);t1.join();t2.join();return 0;
}
**特别注意: **
- 锁的位置对多线程程序的正确性至关重要。
- 锁必须覆盖整个操作,包括对共享资源的访问和条件变量的等待。
- 如果将锁放到
while
循环内部,会导致竞争条件和输出乱序。 - 正确的锁位置应覆盖整个操作,确保线程间的同步和共享资源的安全性。
2.4 再谈lock_guard和unique_lock
主要是为了再讲讲lock_guard和unique_lock, condition_varivable, wait,notify_all
std::mutex mtx;mtx.lock();mtx.unlock();
普通互斥锁的缺点: 有可能中间走掉了,导致没有unlock()
,不安全
lock_guard<std::mutex> guard(mtx);
可以出了作用域 自动析构
lock_guard
不可能用在函数参数传递或者返回过程中,因为拷贝构造和赋值函数都被删除了
只能用在简单的加锁解锁临界区代码段的互斥操作中,出作用域析构自动释放锁
unique_lock<std::mutex> lck(mtx);
– 搭配条件变量使用
unique_lock : 不仅可以使用在简单的加锁解锁临界区代码段的互斥操作中,还能用在函数调用过程中,因为其虽然删除了拷贝构造和赋值函数,但是提供了带右值引用版本的
总结: lock_guard 适用于 无线程通信的 情况
unique_lock 搭配 condition_variable使用, 可以 wait和notify_all,notify_one 结合使用
/*
通知在cv上等待的线程,条件成立了,起来干活了!
其它在cv上等待的线程,收到通知,
从等待状态 -> 到阻塞状态(不能直接运行)
只有当前线程释放锁了,其他线程获取互斥锁了,线程才能继续往下执行
*/
cv.notify_all();
2.5 基于CAS操作的atomic原子类型
本节重点是: atomic 这个模板类定义的原子类型变量 , 这种类型的变量的操作都将是 是原子操作
这意味着:
对 std::atomic
类型的变量进行的所有操作(例如读取、写入、增减等)都是不可中断的,不会被其他线程的操作打断。
-
窗口卖票的代码, count+±- 操作, 是线程不安全的, 之前 使用了 互斥锁 保证线程安全
-
互斥锁是比较重的,临界区代码复杂时可以使用;但现在我们只是做一个加加减减的操作,还是需要一些轻量级原子操作的操作
解决办法:使用CAS保证上面加加减减操作的原子特性就足够了,CAS也叫做无锁操作 -
什么是 原子操作?
原子操作(Atomic Operation)指的是一系列操作在执行期间不可被中断的操作,它是一个不可分割的操作单元。也就是说,当一个原子操作开始执行时,它要么完全执行成功,要么完全不执行,不会被其他线程中断或打断,保证了操作的完整性和一致性。
-
什么是CAS?
CAS(Compare-And-Swap) 是一种原子操作,用于在多线程环境中实现无锁的线程同步。其核心概念是:比较和交换(exchange/swap),即在执行操作时,先比较目标变量的当前值与预期值是否相等,如果相等则将目标变量的值替换为新值,否则不做任何修改。
CAS(Compare-And-Swap) 是一种原子操作,用于实现无锁编程(Lock-Free Programming)。它是一种硬件级别的同步机制,通常用于多线程环境中,确保对共享数据的操作是原子的(即不可分割的)。 -
CAS 就是无锁操作, 面试的 无锁队列啥的, 就是CAS
-
std::atomic 这是一个模板, 所以需要搭配<>使用
-
volatile
是 C 和 C++ 中的一个关键字,用于告诉编译器某个变量的值可能会在程序外部发生变化,因此编译器在优化时不能对该变量进行某些假设(禁止优化),必须每次直接从内存中读取其值。 防止缓存 -
原子操作: 实际就是 所有的读写 不会被其他线程中断
-
std::atomic_bool
是 C++11 标准引入的一个别名类型,它是std::atomic<bool>
的简写或别名。std::atomic_bool
在 C++ 中是一个 类型别名,通常用于让代码更加简洁,便于编写与使用。
头文件:#include <atomic>
代码示例:
#include <iostream>
#include <thread>
#include <list>
#include <mutex>
#include <queue>
#include <condition_variable>
using namespace std;volatile std::atomic_bool isReady = false; // atomic的特例化版本//volatile std::atomic_int ticketCount = 0; // volatile 防止多线程变量 进行缓存
volatile std::atomic<int> ticketCount = 0; // 一般这么用 void task()
{while (!isReady)std::this_thread::yield(); // 让当前线程 自愿地让出 CPU 的控制权(cpu时间片),允许同一线程的其他线程获得执行机会for (int i = 0; i < 100; ++i)ticketCount++;
}int main()
{list<std::thread> tlist;for (int i = 0; i < 10; ++i)tlist.push_back(std::thread(task));std::this_thread::sleep_for(std::chrono::seconds(1));cout << "ticketCount:" << ticketCount << endl; // ticketCount:0isReady = true; // 这里再执行++for (auto& t : tlist)t.join();cout << "ticketCount:" << ticketCount << endl; // ticketCount:1000return 0;
}
2.6额外补充:CAS的成员方法—课里没讲
CAS(Compare-And-Swap) 是一种原子操作,用于在并发编程中保证线程安全,常用于实现无锁数据结构和算法。它的基本原理是比较某个变量的当前值是否等于预期值,如果相等,则交换为新的值。这个操作是原子的,因此能有效避免线程竞争。
在 C++ 中,CAS 通常通过标准库中的 std::atomic
来实现,它提供了原子操作的接口,包括 compare_exchange_weak
和 compare_exchange_strong
两种形式。
基本语法和使用示例
假设你有一个 std::atomic<int>
类型的变量,并且你想使用 CAS 来修改它。
#include <iostream>
#include <atomic>int main() {std::atomic<int> x(5); // 创建一个原子整数,初始值为5int expected = 5; // 预期值为5int new_value = 10; // 新值为10// 使用 compare_exchange_strong 执行 CAS 操作bool success = x.compare_exchange_strong(expected, new_value);if (success) {std::cout << "CAS 操作成功,新的值是: " << x.load() << std::endl;} else {std::cout << "CAS 操作失败,当前值是: " << x.load() << std::endl;}return 0;
}
CAS 操作解释:
x.compare_exchange_strong(expected, new_value)
:- 比较
x
的当前值和expected
的值。如果它们相等,x
将被更新为new_value
,并返回true
。 - 如果
x
的当前值不等于expected
,则expected
将被更新为x
的当前值,操作失败,返回false
。 compare_exchange_strong
是一种强操作,它会进行多个重试,直到操作成功或遇到某些停止条件。
- 比较
expected
变量在 CAS 操作后,可能会被修改为x
当前的值,因此你需要在失败时查看expected
的新值。
compare_exchange_weak
vs compare_exchange_strong
:
compare_exchange_strong
:确保操作尽可能成功地执行,可能会进行多次重试,直到操作成功。compare_exchange_weak
:不保证每次都能执行,可能会失败并返回false
,适用于无锁算法中可能需要退让的情形。
代码解析:
- 成功的 CAS 操作:
expected
和x
的值相等时,x
会被更新为新值,success
为true
。 - 失败的 CAS 操作:
expected
和x
的值不相等时,expected
会更新为x
的当前值,success
为false
。
注意事项:
- ABA 问题:CAS 操作可能会出现 ABA 问题,指的是一个值从
A
改为B
,然后又变回A
,这时 CAS 可能会误认为值没有变化。解决方法之一是使用带有版本号的 CAS 或者增加标记。 - 高并发问题:CAS 操作可能导致忙等(自旋),如果操作频繁失败,可能会消耗大量的 CPU 资源。此时,可以考虑结合自旋锁或引入等待机制。
总结:
- CAS 是一种高效的原子操作,用于并发编程中无锁算法的实现。
- 在 C++ 中,
std::atomic
提供了 CAS 操作的接口,如compare_exchange_strong
和compare_exchange_weak
。 - 使用 CAS 时,需要注意 ABA 问题和自旋的效率问题。