C语言中的线程同步

C语言中的线程同步

本人学习大丙老师博客的笔记,博客地址为:https://subingwen.cn/linux/thread-sync/

1. 概念

当一个线程对内存中的共享资源进行访问的时候,其他线程都不可以操作这块内存,直到线程 A 对这块内存访问完毕为止。

2 为什么要实现线程同步

为了使多个线程操作同一个全局变量的时候,全局变量的数据被及时正确的读取、计算、写入内存中,保证程序正确的执行下去。

3 同步方式

  • 互斥锁
  • 读写锁
  • 条件变量
  • 信号量

所谓的共享资源就是多个线程共同访问的变量,这些变量通常为全局数据区变量或者堆区变量,这些变量对应的共享资源也被称之为临界资源。

image-20200106092600543

  • 临界区上边上锁,下边解锁

1. 互斥锁

  • 通过互斥锁可以锁定一个代码块,

  • 被锁定的这个代码块,所有的线程只能顺序执行 (不能并行处理),这样多线程访问共享资源数据混乱的问题就可以被解决了,需要付出的代价就是执行效率的降低,因为默认临界区多个线程是可以并行处理的,现在只能串行处理。

  • 创建一把互斥锁

    pthread_mutex_t mutex;
    
    // 初始化互斥锁
    // restrict: 是一个关键字, 用来修饰指针, 只有这个关键字修饰的指针可以访问指向的内存地址, 其他指针是不行的
    int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
    // 释放互斥锁资源            
    int pthread_mutex_destroy(pthread_mutex_t *mutex);
    
    //参数:
    //mutex: 互斥锁变量的地址
    //attr: 互斥锁的属性,一般使用默认属性即可,这个参数指定为 NULL
    
  • 上锁

    int pthread_mutex_lock(pthread_mutex_t* mutex);
    
  • 尝试加锁

    int pthread_mutex_trylock(pthread_mutex* mutex);
    //如果锁变量是锁定的,调用这个函数加锁的线程,不会被阻塞,加锁失败直接返回错误号。
    
  • 解锁

    int pthread_mutex_unlock(pthread_mutex* mutex);
    //不是所有的线程都可以对互斥锁解锁,哪个线程加的锁,哪个线程才能解锁成功。
    

死锁:锁使用不当,如果线程死锁造成的后果是:所有的线程都被阻塞,并且线程的阻塞是无法解开的(因为可以解锁的线程也被阻塞了)。

造成死锁的场景有:

  • 加锁后忘记解锁

  • 重复加锁,造成死锁

  • 程序中有多个共享资源,因此有很多把锁,随意加锁,导致相互被阻塞

    场景描述:
      1. 有两个共享资源:X, Y,X对应锁A, Y对应锁B
         - 线程A访问资源X, 加锁A
         - 线程B访问资源Y, 加锁B
      2. 线程A要访问资源Y, 线程B要访问资源X,因为资源X和Y已经被对应的锁锁住了,因此这个两个线程被阻塞
         - 线程A被锁B阻塞了, 无法打开A锁
         - 线程B被锁A阻塞了, 无法打开B锁
    

2. 读写锁

  • 创建 :pthread_rwlock_t rwlock;

  • 在做读操作的时候可以提高程序的执行效率,如果所有的线程都是做读操作, 那么读是并行的

  • 如果读写锁锁定了读操作,需要先解锁才能锁定写操作,反之亦然。

  • 读写锁的特点:

    1. 读锁是共享的
    2. 写锁是独占的
    3. 写锁比读锁的优先级高。

如果说程序中所有的线程都对共享资源做写操作,使用读写锁没有优势,和互斥锁是一样的,如果说程序中所有的线程都对共享资源有写也有读操作,并且对共享资源读的操作越多,读写锁更有优势

  • 读写锁的操作函数
