Linux中线程的同步与互斥、生产者消费模型和读者写者问题、死锁问题

2023-05-16

线程的同步与互斥

线程是一个存在进程中的一个执行控制流,因为线程没有进程的独立性,在进程内部线程的大部分资源数据都是共享的,所以在使用的过程中就需要考虑到线程的安全和数据的可靠。不能因为线程之间资源的竞争而导致数据发生错乱,也不能因为有些线程因为调度器长时间没有调度从而导致饥饿问题。所以在线程中也有了同步与互斥,这里用 “也” 是因为进程中也有同步与互斥,今天来了解线程中的同步与互斥。

互斥量

我们为什么要有互斥量

首先,一个进程中的多个线程因为同处于一个虚拟地址空间中,所以相互之间大部分数据是共享的,从而在线程竞争中出现数据错乱的情况,我们来举例子来看一下

首先我们采用多个线程去竞争着去修改count的数据,我们定义初始的count为100,让其减少为0就可以了,我们看看会有什么结果。

如果对线程不了解或者相关的创建API点击线程概念、线程创建、等待、退出

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>

#define num 3 //我们用3个线程

int count = 100;

void* MyFun(void* arg) //线程入口函数
{
    int j = (int)arg;
    while (ticket >= 0)
    {
        usleep(1000);
        printf("thread %d count %d\n", j, count);
        count--;
    }
    return NULL;
}

int main()
{
    pthread_t th[num];
    int i = 0;
    for (;i < num; ++i)
    {
        // 创建线程
        int ret = pthread_create(&th[i], NULL, MyFun, (void*)i);
        if (ret != 0)
        {
            perror("pthread_create error\n");
            exit(1);
        }
    }

    i = 0;
    for (; i < num; ++i)
    {
        // 等待线程,释放资源
        pthread_join(th[i], NULL);
    }
    return 0;
}

我们先看看结果
线程不安全
看到这个结果,如果仔细看代码就会发现,我们写的是代码count到0就退出,但是为什么会出现到-2呢?这里我们就解释一下

首先线程之间资源共享,所以随着cpu的调度,如果你是多核计算机,线程是可以同时访问一个共享资源,这个时候就会发生:
内存数据到寄存器到cpu
这只是其中的一种情况,还有可能在内存读到寄存器中的时候被切换掉等,就出现了数据错误的情况。
所以我们要加上互斥锁,让互斥的访问临界资源。
我们修改我们的代码:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>

#define num 3

// 互斥锁
pthread_mutex_t mutxe;

int ticket = 100;

void* MyFun(void* arg)
{
    long j = (long)arg;
    while (1)
    {
        usleep(1000);
        pthread_mutex_lock(&mutxe); // 加锁
        if (ticket > 0)
        {
            printf("thread %lu ticket %d\n", j, ticket);
            ticket--;
            pthread_mutex_unlock(&mutxe); // 解锁
        }
        else
        {
            pthread_mutex_unlock(&mutxe); // 解锁
            break;
        }
    }
    return NULL;
}

int main()
{
    pthread_mutex_init(&mutxe, NULL); //初始化锁
    pthread_t th[num];
    long i = 0;
    for (;i < num; ++i)
    {
        // 创建锁
        int ret = pthread_create(&th[i], NULL, MyFun, (void*)i);
        if (ret != 0)
        {
            perror("pthread_create error\n");
            exit(1);
        }
    }

    i = 0;
    for (; i < num; ++i)
    {
        // 等待释放线程
        pthread_join(th[i], NULL);
    }
    pthread_mutex_destroy(&mutxe);
    return 0;
}

我们看结果
加锁

线程中互斥锁API

创建一个互斥锁,一般为全局量

// 当我们初始化为PTHREAD_MUTEX_INITIALIZER的时候互斥量不需要销毁
ptread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
// 初始化互斥量
int pthread_mutex_init(pthread_mutex_t* restrict mutex,\
 const pthread_mutexattr_t* restrict attr);
 // mutex 为互斥量;
 // attr NULL;
