c++11 知识点汇总
一、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:0
isReady = true; // 这里再执行++
for (auto& t : tlist)
t.join();
cout << "ticketCount:" << ticketCount << endl; // ticketCount:1000
return 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); // 创建一个原子整数,初始值为5
int expected = 5; // 预期值为5
int 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 问题和自旋的效率问题。