Linux学习日记20——线程同步

学习视频链接

黑马程序员-Linux系统编程_哔哩哔哩_bilibilihttps://www.bilibili.com/video/BV1KE411q7ee?p=167

目录

一、同步概念

1.1 线程同步

1.2 线程互斥

1.3 数据混乱原因

二、互斥量mutex

2.1 基本概念

2.2 出现的问题

2.3 使用流程

2.4 代码

2.5 互斥锁的使用技巧

三、读写锁

3.1 读写锁特性(操作系统会讲)

3.2 主要函数

3.3 代码

四、死锁

五、条件产量

5.1 定义

5.2 主要应用函数

5.3 初始化使用宏函数代替

5.4 生产者消费者问题

六、信号量

6.1 作用

6.2 主要应用函数

6.3 信号量基本操作

6.4 代码 


一、同步概念

1.1 线程同步

线程1有以下功能:买菜、烧菜、洗碗

线程2有以下功能:洗菜、切菜、端菜

线程1完成买菜,线程2才能执行洗菜、切菜,接着是线程1烧菜,线程2端菜,线程1洗碗

1.2 线程互斥

访问临界资源会互斥进入(参考操作系统)

1.3 数据混乱原因

1、资源共享(独享资源则不会)

2、调度随机(意味着数据访问会出现竞争)

3、线程间缺乏必要的同步机制

以上 3 点中,前两点不能改变,欲提高效率,传递数据,资源必须共享。只要共享资源,就一定会出现竞争。只要存在竞争关系,数据就很容易出现混乱

所以只能从第三点着手解决。使多个线程在访问共享资源的时候,出现互斥

二、互斥量mutex

2.1 基本概念

Linux 中提供一把互斥锁 mutex(也称之为互斥量)

每个线程在对资源操作前都尝试先加锁,成功加锁才能操作,操作结束解锁

资源还是共享的,线程间也还是竞争的,但通过 “锁” 就将资源的访问变成互斥操作,而后与时间有关的错误也不会再产生了

但,应注意:同一时刻,只能有一一个线程持有该锁

当 A 线程对某个全局变量加锁访问,B 在访问前尝试加锁,拿不到锁,B 阻塞。C 线程不去加锁,而直接访问该全局变量,依然能够访问,但会出现数据混乱

所以,互斥锁实质上是操作系统提供的一把 “建议锁”(又称“协同锁”),建议程序中有多线程访问共享资源的时候使用该机制。但,并没有强制限定

因此,即使有了 mutex,如果有线程不按规则来访问数据,依然会造成数据混乱

2.2 出现的问题

 没有进程同步

2.3 使用流程

1、主要函数

pthread_mutex_init 函数

pthread_mutex_destroy 函数

pthread_mutex_lock 函数

pthread_mutex_trylock 函数

pthread_mutex_unlock 函数

以上 5 个函数的返回值都是:成功返回 0,失败返回错误号

pthread_mutex_t 类型,其本质是一个结构体。为简化理解,应用时可忽略其实现细节,简单当成整数看待

pthread_mutex_t mutex; 变量 mutex 只有两种取值 1、0

2、使用 mutex(互斥量、互斥锁)一般步骤

(1) pthread_ mutex_t lock;  创建锁

(2) pthread_mutex_init;  初始化

(3) pthread_mutex_lock;  加锁

(4) 访问共享数据 (stdout)

(5) pthrad_mutext_unlock;  解锁

(6) pthead_mutex_destroy;  销毁锁

2.4 代码

2.5 互斥锁的使用技巧

1、代码写成下面这样,解锁比较靠后

代码执行的时候在一段时间一直是小写或者大写,原因是子线程有锁的时候很难让父线程拿到锁,或者父线程有锁的时候很难让子线程拿到锁

要尽量保证锁的粒度,越小越好。(访问共享数据前,加锁。访问结束立刻解锁)

2、互斥锁,本质是结构体,我们可以看成整数,初值为 1。(pthread_mutex_init() 函数调用成功)

加锁:--操作,阻塞线程

解锁:++操作,唤醒阻塞在锁上的线程

try锁:尝试加锁,成功 -- 、失败 返回错误号,不阻塞

