线程相关
线程是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/%E8%BF%9B%E7%A8%8B%E8%B0%83%E5%BA%A6%E7%AD%96%E7%95%A5/]
多线程伴随的是并发问题,在不同线程访问同一个资源的时候,会发生不一致的情况,为了数据的同步,必须使用锁
按照锁的种类分类,可以分为以下几种
自旋锁其实是获取锁失败时阻塞等待,比较消耗CPU时间,所以比较适合占用锁比较少时间的场景
在c++里实现自旋
链接: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);
}
也就是满足某个条件时才继续 std::condition_variable,需要搭配std::unique_lock来使用 std::condition_variable_any,不限于std::unique_lock
原子操作即是进行过程中不能被中断的操作,针对某个值的原子操作在被进行的过程中,CPU绝不会再去进行其他的针对该值的操作。为了实现这样的严谨性,原子操作仅会由一个独立的CPU指令代表和完成。原子操作是无锁的,常常直接通过CPU指令直接实现。事实上,其它同步技术的实现常常依赖于原子操作。
std::atomic,原子变量。但不保证原子性不是由锁来实现的 std::atomic_flag,原子性的标记变量,保证其原子性的实现是无锁的
上面的自旋锁就是用原子变量实现的
c++里有自动管理锁的管理器
在c++里实现读写锁
#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;
}
这是两个抽象的概念,这两种形式的锁一般用于数据库访问和更新
乐观锁在适合在多读的场景,如果在多写下,乐观锁不断失败重试反而性能降低
乐观锁虽然在业务层无锁,但是在底层更新的时候也会用到锁,只不过在底层的锁粒度更小,开销也更小 总的来说,乐观锁是先读后锁,悲观锁是先锁后读