// 销毁互斥量
int pthread_mutex_destroy(pthread_mutex_t* mutex);
// mutex 互斥量
// 加锁解锁
int pthread_mutex_lock(pthread_mutex_t* mutex);
int pthread_mutex_unlock(pthread_mutex_t* mutex);

条件变量(同步)

首先我们还是了解一下为什么需要同步,条件变量。
为了防止竞争造成数据错乱,所以加上互斥锁,但是当两个线程同时访问时候,一个线程的访问速度快,一个访问慢,所以当一个线程不停的申请锁释放锁,但是里面的状态没有得到另一个线程改变,那么就会产生资源的浪费。

还有可能因为优先级问题,一个线程的优先级高,临界资源不能满足它的需求,它不停的申请锁释放锁,但是得不到满足,但是其它线程不等访问临界资源,从而造成饥饿问题。

问了解决这个问题,所以我们就有了一个同步的条件,来保证公平的访问,也可以说减少性能上的开销。

我们先看一下相关API

先需要有个同步变量和互斥锁

pthread_cond_t cond; // 同步变量,也一般在全局区
// 初始化条件变量
int pthread_cond_init(pthread_cond_t* restrict cond, const \
pthread_condattr_t* resttict attr);
// cond 初始化的条件变量
// attr NULL
// 销毁
int pthread_cond_destroy(pthread_cond_t* cond);
// cond 销毁的条件变量
// 等待条件
int pthread_cond_wait(pthread_cond_t* restrict cond, \
pthread_mutex_t* restrict mutex);
// cond 在cond条件变量上等待
// mutex 互斥量(一会解释)
// 唤醒等待
int pthread_cond_signal(pthread_cond_t* cond);
// cond 唤醒的条件变量

在这里我们要注意,在条件变量中的等待,为什么互斥变量,是因为 wait 方法所做的功能,pthread_cond_wait 函数在等待,要做三件事,先是解锁再等待等到信号后还要加锁,为什么一个函数要干这么多事情呢?

这里因为竞态条件产生的问题,必须要把这三个步骤合到一块为一个原子操作。
我们来画图分析
竞态条件

为了运用条件变量时候,不容易出错,最好运用

等待条件规范(自己觉得可靠)

pthread_mutex_lock(&mutex);
while (判断条件)//这里用while是因为,当收到异常信号的时候可以重新判断
    pthread_cond_wait(cond, mutex)
修改条件
pthread_mutex_unlock(&mutex);

给条件发送代码

pthread_mutex_lock(&mutex)
设置条件
pthread_cond_signal(cond);
pthread_mutex_unlock(&mutex);

生产者消费者模型

生产者消费者模型是线程中同步互斥的很典型的例子。
其中有三种关系:
1)生产者与生产者:互斥
2)消费者与消费者:互斥
3)生产者与消费者:同步互斥
生产者与消费者模型是基于一个场景,就是生产者在每次生产一个数据必须要放在一个仓库中,而消费者消费必须去仓库中拿数据,这样我们就会产生一些问题,比如,在生产者生产好数据去仓库放东西的时候,消费者是不能进去的,同行消费者取的时候,生产者也不能进去。当仓库为空的时候消费者就得等生产者放数据,仓库满了就必须等消费者取数据。所以为了满足上面的需求,我们就用同步与互斥来进行让他们有效的进行。

我们用代码模拟实现。我们用一个链表来模拟一个仓库。

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>

// 互斥量
pthread_mutex_t mutex;

// 条件
pthread_cond_t cond;

// 用与记录次数
int count = 0;

// 用一个链表来当作仓库
typedef struct ListNode
{
    struct ListNode* next;
    int data;
}ListNode;

// 带头节点
ListNode head;

ListNode* CreateNode(int value)
{
    ListNode* new_node = (ListNode*)malloc(sizeof(ListNode));
    new_node->next = NULL;
    new_node->data = value;
    return new_node;
}

