Linux线程同步
条件变量
当一个线程互斥地访问某个变量时,它可能在其它线程改变状态之前,什么也做不了。
例如一个线程访问队列时,发现队列为空,它只能等待,直到其它线程将一个节点添加到队列中。这种情况就需要用到条件变量。
pthread库中,条件变量的类型是pthread_cond_t
同步概念与竞态条件
同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步
竞态条件:因为时序问题而导致程序异常,我们称之为竞态条件。在线程场景下,这种问题也不难理解
条件变量函数初始化
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
参数:
cond:要初始化的条件变量
attr:NULL
销毁
int pthread_cond_destroy(pthread_cond_t *cond)
等待条件满足
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
参数:
cond:要在这个条件变量上等待
mutex:互斥量,后面详细解释,暂时理解为锁
唤醒等待
//将所有因为cond等待的线程唤醒
int pthread_cond_broadcast(pthread_cond_t *cond);
//将正在因为cond等待的某个线程唤醒
int pthread_cond_signal(pthread_cond_t *cond);
简单案例:
#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
pthread_cond_t cond;
pthread_mutex_t lock;
void* act(void*arg)
{
while (1)
{
//等待r2发送信号活动
pthread_cond_wait(&cond, &lock);
printf("%s acts\n", (char*)arg);
}
}
void* send(void*arg)
{
while (1)
{
//1s发送一次信号让r1活动
sleep(1);
pthread_cond_signal(&cond);
printf("%s sends\n", (char*)arg);
}
}
int main()
{
//初始化锁和条件变量
pthread_mutex_init(&lock, NULL);
pthread_cond_init(&cond, NULL);
pthread_t r1, r2;
//创建线程
pthread_create(&r1, NULL, act, "thread 1");
pthread_create(&r2, NULL, send, "thread2");
//主线程等待
pthread_join(r1, NULL);
pthread_join(r2, NULL);
//销毁锁和条件变量
pthread_mutex_destroy(&lock);
pthread_cond_destroy(&cond);
return 0;
}
效果:
可以看到上述线程r1被r2控制了,可不可以实现多个线程被一个线程控制呢?
#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
pthread_cond_t cond;
pthread_mutex_t lock;
void* act(void*arg)
{
while (1)
{
//等待r2发送信号活动
pthread_cond_wait(&cond, &lock);
printf("%s acts\n", (char*)arg);
}
}
void* send(void*arg)
{
while (1)
{
//1s发送一次信号让r1活动
sleep(1);
pthread_cond_signal(&cond);
printf("%s sends\n", (char*)arg);
}
}
int main()
{
//初始化锁和条件变量
pthread_mutex_init(&lock, NULL);
pthread_cond_init(&cond, NULL);
pthread_t r1, r2, r3, r4, r5, r6;
//创建线程
pthread_create(&r1, NULL, act, "thread 1");
pthread_create(&r2, NULL, send, "thread 2");
pthread_create(&r3, NULL, act, "thread 3");
pthread_create(&r4, NULL, act, "thread 4");
pthread_create(&r5, NULL, act, "thread 5");
pthread_create(&r6, NULL, act, "thread 6");
//主线程等待
pthread_join(r1, NULL);
pthread_join(r2, NULL);
pthread_join(r3, NULL);
pthread_join(r4, NULL);
pthread_join(r5, NULL);
pthread_join(r6, NULL);
//销毁锁和条件变量
pthread_mutex_destroy(&lock);
pthread_cond_destroy(&cond);
return 0;
}
可以看到线程13456是按照一定的顺序活动的,于是推知线程同步是按照一定顺序等待的。
为什么pthread_ cond_ wait 需要互斥量?
条件等待是线程间同步的一种手段,如果只有一个线程条件不满足,一直等下去都不会满足,所以必须要有一个线程通过某些操作,改变共享变量,使原先不满足的条件变得满足,并且通知等待在条件变量上的线程。
条件不会无缘无故地突然变得满足了,必然会牵扯到共享数据的变化。所以一定要用互斥锁来保护。没有互斥锁就无法安全的获取和修改共享数据。
按照上面的说法,我们设计出如下的代码:先上锁,发现条件不满足,解锁,然后等待在条件变量上不就行了,如下代码:
// 错误的设计 pthread_mutex_lock(&mutex); while (condition_is_false) { pthread_mutex_unlock(&mutex); //解锁之后,等待之前,条件可能已经满足,信号已经发出,但是该信号可能被错过 pthread_cond_wait(&cond); pthread_mutex_lock(&mutex); } pthread_mutex_unlock(&mutex);
由于解锁和等待不是原子操作。调用解锁之后,pthread_ cond_ wait之前,如果已经有其他线程获取到互斥量,摒弃条件满足,发送了信号,那么pthread_ cond_ wait将错过这个信号,可能会导致线程永远阻塞在这个pthread_ cond_ wait。所以解锁和等待必须是一个原子操作。
int pthread_cond_wait(pthread_cond_ t *cond,pthread_mutex_ t * mutex); 进入该函数后,会去看条件量等于0不?等于,就把互斥量变成1,直到cond_ wait返回,把条件量改成1,把互斥量恢复成原样
条件变量使用规范
等待条件代码
pthread_mutex_lock(&mutex); while (条件为假) pthread_cond_wait(cond, mutex); 修改条件 pthread_mutex_unlock(&mutex);
给条件发送信号代码
pthread_mutex_lock(&mutex); 设置条件为真 pthread_cond_signal(cond); pthread_mutex_unlock(&mutex);
生产者消费者模型
321原则(便于记忆)
3:3种关系:生产者与生产者(互斥关系) 生产者与消费者(同步关系) 消费者与消费者(互斥关系)
2:2种角色:生产者与消费者
1:1个交易场所
为何要使用生产者消费者模型
生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给生产者和消费者解耦的。
生产者消费者模型优点
- 解耦
- 支持并发
- 支持忙闲不均
基于BlockingQueue的生产者消费者模型
BlockingQueue 在多线程编程中阻塞队列(Blocking Queue)是一种常用于实现生产者和消费者模型的数据结构。其与普通的队列区别在于,当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入了元素;当队列满时,往队列里存放元素的操作也会被阻塞,直到有元素被从队列中取出(以上的操作都是基于不同的线程来说的,线程在对阻塞队列进程操作时会被阻塞)
C++ queue模拟阻塞队列的生产消费模型 代码:
生产快,消费慢
BlockQueue.hpp(声明和定义)
#ifndef __Block_Queue_H__
#define __Block_Queue_H__
#include<iostream>
#include<queue>
#include<unistd.h>
#include<pthread.h>
class BlockQueue{
private:
std::queue<int> q;//阻塞队列
size_t cap;
pthread_mutex_t lock;
pthread_cond_t c_cond;//消费者通知
pthread_cond_t p_cond;//生产者通知
public:
//接口实现
bool IsFull()
{
return cap <= q.size();
}
bool IsEmpty()
{
return q.empty();
}
void LockQueue()
{
pthread_mutex_lock(&lock);
}
void UnlockQueue()
{
pthread_mutex_unlock(&lock);
}
void WakeUpProductor()
{
std::cout << "Productor Wake up" << std::endl;
pthread_cond_signal(&p_cond);
}
void WakeUpConsumer()
{
std::cout << "Consumer Wake up" << std::endl;
pthread_cond_signal(&c_cond);
}
void ProductorWait()
{
//该线程进入等待时,要对资源进行解锁,函数返回时,又将该锁返回给该线程,所以才要传lock
std::cout<< "Productor wait" << std::endl;
pthread_cond_wait(&p_cond, &lock);
}
void ConsumerWait()
{
std::cout<< "Consumer wait" << std::endl;
pthread_cond_wait(&c_cond, &lock);
}
public:
BlockQueue()
{
cap = 5;
pthread_mutex_init(&lock, nullptr);
pthread_cond_init(&c_cond, nullptr);
pthread_cond_init(&p_cond, nullptr);
}
void Put(int data)
{
//先上锁,只允许一个生产者进行生产
LockQueue();
//当队列中数据满了时,就不生产了,唤醒消费者进行消费,并开始等待
while (IsFull())//因为wait可能会失败,所以用while来保证生产者不会在队列满的情况下再放数据
{
WakeUpConsumer();
ProductorWait();
}
q.push(data);
UnlockQueue();
}
void Get(int& data)
{
//只允许一个消费者进行消费
LockQueue();
while (IsEmpty())//当队列中没数据时,消费者就需要唤醒生产者来生产数据,并进入等待
{
WakeUpProductor();
ConsumerWait();
}
data = q.front();
std::cout << "consumer:get a data:" << data << std::endl;
q.pop();
UnlockQueue();
}
~BlockQueue()
{
pthread_mutex_destroy(&lock);
pthread_cond_destroy(&c_cond);
pthread_cond_destroy(&p_cond);
}
};
#endif
main.cpp
#include "BlockQueue.hpp"
using namespace std;
void* consumer_run(void*bq)
{
BlockQueue*q = (BlockQueue*)bq;
int data = 0;
while (1)
{
sleep(1);
q->Get(data);
cout << "get a data: " << data << endl;
}
}
void* productor_run(void*bq)
{
BlockQueue*q = (BlockQueue*)bq;
int count = 0;
while (1)
{
count++;
q->Put(count);
count %= 5;
cout << "productor puts a data:" << count << endl;
}
}
int main()
{
pthread_t c,p;
BlockQueue*bq = new BlockQueue;
pthread_create(&c, nullptr, consumer_run, (void*)bq);
pthread_create(&p, nullptr, productor_run, (void*) bq);
pthread_join(p, nullptr);
pthread_join(c, nullptr);
return 0;
}
效果(生产快消费慢):
![生产消费者模型](https://i-blog.csdnimg.cn/blog_migrate/3af747582c9fae11c31c3f5e63401aa0.gif)
多线程版本:
main.cc
#include "BlockQueue.hpp"
pthread_mutex_t p_mutex;
void* consume(void*arg)
{
BlockQueue* bq = (BlockQueue*)arg;
while (1)
{
int data = 0;
bq->Get(data);
printf("thread:%ld consumer get a data: %d\n", pthread_self(), data);
sleep(1);
}
}
void* produce(void*arg)
{
BlockQueue* bq = (BlockQueue*)arg;
int i = 0;
while (1)
{
pthread_mutex_lock(&p_mutex);
bq->Put(i);
printf("thread: %ld producer put a data:%d\n", pthread_self(), i);
++i;
pthread_mutex_unlock(&p_mutex);
}
}
int main()
{
pthread_mutex_init(&p_mutex, NULL);
auto bq = new BlockQueue();
std::vector<pthread_t> p_v;
std::vector<pthread_t> c_v;
for (int i = 0; i < 4; ++i)
{
pthread_t t;
p_v.push_back(t);
pthread_create(&t, NULL, produce, (void*)bq);
}
for (int i = 0; i < 4; ++i)
{
pthread_t t;
c_v.push_back(t);
pthread_create(&t, NULL, consume, (void*)bq);
}
for (int i = 0; i < 4; ++i)
{
int ret = 0;
ret = pthread_join(p_v[i], NULL);
if (!ret)
{
perror("join error");
printf("p[%d]\n", i);
return 1;
}
ret = pthread_join(c_v[i], NULL);
if (!ret)
{
perror("join error");
printf("c[%d]\n", i);
return 1;
}
}
delete bq;
return 0;
}
POSIX信号量
POSIX信号量和SystemV信号量作用相同,都是用于同步操作,达到无冲突的访问共享资源目的。 但POSIX可以用于线程间同步。
拿出之前的一个图进行解释:
![image-20211018210319276](https://i-blog.csdnimg.cn/blog_migrate/bb5dd8ac9298156d14865c839f7a23f4.png)
我们可以对临界区进行分块,而信号量就是一个计算可用临界区的块数,每有一个线程来申请临界区资源,如果当前计数器count不为0,则可以分配出去,此时count–;如果count为0,当前线程就要进入等待队列。而每归还一块临界区资源,count++。
所以,信号量本质是一个计数器,描述临界资源有效个数的计数器。
上述的关于申请资源、count–的一系列操作称为P操作,而归还资源、count++的一系列操作称为C操作。
因为每个线程都可以访问信号量,所以信号量也是临界资源,因此P、C操作也是原子的
上面讲述了信号量的大概作用,现在我们更加深入理解一下。
我们可以理解为信号量sem也是一个结构体,里面有计数器count计算当前可用的临界区个数,锁lock对临界区上锁,队列head、tail用来将等待线程入队出队等
初始化信号量
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
参数:
pshared:0表示线程间共享,非零表示进程间共享
value:信号量初始值
销毁信号量
int sem_destroy(sem_t *sem);
等待信号量
功能:等待信号量,会将信号量的值减1
int sem_wait(sem_t *sem);
发布信号量
功能:发布信号量,表示资源使用完毕,可以归还资源了。将信号量值加1。
int sem_post(sem_t *sem);
基于环形队列的生产消费模型
上一节生产者-消费者的例子是基于queue的,其空间可以动态分配,现在基于固定大小的环形队列重写这个程序
(POSIX信号量): 基于环形队列的生产消费模型
![image-20211018211142199](https://i-blog.csdnimg.cn/blog_migrate/34c41f75d648a4fd4adae1a79af72ed0.png)
- 环形队列采用数组模拟,用模运算来模拟环状特性
- 环形结构起始状态和结束状态都是一样的,不好判断为空或者为满,所以可以通过加计数器或者标记位来
判断满或者空。另外也可以预留一个空的位置,作为满的状态- 但是我们现在有信号量这个计数器,就很简单的进行多线程间的同步过程
RingQueue.hpp
#ifndef __RingQueue_H__
#define __RingQueue_H__
#include<iostream>
#include<vector>
#include<pthread.h>
#include<unistd.h>
#include <semaphore.h>
class RingQueue{
private:
std::vector<int> v;
sem_t sem_blank;//环形队列中的空格子数
sem_t sem_data;//环形队列中的数据个数
int max_cap;//容量
int p_index;//生产者下标
int c_index;//消费者下标
void P(sem_t& s)
{
//将信号量减少;如果没有申请到,则挂起等待
sem_wait(&s);
}
void V(sem_t& s)
{
//将信号量增加
sem_post(&s);
}
public:
RingQueue(int cap = 10)
:max_cap(cap)
,p_index(0)
,c_index(0)
{
sem_init(&sem_blank, 0, max_cap);
sem_init(&sem_data, 0, 0);
v.resize(max_cap);
}
//consumer获取数据
void Get(int& out)
{
//申请数据资源
P(sem_data);
out = v[c_index];
//消费者下标索引加1
c_index++;
//防止越界
c_index %= max_cap;
//添加空白块
V(sem_blank);
}
//productor生产数据
void Put(int& in)
{
//申请空白资源
P(sem_blank);
v[p_index] = in;
//生产者下标索引加1
p_index++;
//防止越界
p_index %= max_cap;
//添加数据
V(sem_data);
}
~RingQueue()
{
sem_destroy(&sem_blank);
sem_destroy(&sem_data);
}
};
#endif
main.cpp
#include"RingQueue.hpp"
void* consumer(void* arg)
{
RingQueue* rq = (RingQueue*)arg;
int data;
while (true)
{
rq->Get(data);
std::cout<<"consumer gets a data:"<<data<<std::endl;
sleep(1);
}
}
void* productor(void* arg)
{
RingQueue* rq = (RingQueue*)arg;
int data = 100;
while (true)
{
if (data == 110)
data = 100;
rq->Put(data);
std::cout<<"productor puts a data:"<<data<<std::endl;
data++;
}
}
int main()
{
pthread_t c, p;
RingQueue*rq = new RingQueue;
pthread_create(&c, nullptr, consumer, rq);
pthread_create(&p, nullptr, productor, rq);
pthread_join(c, nullptr);
pthread_join(p, nullptr);
delete rq;
return 0;
}
我们可以看到,在一瞬间,生产者就将数据填满了队列,然后生产者可用的信号量为0,因此停止生产,而消费者消费速度比较慢,所以每当消费者消费一个数据,生产者就立马申请空白空间,生产数据填满队列,然后再等消费者消费了,又继续填满…
一定是生产者先生产,消费者再消费。假设消费者先进入队列拿数据,因为消费者可申请的资源为0,所以会申请失败,从而进入等待队列。而同时生产者又开始生产,一瞬间就将数据生产完毕,等待消费者消费。
用信号量实现的生产消费者模型就不用考虑将队列上锁与手动进行等待,因为当生产者与消费者对应的信号量为0时,自动就会被挂起,不用担心生产者还没生产,消费者就拿走数据,或消费者还没消费,生产者又重新生产的情况。
读者写者问题
读写锁
在编写多线程的时候,有一种情况是十分常见的。那就是,有些公共数据修改的机会比较少。相比较改写,它们读的机会反而高的多。通常而言,在读的过程中,往往伴随着查找的操作,中间耗时很长。给这种代码段加锁,会极大地降低我们程序的效率。那么有没有一种方法,可以专门处理这种多读少写的情况呢? 有,那就是读写锁。
读者写者模型也可以用“321”原则来描述:
3:3种关系:读者与读者(共享),读者与写者(互斥与同步),写者与写者(互斥)
2:2种角色:读者与写者
1:1个场所
** 注意:写独占,读共享,写锁优先级高 **
ThreadPool.hpp
#pragma once
#include <iostream>
#include <queue>
#include <pthread.h>
#include <stdio.h>
#include <assert.h>
#include <unistd.h>
#define NUM 10 //任务数量
#define threads_num 5
using std::cout;
using std::endl;
//计算平方
class Task{
private:
int _base;
public:
Task(int n = 0)
:_base(n)
{}
void Run()
{
cout << "thread["<< pthread_self() << "]: pow(_base, 2):" << pow(_base, 2) << endl;
}
~Task()
{}
};
class ThreadPool{
private:
std::queue<Task*> _q;
std::vector<pthread_t> threads;
size_t _num;//线程数量
//用来保证线程安全
pthread_mutex_t _lock;
pthread_cond_t c_cond;
pthread_cond_t p_cond;
void LockQueue()
{
pthread_mutex_lock(&_lock);
}
void UnlockQueue()
{
pthread_mutex_unlock(&_lock);
}
void ConsumerWait()
{
pthread_cond_wait(&c_cond, &_lock);
}
void ConsumerWakeUp()
{
pthread_cond_signal(&c_cond);
}
void ProducerWait()
{
pthread_cond_wait(&p_cond, &_lock);
}
void ProducerWakeUp()
{
pthread_cond_signal(&p_cond);
}
bool IsEmpty()
{
return _q.empty();
}
bool IsFull()
{
return _q.size() == _num;
}
static void* thread_run(void* arg)
{
ThreadPool* tp = (ThreadPool*)arg;
while (1)
{
tp->LockQueue();
while (tp->IsEmpty())
{
tp->ConsumerWait();
}
Task* t = tp->Get();//取出任务
tp->ProducerWakeUp();
tp->UnlockQueue();
//执行任务
t->Run();
}
}
void ThreadPoolInit()
{
pthread_mutex_init(&_lock, NULL);
pthread_cond_init(&c_cond, NULL);
pthread_cond_init(&p_cond, NULL);
threads.resize(threads_num);
// 创建线程
for (size_t i = 0; i < threads_num; ++i)
{
pthread_create(&threads[i], NULL, thread_run, (void*)this);
pthread_detach(threads[i]);
}
}
public:
ThreadPool(size_t num = NUM)
:_num(num)
{
ThreadPoolInit();
}
//取出任务
Task* Get()
{
Task* t = _q.front();
_q.pop();
return t;
}
//放任务
void Put(Task& t)
{
while (IsFull())
{
ProducerWait();
}
_q.push(&t);
cout << "put a task..." << endl;
ConsumerWakeUp();
}
~ThreadPool()
{
pthread_mutex_destroy(&_lock);
pthread_cond_destroy(&c_cond);
pthread_cond_destroy(&p_cond);
}
};
效果:
可以看到,每个线程按照一定的顺序获取任务并执行任务,前提是用pthread_cond_signal唤醒线程。
如果是用pthread_cond_broadcast唤醒,就会唤醒所有线程,然后所有线程又去抢一个任务,会引发惊群效应。如果有多个任务需要被执行(涉及到高低水位线),就可以使用该函数。
线程池存在的价值
1.有任务,立马有线程进行服务,省掉了线程创建的时间
2.有效防止server中线程过多,导致系统过载的问题
线程池vs进程池
1.线程池占用的资源更少,但是健壮性(鲁棒性)不强
2.进程池占用的资源更多,但是健壮性(鲁棒性)很强
线程安全的单例模式
什么是单例模式
单例模式是一种 “经典的, 常用的, 常考的” 设计模式
什么是设计模式
IT行业这么火, 涌入的人很多. 俗话说林子大了啥鸟都有. 大佬和菜鸡们两极分化的越来越严重. 为了让菜鸡们不太拖大佬的后腿, 于是大佬们针对一些经典的常见的场景, 给定了一些对应的解决方案, 这个就是设计模式
单例模式的特点
某些类, 只应该具有一个对象(实例), 就称之为单例.
例如一个男人只能有一个媳妇.
在很多服务器开发场景中, 经常需要让服务器加载很多的数据 (上百G) 到内存中. 此时往往要用一个单例的类来管理这些数据
饿汉实现方式和懒汉实现方式
洗碗的例子
吃完饭, 立刻洗碗, 这种就是饿汉方式. 因为下一顿吃的时候可以立刻拿着碗就能吃饭.
吃完饭, 先把碗放下, 然后下一顿饭用到这个碗了再洗碗, 就是懒汉方式
懒汉方式最核心的思想是 “延时加载”. 从而能够优化服务器的启动速度
饿汉方式实现单例模式
template <typename T>
class Singleton {
static T data;//一开始就创建了T对象
public:
static T* GetInstance()
{
return &data;
}
private:
//构造函数私有化,删除拷贝构造,防止被构造或拷贝
Singleton();
Singleton(const Sinleton&) = delete;
};
只要通过 Singleton 这个包装类来使用 T 对象, 则一个进程中只有一个 T 对象的实例,这样会使进程的加载速度减慢
懒汉方式实现单例模式
template <typename T>
class Singleton {
static T* inst;//创建的只是一个T类型的指针,没有创建对象
public:
static T* GetInstance()
{
if (inst == NULL)
{
inst = new T();
}
return inst;
}
};
虽然不会减慢进程的加载,但存在一个严重的问题, 线程不安全.
第一次调用 GetInstance 的时候, 如果两个线程同时调用, 同时进入if判断,可能会创建出两份 T 对象的实例.因此要在进入GetInstance函数后先上锁,再创建inst。
但是后续再次调用, inst就不为空了,就不会再创建inst了。
懒汉方式实现单例模式(线程安全版本)
// 懒汉模式, 线程安全
template <typename T>
class Singleton {
volatile static T* inst; // 需要设置 volatile 关键字, 否则可能被编译器优化.
static std::mutex lock;
public:
static T* GetInstance()
{
if (inst == NULL) // 双重判定空指针, 降低锁冲突的概率, 提高性能.
{
lock.lock();
if (inst == NULL) // 使用互斥锁, 保证多线程情况下也只调用一次 new.
{
inst = new T();
}
lock.unlock();
}
return inst;
}
};
注意事项:
- 加锁解锁的位置
- 双重 if 判定, 避免不必要的锁竞争
- volatile关键字防止过度优化
STL,智能指针和线程安全
STL中的容器是否是线程安全的?
不是.
原因是, STL 的设计初衷是将性能挖掘到极致, 而一旦涉及到加锁保证线程安全, 会对性能造成巨大的影响.
而且对于不同的容器, 加锁方式的不同, 性能可能也不同(例如hash表的锁表和锁桶).
因此 STL 默认不是线程安全. 如果需要在多线程环境下使用, 往往需要调用者自行保证线程安全.
智能指针是否是线程安全的?
对于 unique_ptr, 由于只是在当前代码块范围内生效, 因此不涉及线程安全问题.
对于 shared_ptr, 多个对象需要共用一个引用计数变量, 所以会存在线程安全问题. 但是标准库实现的时候考虑到了这个问题, 基于原子操作(CAS)的方式保证 shared_ptr 能够高效, 原子的操作引用计数
其他常见的各种锁
- 悲观锁:在每次取数据时,总是担心数据会被其他线程修改,所以会在取数据前先加锁(读锁,写锁,行锁等),当其他线程想要访问数据时,被阻塞挂起。
- 乐观锁:每次取数据时候,总是乐观的认为数据不会被其他线程修改,因此不上锁。但是在更新数据前,会判断其他数据在更新前有没有对数据进行修改。主要采用两种方式:版本号机制和CAS操作。
- CAS操作:当需要更新数据时,判断当前内存值和之前取得的值是否相等。如果相等则用新值更新。若不等则失败,失败则重试,一般是一个自旋的过程,即不断重试。
- 自旋锁,公平锁,非公平锁?
自旋锁
自旋锁的理解
如果我们想要预约一个篮球馆打球,我们打电话给篮球馆,篮球馆说今天已经有人预订了,要等到明天才能预订,所以我们今天就不会再选择去打球了,而是等到明天再预约。在等待的这段时间内可以选择去学习。而如果篮球馆的回复是,现在有人在打了,但是马上就打完了,于是我们就选择过一会再问问,如果还在打,我们又等一会再问问,直到球馆可以被预约。在这等待的一小会中,我们就无法利用短短的时间去学习,否则可能在学习的过程中,场地又被别人预约了。这样是为了提高效率。
自旋锁类型pthread_spinlock_t
自旋锁初始化
//成功返回0,错误返回错误码
int pthread_spin_init(pthread_spinlock_t *lock, int pshared);
上锁
//成功返回0,错误返回错误码
int pthread_spin_lock(pthread_spinlock_t *lock);
自旋锁的销毁
//成功返回0,错误返回错误码
int pthread_spin_destroy(pthread_spinlock_t *lock);
因为当前占有临界区内的线程的占有时间特别短,无需挂起,让当前申请临界区的线程处于自旋状态,不断去检测锁的状态,而其中,自旋锁为我们提供上述功能
再用之前对应的汇编伪代码(修改)理解一下(al寄存器的内容为1表示当前线程成功申请了锁)
![image-20211021234323295](https://i-blog.csdnimg.cn/blog_migrate/f65d18127d1ec1d73fa744bfd7587859.png)
lock:
while (true)
{
movb $0, %al
xchgb %al, mutex
if (al寄存器的内容 > 0)
{
return 0;
}
}