三、读写锁

3.1 读写锁特性(操作系统会讲)

1、读写锁是 “写模式加锁” 时,解锁前,所有对该锁加锁的线程都会被阻塞

2、读写锁是 “读模式加锁” 时,如果线程以读模式对其加锁会成功;如果线程以写模式加锁会阻塞。。
3.读写锁是 “读模式加锁” 时,既有试图以写模式加锁的线程,也有试图以读模式加锁的线程。那么读写锁会阻塞随后的读模式锁请求。优先满足写模式锁。读锁、写锁并行阻塞,写锁优先级高

读写锁也叫共享独占锁。当读写锁以读模式锁住时,它是以共享模式锁住的;当它以写模式锁住时,它是以独占模式锁住的。写独占、读共享

读写锁非常适合于对数据结构读的次数远大于写的情况

3.2 主要函数

pthread_rwlock_init 函数

pthread_rwlock_destroy 函数

pthread_rwlock_rdlock 函数

pthread_rwlock_wrlock 函数

pthread_rwlock_tryrdlock 函数

pthread_rwlock_trywrlock 函数

pthread_rwlock_unlock 函数

以上 7 个函数的返回值都是:成功返回 0,失败直接返回错误号

pthread_rwlock_t 类型    用于定义一个读写锁变量

pthread_rwlock_t rwlock;

3.3 代码

 在一个 write 执行期间有其他的 write 到来后会再次先执行 write,而不是比 write 先到的 read,因为写锁的优先级更高

四、死锁

是使用锁不恰当导致的现象

1、对一个变量反复lock

2、两个线程,各自持有一把锁,请求另一把(操作系统占有等待)

五、条件产量

5.1 定义

条件变量本身不是锁!但它也可以造成线程阻塞。通常与互斥锁配合使用。给多线程提供一个会合的场所

5.2 主要应用函数

1、简介

pthread_cond_init 函数

pthread_cond_destroy 函数

pthread_cond_wait 函数

pthread_cond_timedwait 函数

pthread_cond_signal 函数

pthread_cond_broadcast 函数

以上 6 个函数的返回值都是:成功返回 0, 失败直接返回错误号

pthread_cond_t 类型  用于定义条件变量

pthread_cond_t cond;

2、pthread_cond_init 函数

(1) 初始化一个条件变量

int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);

参2:attr 表条件变量属性,通常为默认值,传 NULL 即可

(2) 也可以使用静态初始化的方法,初始化条件变量:

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

3、pthread_cond_destroy 函数

销毁一个条件变量

int pthread_cond_destroy(pthread_cond_t *cond);

4、pthread_cond_wait 函数

阻塞等待一个条件变量

int pthread cond_ wait(pthread cond t *restrict cond, pthread mutex t *restrict mutex);

函数作用:

(1) 阻塞等待条件变量 cond (参1) 满足

(2) 释放已掌握的互斥锁(解锁互斥量)相当于pthread_mutex_unlock(&mutex);

(1) (2) 两步为一个原子操作(原子操作是不可分的,一次性完成)

(3) 当被唤醒,pthread_cond_wait 函数返回时,解除阻塞并重新申请获取互斥锁 pthread_mutex_lock(&mutex);

5、pthread_cond_signal()

唤醒阻塞在条件变量上的(至少)一个线程

6、pthread_cond_broadcast()

唤醒阻塞在条件上的所有进程

5.3 初始化使用宏函数代替

1、动态初始化

pthread_mutex_t mutex;

pthread_mutex_init(&mutex, NULL);

2、静态初始化

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER

5.4 生产者消费者问题

1、一个生产者一个消费者

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

struct msg {
    int num;
    struct msg *next;
};

struct msg *head;

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;   // 定义/初始化一个互斥量
pthread_cond_t has_data = PTHREAD_COND_INITIALIZER;  // 定义/初始化一个条件变量

void err_thread(int ret, char *str)
{   
    if (ret != 0) {
        fprintf(stderr, "%s:%s\n", str, strerror(ret));
        pthread_exit(NULL);
    }
}

