【Linux】线程同步 -- 条件变量 | 生产者消费者模型 | 自旋锁 |读写锁

  • 初识生产者消费者模型
  • 同步
    • 条件变量
    • 初步使用
  • POSIX信号量
  • 其他常见的各种锁
    • 自旋锁
    • 读写锁

初识生产者消费者模型

举一个例子:
【Linux】线程同步 -- 条件变量 | 生产者消费者模型 | 自旋锁 |读写锁_第1张图片
学生去超市消费的时候,与厂家生产的时候,两者互不相冲突。
生产的过程与消费的过程 – 解耦
临时的保存产品的场所(超时) – 缓冲区

【Linux】线程同步 -- 条件变量 | 生产者消费者模型 | 自旋锁 |读写锁_第2张图片
模型总结“321”原则:

  • 3种关系:生产者和生产者(互斥),消费者和消费者(互斥),生产者和消费者(互斥[保证共享资源的安全性] && 同步) – 产品(数据)
  • 2种角色:生产者线程,消费者线程
  • 1个交易场所:一段特定结构的缓冲区

只要我们想写生产消费模型,我们本质工作其实就是维护321原则!

特点:

  1. 生产线程和消费线程进行解耦
  2. 支持生产和消费的一段时间的忙闲不均的问题
  3. 提高效率

举例:
我们以前:main函数获取用户输入,然后调用fun函数,fun函数执行,打印结果。这是一个串行的流程,现在我们对应生产者消费者模型:
【Linux】线程同步 -- 条件变量 | 生产者消费者模型 | 自旋锁 |读写锁_第3张图片


同步

条件变量

【Linux】线程同步 -- 条件变量 | 生产者消费者模型 | 自旋锁 |读写锁_第4张图片
由于上述的原因,我们不能仅靠锁来去实现线程互斥。

什么是条件变量:

  • 当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。(比如我们抢票,如果目前还没有票,那么每个人都是做同样的动作:加锁→判断票是否为0→解锁,在还没有放票之前,每个人都什么也做不了)
  • 例如一个线程访问队列时,发现队列为空,它只能等待,只到其它线程将一个节点添加到队列中。这种情况就需要用到条件变量。

感性认识条件变量:
模拟一个面试流程:
1.假如我们一堆人都要到一家公司面试,但是这个公司的HR组织的不行,每次面试都是一堆人举手来竞争面试机会,都说要自己先来,而HR也是看谁顺眼就让他进去面试 – 一份共享资源被多线程并发式的竞争
2.同样是面试,而HR组织的好,说要面试必须要到等待区等待,按照排队顺序来:(这个等待区就是一个条件变量)
【Linux】线程同步 -- 条件变量 | 生产者消费者模型 | 自旋锁 |读写锁_第5张图片

当条件不满足的时候,我们线程必须去某些定义好的条件变量上进行等待!

初步使用

#include 
#include 
#include 
#include 

int tickets = 1000;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

void *start_routine(void *args)
{
    std::string name = static_cast<const char *>(args);
    while (true)
    {
        pthread_mutex_lock(&mutex);
        pthread_cond_wait(&cond, &mutex); // 为什么要有mutex,后面就说
        // 判断暂时省略
        std::cout << name << " -> " << tickets << std::endl;
        tickets--;
        pthread_mutex_unlock(&mutex);
    }
}

int main()
{
    // 通过条件变量控制线程的执行
    pthread_t t[5];
    for (int i = 0; i < 5; i++)
    {
        char *name = new char[64];
        snprintf(name, 64, "thread %d", i + 1);
        pthread_create(t+i, nullptr, start_routine, name);
    }

    while (true)
    {
        sleep(1);
        pthread_cond_signal(&cond);
        //pthread_cond_broadcast(&cond);
        std::cout << "main thread wakeup one thread..." << std::endl;
    }
    for (int i = 0; i < 5; i++)
    {
        pthread_join(t[i], nullptr);
    }

    return 0;
}

【Linux】线程同步 -- 条件变量 | 生产者消费者模型 | 自旋锁 |读写锁_第6张图片
我们可以发现进程按照一定的顺序在执行

【Linux】线程同步 -- 条件变量 | 生产者消费者模型 | 自旋锁 |读写锁_第7张图片


POSIX信号量

POSIX信号量和SystemV信号量作用相同,都是用于同步操作,达到无冲突的访问共享资源目的。 但POSIX可以用于线程间同步。

初始化信号量

#include 
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); //P()

发布信号量

功能:发布信号量,表示资源使用完毕,可以归还资源了。将信号量值加1int sem_post(sem_t *sem);//V()