#include<pthread.h>
pthread_rwlock_t rwlock;
//初始化读写锁
int pthread_rwlock(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t* restrict attr);
//attr: 读写锁属性,一般使用默认属性,指定为 NULL
//释放读写锁占用的系统资源
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
//加读锁
int phtread_rwlock_rdlock(pthread_rwlock_t* rwlocck);
int phtread_rwlock_tryrdlock(pthread_rwlock_t* rwlocck);
//加写锁
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
// 解锁, 不管锁定了读还是写都可用解锁
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

3. 条件变量

条件变量的主要作用不是处理线程同步,而是进行线程的阻塞。在多线程程序中,条件变量需要配合互斥锁来使用。

条件变量只有在满足指定条件下才会阻塞线程,如果条件不满足,多个线程可以同时进入临界区,同时读写临界资源,这种情况下还是会出现共享资源中数据的混乱。

  • 条件变量的操作函数如下:
#include<pthread.h>
pthread_cond_t cond;
//初始化
int pthread_cond_init(pthread_cond_t* restrict cond, const pthread_condattr_t* restrict attr);//att 指定为NULL
// 销毁释放资源        
int pthread_cond_destroy(pthread_cond_t *cond);

// 线程阻塞函数, 哪个线程调用这个函数, 哪个线程就会被阻塞
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);

// 表示的时间是从1971.1.1到某个时间点的时间, 总长度使用秒/纳秒表示
struct timespec {
	time_t tv_sec;      /* Seconds */
	long   tv_nsec;     /* Nanoseconds [0 .. 999999999] */
};
// 将线程阻塞一定的时间长度, 时间到达之后, 线程就解除阻塞了
int pthread_cond_timedwait(pthread_cond_t *restrict cond,
           pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);

// 唤醒阻塞在条件变量上的线程, 至少有一个被解除阻塞
int pthread_cond_signal(pthread_cond_t *cond);
// 唤醒阻塞在条件变量上的线程, 被阻塞的线程全部解除阻塞
int pthread_cond_broadcast(pthread_cond_t *cond);



生产者和消费者模型

1564644834918

生产者和消费者模型的组成:

  1. 生产者线程 -> 若干个
    1. 生产商品或者任务放入到任务队列中
    2. 任务队列满了就阻塞,不满的时候就工作
    3. 通过一个生产者的条件变量控制生产者线程阻塞和非阻塞
  2. 消费者线程 -> 若干个
    1. 读任务队列,将任务或者数据取出
    2. 任务队列中有数据就消费,没有数据就阻塞
    3. 通过一个消费者的条件变量控制消费者线程阻塞和非阻塞
  3. 队列->存储任务 / 数据,对应一块内存,为了读写访问可以通过一个数据结构维护这块内存
    • 可以是数组、链表,也可以使用 stl容器:queue / stack / list / vector

4. 信号量

信号量用在多线程多任务同步的,一个线程完成了某一个动作就通过信号量告诉别的线程,别的线程再进行某些动作。信号量主要阻塞线程,不能完全保证线程安全,如果要保证线程安全,需要信号量和互斥锁一起使用。

信号量和条件变量一样用于处理生产者和消费者模型,用于阻塞生产者线程或者消费者线程的运行。信号的类型为sem_t对应的头文件为 <semaphore.h>:

信号量操作函数如下:

#include<semaphore.h>
//初始化信号量/灯
int sem_init(sem_t* sem, int pshared, unsigned int value);
// 资源释放, 线程销毁之后调用这个函数即可
// 参数 sem 就是 sem_init() 的第一个参数       
int sem_destroy(sem_t* sem);

参数:
sem:信号量变量地址
pshared
0:线程同步
非 0:进程同步
value:初始化当前信号量拥有的资源数(>=0),如果资源数为 0,线程就会被阻塞了.

// 参数 sem 就是 sem_init() 的第一个参数  
// 函数被调用sem中的资源就会被消耗1个, 资源数-1
int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem); // sem 中的资源数减为 0 时,资源被耗尽,但是线程不会被阻塞,直接返回错误号

当线程调用这个函数,并且 sem 中的资源数 >0,线程不会阻塞,线程会占用 sem 中的一个资源,因此资源数 - 1,直到 sem 中的资源数减为 0 时,资源被耗尽,因此线程也就被阻塞了。

