线程相关
什么是线程
线程是CPU调度的基本单位,在早期,单核CPU上,一个CPU在某个事件执行一个线程,这就没有多线程的说法,后来单核CPU采取时间片轮转调度,不同的线程分配一定的时间,并在时间结束后切换线程,也就是CPU频繁切换线程,让我们看起来多个任务真的在“同时”进行,其实只是单核在不停切换,到了多核CPU才实现了真正的多线程,异步进行,每个核心都可以处理一个线程
线程占有的资源
寄存器 栈 程序计数器 状态
线程数量设置多少合适
一般一个线程占用1M的内存,理论上,一个2G的内存,可以开辟2048个线程,但是线程多也不意味着高并发,工作线程数量主要由CPU核心数和处理器能力决定,一般一个核心一个线程最佳,如果是CPU密集型,会设置线程数N 1或者N 2,N是核心数,如果是IO密集型,设置为2N。当然还有个公式
最佳线程数目 = ((线程等待时间 线程CPU时间)/线程CPU时间 )* CPU数目
线程池设计
代码可见[https://github.com/progschj/ThreadPool/blob/master/ThreadPool.h]
主要是工作线程std::vector< std::thread > workers,一个vector,每个元素都是一个循环等待任务,然后是任务队列std::queue< std::function<void()> > tasks
线程池如果核心线程满了,就加入任务队列,如果队列也满了,那这一般会有集中策略
- 丢弃任务,同时抛出异常
- 丢弃任务,不抛出异常
- 删除队列队头的任务,然后加入
- 直接运行该任务
线程调度策略
(可见)[https://www.openrad.ink/2021/08/31/进程调度策略/]
什么是锁,为什么要锁
多线程伴随的是并发问题,在不同线程访问同一个资源的时候,会发生不一致的情况,为了数据的同步,必须使用锁
锁的种类
按照锁的种类分类,可以分为以下几种
- 互斥锁
- 自旋锁
- 条件变量
1. 对于互斥锁在C 标准库里有的:
- std::mutex,可以阻塞式等锁(lock())也可以非阻塞式上锁(try_lock()),lock可以同时锁定几个互斥量,try_lock如果锁定失败会直接返回
- std::timed_mutex,互斥锁的加时版,如果在一段时间内(try_lock_for())或是在某个时间之前(try_lock_until())获取锁成功则成功上锁
- std::recursive_mutex,递归互斥锁,在互斥锁的基础上允许持有锁的线程多次通过lock()或者try_lock()获取锁,而std::mutex的拥有者不能继续请求上锁
- std::recursive_timed_mutex,递归互斥锁加时版
- std::shared_mutex,共享互斥锁,允许多个线程共享锁(lock_shared()系列),但只有一个线程能够持有互斥锁(lock()系列),也就是一般所说的读写锁
- std::shared_timed_mutex,共享互斥锁的加时版本
2. 自旋锁:
自旋锁其实是获取锁失败时阻塞等待,比较消耗CPU时间,所以比较适合占用锁比较少时间的场景
在c 里实现自旋
代码语言:javascript复制链接:https://www.nowcoder.com/questionTerminal/554355eea5aa44d697a3a4bc99795207
来源:牛客网
#include <atomic>
#include <iostream>
std::atomic_flag lock = ATOMIC_FLAG_INIT; // 这是个标准库里的宏
void spin_lock_output(int n) {
// 上锁
while(lock.test_and_set(std::memory_order_acquire))
; // 忙等自旋
std::cout << "output from thread " << n << std::endl;
// 解锁
lock.clear(std::memory_order_release);
}
3. 条件锁
也就是满足某个条件时才继续 std::condition_variable,需要搭配std::unique_lock来使用 std::condition_variable_any,不限于std::unique_lock
原子变量
原子操作即是进行过程中不能被中断的操作,针对某个值的原子操作在被进行的过程中,CPU绝不会再去进行其他的针对该值的操作。为了实现这样的严谨性,原子操作仅会由一个独立的CPU指令代表和完成。原子操作是无锁的,常常直接通过CPU指令直接实现。事实上,其它同步技术的实现常常依赖于原子操作。
std::atomic,原子变量。但不保证原子性不是由锁来实现的 std::atomic_flag,原子性的标记变量,保证其原子性的实现是无锁的
上面的自旋锁就是用原子变量实现的
RAII式锁管理器
c 里有自动管理锁的管理器
- std::lock_guard,自动上锁,退出作用域自动解锁,但是提前解锁做不到
- std::unique_lock,独享所有权的锁管理器,除基础RAII功能之外还能移交所有权(此时不解锁),(解锁后)上锁和(提前)解锁
- std::shared_lock,配合共享锁使用的锁管理器
再深入了解读写锁
在c 里实现读写锁
代码语言:javascript复制#include <iostream>
//std::unique_lock
#include <mutex>
#include <shared_mutex>
#include <thread>
class ThreadSafeCounter {
public:
ThreadSafeCounter() = default;
// 多个线程/读者能同时读计数器的值。
unsigned int get() const {
std::shared_lock<std::shared_mutex> lock(mutex_);
return value_;
}
// 只有一个线程/写者能增加/写线程的值。
void increment() {
std::unique_lock<std::shared_mutex> lock(mutex_);
value_ ;
}
// 只有一个线程/写者能重置/写线程的值。
void reset() {
std::unique_lock<std::shared_mutex> lock(mutex_);
value_ = 0;
}
private:
mutable std::shared_mutex mutex_;
unsigned int value_ = 0;
};
int main() {
ThreadSafeCounter counter;
auto increment_and_print = [&counter]() {
for (int i = 0; i < 3; i ) {
counter.increment();
std::cout << std::this_thread::get_id() << 't' << counter.get() << std::endl;
}
};
std::thread thread1(increment_and_print);
std::thread thread2(increment_and_print);
thread1.join();
thread2.join();
system("pause");
return 0;
}
悲观锁和乐观锁
这是两个抽象的概念,这两种形式的锁一般用于数据库访问和更新
- 悲观锁是指当你访问和修改数据前,需要对数据加锁,可能是数据库中的行锁,表锁,读写锁.这样通过加锁能够很好的保证数据一致性,但是锁会带来开销
- 乐观锁是指当你访问数据时可以直接获取到,但当你需要更新时,你需要验证版本号或者上一次访问的时间戳,来确保没有人在你之前更新过数据,如果发现版本号和时间戳改变了,就重新获取该数值,再一次更新;如果发现版本号和时间戳没改变,则直接更新.
乐观锁在适合在多读的场景,如果在多写下,乐观锁不断失败重试反而性能降低
乐观锁虽然在业务层无锁,但是在底层更新的时候也会用到锁,只不过在底层的锁粒度更小,开销也更小 总的来说,乐观锁是先读后锁,悲观锁是先锁后读