其他常见的各种锁

  • 悲观锁:在每次取数据时,总是担心数据会被其他线程修改,所以会在取数据前先加锁(读锁,写锁,行锁等),当其他线程想要访问数据时,被阻塞挂起。
  • 乐观锁:每次取数据时候,总是乐观的认为数据不会被其他线程修改,因此不上锁。但是在更新数据前,会判断其他数据在更新前有没有对数据进行修改。主要采用两种方式:版本号机制和CAS操作。
  • CAS操作:当需要更新数据时,判断当前内存值和之前取得的值是否相等。如果相等则用新值更新。若不等则失败,失败则重试,一般是一个自旋的过程,即不断重试。

自旋锁

场景一:你喊你的朋友一起去吃饭,你的朋友说等他一会就来,在等的期间你每隔一会就打电话问他来没来 – 自旋
场景二:你喊你的朋友一起去吃饭,你的朋友说等他一会就来,你不想就只在原地等,你走路去学校网吧上网,走路去的过程叫做挂起,在网吧上网叫做等待,你朋友跟你打电话说他已经来了,然后你回学校这叫做唤醒。-- 挂起等待

是什么决定了最终的等待方式?
等待的时间问题。已经被申请的临界资源决定了其他线程要等待,一个成功申请临界资源的线程在临界区内要待多少时间,这个时间长短就决定了我们使用哪种方式:(阻塞等待/自旋)。时间的长短如何定义?都是用分别测试效果。

【Linux】线程同步 -- 条件变量 | 生产者消费者模型 | 自旋锁 |读写锁_第8张图片

int pthread_spin_lock(pthread_spinlock_t *lock);
int pthread_spin_trylock(pthread_spinlock_t *lock);

【Linux】线程同步 -- 条件变量 | 生产者消费者模型 | 自旋锁 |读写锁_第9张图片

int pthread_spin_destroy(pthread_spinlock_t *lock);
int pthread_spin_init(pthread_spinlock_t *lock, int pshared);

【Linux】线程同步 -- 条件变量 | 生产者消费者模型 | 自旋锁 |读写锁_第10张图片

int pthread_spin_unlock(pthread_spinlock_t *lock);

读写锁

在编写多线程的时候,有一种情况是十分常见的。那就是,有些公共数据修改的机会比较少。相比较改写,它们读的机会反而高的多。通常而言,在读的过程中,往往伴随着查找的操作,中间耗时很长。给这种代码段加锁,会极大地降低我们程序的效率。那么有没有一种方法,可以专门处理这种多读少写的情况呢? 有,那就是读写锁。

场景:比如在以前学校的黑板报,画黑板报的为写者,写者与写者之间是互斥关系(你正在写字,他不能把你写的字擦了画画),而观看的同学是读者,读者与读者之间不存在什么关系(黑板报画好了,我们都是一起看的,不存在先来就先看其余的蒙着眼睛不准看)。

读者写者模型与生产者消费者模型本质区别是:消费者会拿走数据而读者不会。
读者写者:写者之间 – 互斥 、读写者 – 互斥/同步 、读者之间 – 没关系
读者写者模型适用场景:一次发布,很长时间不做修改,大部分时间都是被读取的(大部分时间都是被读写,少量的时间在进行写入)
【Linux】线程同步 -- 条件变量 | 生产者消费者模型 | 自旋锁 |读写锁_第11张图片

设置读写优先

int pthread_rwlockattr_setkind_np(pthread_rwlockattr_t *attr, int pref);
pref 共有 3 种选择
PTHREAD_RWLOCK_PREFER_READER_NP (默认设置) 读者优先,可能会导致写者饥饿情况
PTHREAD_RWLOCK_PREFER_WRITER_NP 写者优先,目前有 BUG,导致表现行为和
PTHREAD_RWLOCK_PREFER_READER_NP 一致
PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP 写者优先,但写者不能递归加锁

【Linux】线程同步 -- 条件变量 | 生产者消费者模型 | 自旋锁 |读写锁_第12张图片

int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
              const pthread_rwlockattr_t *restrict attr);

读者加锁:
【Linux】线程同步 -- 条件变量 | 生产者消费者模型 | 自旋锁 |读写锁_第13张图片

int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);

写者加锁:
【Linux】线程同步 -- 条件变量 | 生产者消费者模型 | 自旋锁 |读写锁_第14张图片

int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);

统一解锁:
【Linux】线程同步 -- 条件变量 | 生产者消费者模型 | 自旋锁 |读写锁_第15张图片

int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

【Linux】线程同步 -- 条件变量 | 生产者消费者模型 | 自旋锁 |读写锁_第16张图片


如有错误或者不清楚的地方欢迎私信或者评论指出

你可能感兴趣的:(Linux系统基础,linux,运维,线程同步,条件变量,生产者消费者,后端)