//调用调用该函数给sem中的资源数+1
int sem_post(sem_t* sem);
//查看信号量sem中的整型数的当前值,个值会被写入到sval指针对应的内存中
// sval是一个传出参数
int sem_getvalue(sem_t* sem, int* sval);
总资源数为1

如果生产者和消费者使用的信号量总资源数为 1,那么不会出现生产者线程和消费者线程同时访问共享资源的情况,不管生产者和消费者线程有多少个,它们都是顺序执行的。

总资源数大于 1

为防止共享资源出现数据混乱,需要使用互斥锁进行线程同步。

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

// 链表的节点
struct Node
{
    int number;
    struct Node* next;
};

// 生产者线程信号量
sem_t psem;
// 消费者线程信号量
sem_t csem;

// 互斥锁变量
pthread_mutex_t mutex;
// 指向头结点的指针
struct Node * head = NULL;

// 生产者的回调函数
void* producer(void* arg)
{
    // 一直生产
    while(1)
    {
        // 生产者拿一个信号灯
        sem_wait(&psem);
        // 加锁, 这句代码放到 sem_wait()上边, 有可能会造成死锁
        pthread_mutex_lock(&mutex);
        // 创建一个链表的新节点
        struct Node* pnew = (struct Node*)malloc(sizeof(struct Node));
        // 节点初始化
        pnew->number = rand() % 1000;
        // 节点的连接, 添加到链表的头部, 新节点就新的头结点
        pnew->next = head;
        // head指针前移
        head = pnew;
        printf("+++producer, number = %d, tid = %ld\n", pnew->number, pthread_self());
        pthread_mutex_unlock(&mutex);

        // 通知消费者消费
        sem_post(&csem);
        
        // 生产慢一点
        sleep(rand() % 3);
    }
    return NULL;
}

// 消费者的回调函数
void* consumer(void* arg)
{
    while(1)
    {
        sem_wait(&csem);
        pthread_mutex_lock(&mutex);
        struct Node* pnode = head;
        printf("--consumer: number: %d, tid = %ld\n", pnode->number, pthread_self());
        head  = pnode->next;
        // 取出链表的头结点, 将其删除
        free(pnode);
        pthread_mutex_unlock(&mutex);
        // 通知生产者生成, 给生产者加信号灯
        sem_post(&psem);

        sleep(rand() % 3);
    }
    return NULL;
}

int main()
{
    // 初始化信号量
    sem_init(&psem, 0, 5);  // 生成者线程一共有5个信号灯
    sem_init(&csem, 0, 0);  // 消费者线程一共有0个信号灯
    // 初始化互斥锁
    pthread_mutex_init(&mutex, NULL);

    // 创建5个生产者, 5个消费者
    pthread_t ptid[5];
    pthread_t ctid[5];
    for(int i=0; i<5; ++i)
    {
        pthread_create(&ptid[i], NULL, producer, NULL);
    }

    for(int i=0; i<5; ++i)
    {
        pthread_create(&ctid[i], NULL, consumer, NULL);
    }

    // 释放资源
    for(int i=0; i<5; ++i)
    {
        pthread_join(ptid[i], NULL);
    }

    for(int i=0; i<5; ++i)
    {
        pthread_join(ctid[i], NULL);
    }

    sem_destroy(&psem);
    sem_destroy(&csem);
    pthread_mutex_destroy(&mutex);

    return 0;
}


//作者: 苏丙榅
//链接: https://subingwen.cn/linux/thread-sync/
//来源: 爱编程的大丙
//著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

在编写上述代码的时候还有一个需要注意是事项,不管是消费者线程的处理函数还是生产者线程的处理函数内部有这么两行代码:

// 消费者
sem_wait(&csem);
pthread_mutex_lock(&mutex);
// 生产者
sem_wait(&csem);
pthread_mutex_lock(&mutex);

这两行代码的调用顺序是不能颠倒的,如果颠倒过来就有可能会造成死锁.

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值