void Init(ListNode* head)
{
    head->data = 0;
    head->next = NULL;
}

void Push(ListNode* head, int value)
{
    if (head == NULL)
    {
        return;
    }
    ListNode* node = CreateNode(value);
    ListNode* nex = head->next;
    head->next = node;
    node->next = nex;
}

void Pop(ListNode* head, int *top)
{
    if (head == NULL)
    {
        return;
    }
    if (head->next == NULL)
    {
        return;
    }
    *top = head->next->data;
    ListNode* node = head->next;
    head->next = node->next;
    free(node);
}

void* Producer(void* arg)
{
    (void)arg;
    while (1)
    {
        // 加锁
        pthread_mutex_lock(&mutex);
        Push(&head, count);
        printf("Producer %d \n", head.next->data);
        ++count;
        // 产生一个数据,就要唤醒等待的线程
        pthread_cond_signal(&cond);
        // 解锁
        pthread_mutex_unlock(&mutex);
        sleep(1);
    }
    return NULL;
}

void* Consumer(void* arg)
{
    (void)arg;
    while (1)
    {
        int value = -1;
        pthread_mutex_lock(&mutex);

        // 这里用while是因为有可能被信号打断,
        // 当再次返回时候,就可以重新判断
        // 也有可能被其它异常信号唤醒
        while (head.next == NULL)
        {
            // 没有数据,就要进行
            // 解锁
            // 等待
            // 加锁
            pthread_cond_wait(&cond, &mutex);
        }
        Pop(&head, &value);
        printf("consumer %d\n",value);

        pthread_mutex_unlock(&mutex);
        usleep(100000);
    }
    return NULL;
}

int main()
{
    Init(&head);

    // 初始化
    pthread_mutex_init(&mutex, NULL);
    pthread_cond_init(&cond, NULL);

    // 创建线程
    pthread_t producer, consumer;
    pthread_create(&producer, NULL, Producer, NULL);
    pthread_create(&consumer, NULL, Consumer, NULL);

    // 等待线程
    pthread_join(producer, NULL);
    pthread_join(consumer, NULL);

    // 销毁同步与互斥量
    pthread_cond_destroy(&cond);
    pthread_mutex_destroy(&mutex);
    return 0;
}

读者写者问题

上面的生产者消费者模型是两个线程都在修改临界资源,那么我们的读者写者是一个线程修改多个线程读不修改的访问,这里我们要做到一下几点:
1)读者与读者之间是可以同时访问临界资源
2)写者与写者只能有一个,当然这里只有一个写者
3)读者和写者同时访问临界资源,写者优先
我们来看相关的函数的API

//定义一个读写锁变量
pthread_rwlock_t rwlock;

// 初始化 变量
int pthread_rwlock_init(pthread_rwlock_t* restrict rwlock,\
phthread_rwlockattr_t* restrict attr);

// 销毁
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

// 加读锁和加写锁
int pthread_rwlock_rdlock(pthread_rwlock_t* rwlock); // 读锁
int pthread_rwlock_wrlock(pthread_rwlock_t* rwlock); // 写锁

// 解锁
int pthread_rwlock_unlock(pthread_rwlock_t* rwlock);

对照上面的场景和和相关API我们来写代码

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <stdlib.h>

#define num 5

pthread_rwlock_t lock; //读写锁

int count = 0; //临界资源

void *Reader(void* arg)
{
    long i = (long)arg;
    while (1)
    {
        pthread_rwlock_rdlock(&lock); //加读锁
        printf("Reader %lu count %d\n", i, count);
        pthread_rwlock_unlock(&lock); // 解锁
        usleep(500000);
    }
    return NULL;
}

void *Writer(void* arg)
{
    long i = (long)arg;
    while (1)
    {
        pthread_rwlock_wrlock(&lock); //写锁
        ++count;
        printf("Writer %lu count %d\n", i, count);
        pthread_rwlock_unlock(&lock); //解锁
        sleep(1);
    }
    return NULL;
}