void *produser(void *arg)  // 生产者进程
{
    while (1) {
        struct msg *mp = malloc(sizeof(struct msg));

        mp->num = rand() % 1000 + 1;    // 模拟生产一个数据
        printf("------ produce: %d\n", mp->num);

        pthread_mutex_lock(&mutex);     // 加锁 互斥量
        mp->next = head;                // 写公共区域
        head = mp;
        pthread_mutex_unlock(&mutex);   // 解锁 互斥量

        pthread_cond_signal(&has_data); // 唤醒阻塞在条件变量has_data上的线程
        sleep(rand() % 3);
    }

    return NULL;
}

void *consumer(void *arg)  // 消费者进程
{
    while (1) {
        struct msg *mp;

        pthread_mutex_lock(&mutex);               // 加锁 互斥量
        if (head == NULL) {
            pthread_cond_wait(&has_data, &mutex); // 阻塞等待条件变量
        }
        mp = head;
        head = mp->next;
        pthread_mutex_unlock(&mutex);             // 解锁 互斥量

        printf("+++++ consumer: %d\n", mp->num);
        free(mp);
        sleep(rand() % 3);
    }

    return NULL;
}

int main(void)
{
    int ret;
    pthread_t pid, cid;

    srand(time(NULL));

    ret = pthread_create(&pid, NULL, produser, NULL);
    err_thread(ret, "pthread_create produser error");

    ret = pthread_create(&cid, NULL, consumer, NULL);
    err_thread(ret, "pthread_create consumer error");

    pthread_join(pid, NULL);
    pthread_join(cid, NULL);

    return 0;
}

2、一个生产者多个消费者

一上来没有数据,所有的消费者进程阻塞在 pthread_cond_wait(&has_data, &mutex); 函数上,执行了一次生产者进程,所有的消费者进程同时被唤醒,去抢锁。

一个消费者进程抢到了锁,把生产者生产的内容消耗了,解除锁,另一个消费者进程拿到锁去读数据,但是读不到数据了。

所以应该先判断条件变量是否满足,如果满足再去拿锁,所以使用 while 循环,如下图

另外稍微修改了以下生产者进程

得到的结果:

六、信号量

6.1 作用

相当于初始化值为 N 的互斥量。N 值, 表示可以同时访问共享数据区的线程数

6.2 主要应用函数

sem_init 函数

sem_destroy 函数

sem_wait 函数

sem_trywait 函数

sem_timedwait 函数

sem post 函数

以上 6 个函数的返回值都是:成功返回 0,失败返回 -1, 同时设置 errno。(注意,它们没 pthread 前缀)

sem_t 类型,本质仍是结构体。但应用期间可简单看作为整数,忽略实现细节(类似于使用文件描述符)

sem_tsem;  规定信号量 sem 不能 < 0。头文件 <semaphore.h>

1、sem_init 函数

初始化一个信号量

int sem_init(sem_t *sem, int pshared, unsigned int value);

参1:sem 信号量

参2:pshared 取 0 用于线程间,取非 0(一般为 1)用于进程间

参3:value 指定信号量初值

2、sem_destroy 函数

销毁一个信号量

int sem_destroy(sem_t *sem);

3、sem_wait 函数

给信号量加锁 --

int sem_wait(sem_t *sem);

4、sem_post 函数

给信号量解锁 ++ 

int sem_post(sem_t *sem);

5、sem_trywait 函数

尝试对信号量加锁 --(与 sem_wait 的区别类比 lock 和 trylock)

int sem_trywait(sem_t *sem);

6、sem_timedwait 函数

限时尝试对信号量加锁 --

int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);

参2:abs_timeout 采用的是绝对时间

定时 1 秒:

time_t cur = time(NULL);    获取当前时间

struct timespec t;    定义 timespec 结构体变量 t

t.tv_sec = cur + 1;    定时 1 秒

t.tv_nsec = t.tv_sec + 100;

sem_timedwait(&sem, &t);    传参

6.3 信号量基本操作

但,由于 sem_t 的实现对用户隐藏,所以所谓的 ++、-- 操作只能通过函数来实现,而不能直接++、-- 符号

信号量的初值,决定了占用信号量的线程的个数

 

6.4 代码 

信号量实现 生产者 消费者问题

 

 

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

herb.dr

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值