int main()
{
    // 初始化
    pthread_rwlock_init(&lock, NULL);

    // 创建线程
    pthread_t th[num];
    long i = 0;
    for (; i < num-1; ++i)
    {
        pthread_create(&th[i], NULL, Reader, (void*)i);
    }
    pthread_create(&th[num], NULL, Writer, (void*)1);

    // 等待线程,释放资源
    i = 0;
    for (; i < num; ++i)
    {
        pthread_join(th[i], NULL);
    }
    pthread_rwlock_destroy(&lock);
    return 0;
}

死锁问题

死锁的产生是因为我们为了保证线程或者进程访问临界资源时候保证真确的数据。但是又引来了新的问题,就是死锁。

死锁,在我们写代码的过程中,加锁就必须要解锁,不然就产生死锁,还有在一个线程中我们尝试对一个锁进行两次加锁操作也会产生死锁。

还有锁的数量和资源的数量相同,而每个线程需要两个资源,当同时对资源进行加锁,就会产生经典的哲学家进餐问题。这也是一种死锁。
我们总结一下:
死锁的形成
- 竞争不可抢占资源引起死锁
- 竞争可消耗资源引起死锁
- 进程推进顺序不当引起死锁

产生死锁的四个必要条件

  • 互斥条件
  • 请求和保持条件
  • 不可抢占条件
  • 循环等待条件

那么我们怎么解决死锁问题呢?
预防死锁必须破坏四个必要条件之一
但是不能破坏互斥条件

如有错误,请多多指导!谢谢!

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Linux中线程的同步与互斥、生产者消费模型和读者写者问题、死锁问题 的相关文章

  • Kotlin 基础知识汇总(知识与实践相结合)

    2个月的时间总算把 Kotlin 的基础知识写完了 xff0c 下面咱们看看具体内容 xff1a 学习 Kotlin 的必要性 Kotlin 初学者 为什么要学Kotlin Kotlin 初学者 打牢基础的重要性 运行环境 Kotlin 初
  • HashMap的产生与原理

    一 HashMap的诞生 1 1 数组 数组 xff1a 一片物理上连续的大小确定的储存空间 好处 xff1a 根据下标快速的查找和修改里面的内容 缺点 xff1a 大小确定 xff0c 无法修改 添加新的元素或者删除元素比较麻烦 数组的静
  • Android 数据存储(一)-文件存储

    目录 一 数据存储概念 二 应用程序专属文件存储 2 1 访问持久文件 2 2 将数据存储到文件 2 3 从文件中读取数据 2 4 查看文件列表 2 5 删除文件 三 缓存文件 cache目录下 3 1 创建缓存文件 3 2 删除文件 四
  • 回顾2021,展望2022 | 年终总结

    你付出多少努力 xff0c 就必有多少收获 一 回顾 2021 2021 年输出109篇文章 xff0c 收获 xff1a 博客专家认证 Android领域新星创作者认证 博客之星Top50 同时也在问答模块解决了部分小伙伴的问题 xff0
  • Android 数据存储(二)-SP VS DataStore VS MMKV

    一 SharedPreferences 不同于文件的存储方式 xff0c 如果要保存的键值集合相对较小 xff0c 则应使用SharedReferences API SharedReferences对象指向一个包含键值对的文件 xff0c
  • Jetpack DataStore 你总要了解一下吧?

    目录 一 DataStore 介绍 Preferences DataStore 和 Proto DataStore 二 Preferences DataStore 2 1 添加依赖 2 2 使用 Preferences DataStore
  • Android 数据存储(四)-Room

    目录 一 概述 1 1 描述 1 2 主要部件 二 创建 Room 2 1 添加依赖项 2 2 创建数据实体 2 2 1 设置 tableName or name 属性 2 2 2 设置主键 2 2 3 忽略字段 2 3 创建数据访问对象
  • Android 抛弃IMEI改用ANDROID_ID

    介绍 之前一直使用IMEI作为唯一标识符 xff0c 缺点就是需要权限 xff1a Android 10以前 xff0c 需要READ PHONE STATE权限 Android 10限制 xff0c 需要READ PRIVILEGED P
  • Android 单例模式必知必会

    目录 一 概念 1 1 单例类 1 2 优缺点 1 2 1 优点 1 2 2 缺点 二 创建单例模式的方法 2 1 饿汉式 2 2 懒汉式 2 2 1 懒汉式 非线程安全 2 2 2 懒汉式 线程安全 2 3 双重检验锁 2 4 静态内部类
  • 使用netinstall重置MIKROTIK RouterOS

    相信查看本文的读者手头应该有一台Mikrotik的路由产品 xff0c 本文中出现的RouterOS是指笔者拥有的一台家庭版路由器 非Routeros玩家可以忽略本文 下图就是笔者正在使用的一个Routeros路由器 我们在给Routero
  • BigDecimal 简单使用

    目录 为什么使用BigDecimal 解决方案 构造方法 类型转换 double 转 BigDecimal BigDecimal 转 String BigDecimal 转 double int long等 加减乘除取余 divide 舍入
  • Android Framework 启动流程必知必会

    课前预习 在了解启动流程之前先了解一下下面两个概念 1 子进程与父进程的区别 1 除了文件锁以外 其他的锁都会被继承 2 各自的进程ID和父进程ID不同 3 子进程的未决告警被清除 4 子进程的未决信号集设置为空集 2 什么是写时拷贝 co
  • ADB 操作命令及用法

    ADB 操作命令及用法 文章目录 ADB 操作命令及用法一 ADB是什么 xff1f 二 ADB有什么作用 xff1f 三 ADB命令语法单一设备 模拟器连接多个设备 模拟器连接4 1 基本命令4 1 1 查看adb的版本信息4 1 2 启
  • 研究生生涯的一些经验和感悟

    研究生生涯的一些经验和感悟 引言 写这篇博客前 xff0c 我不禁要感慨一下互联网分享所带来的好处 我这里讲的分享主要是指知识 技术和个人思想的分享 网络新闻媒体更多是传播一些资讯 xff0c 而这些资讯一般不涉及深入的技术 xff0c 深
  • PyCharm上传本地项目到GitLab - MacOS版

    登录GitLab 创建一个项目 填写项目名称 xff0c 选择显示级别 复制GitLab的这个项目地址 xff0c 后面会在PyCharm里面用到 PyCharm操作 从PyCharm打开本地项目 xff0c 然后创建本地代码仓库 xff0
  • 【Python】使用Scrapy 网络爬虫框架Demo

    安装 使用PyCharm安装 xff0c 进入到PyCharm gt Preferences gt Project Interpreter xff0c 点击加号 查询框输入 Scrapy xff0c 点击 Install Package 使
  • Docker+Selenium Grid运行UI自动化

    简介 使用Selenium Grid可以分布式运行UI自动化测试 xff0c 可以同时启动多个不同的浏览器 xff0c 也可以同时启动同一个浏览器的多个session 这里使用Docker Compose来同时启动不同浏览器的容器和Sele
  • python-生成HTMLTestRunner测试报告

    一 安装HTMLTestRunner HTMLTestRunner 是 Python 标准库的 unittest 模块的一个扩展 xff0c 它可以生成 HTML的 测试报告 首先要下 HTMLTestRunner py 文件 xff0c
  • Jenkins发送测试报告邮件

    简介 总结怎么使用Jenkins执行自动化测试后发送测试报告邮件 一 系统设置 1 在Jenkins安装Email Extension Plugin插件 xff0c 如下图 xff1a 2 设置Extended E mail Notific
  • Java读取csv文件

    简介 xff1a 总结用java通过读取csv文件方法 xff0c 为用csv文件来做数据驱动测试提供解决方案 创建csv文件 用WPS表格或excel创建文件 xff0c 填写数据 xff0c 另存为选择CSV格式进行保存 xff0c 如

随机推荐