alin的学习之路(Linux系统编程:十)(互斥锁、读写锁、条件变量、信号量、哲学家就餐问题)

alin的学习之路(Linux系统编程:十)(互斥锁、读写锁、条件变量、信号量、哲学家就餐问题)

1. 互斥锁 mutex

1. 同步与互斥

现代操作系统基本都是多任务操作系统,即同时有大量可调度实体在运行。在多任务操作系统中,同时运行的多个任务可能:

  • 都需要访问/使用同一种资源
  • 多个任务之间有依赖关系,某个任务的运行依赖于另一个任务

这两种情形是多任务编程中遇到的最基本的问题,也是多任务编程中的核心问题,同步和互斥就是用于解决这两个问题的。

**互斥:**是指散布在不同任务之间的若干程序片断,当某个任务运行其中一个程序片段时,其它任务就不能运行它们之中的任一程序片段,只能等到该任务运行完这个程序片段后才可以运行。最基本的场景就是:一个公共资源同一时刻只能被一个进程或线程使用,多个进程或线程不能同时使用公共资源。

**同步:**是指散步在不同任务之间的若干程序片断,它们的运行必须严格按照规定的某种先后次序来运行,这种先后次序依赖于要完成的特定的任务。最基本的场景就是:两个或两个以上的进程或线程在运行过程中协同步调,按预定的先后次序运行。比如 A 任务的运行依赖于 B 任务产生的数据。

2. 互斥锁概述

为什么需要互斥锁:如果没有互斥锁,例如打印机,只能一个人一个人打印,如果没有互斥,则两个人打印的内容会混在一起。

在线程里也有这么一把锁:互斥锁(mutex),也叫互斥量,互斥锁是一种简单的加锁的方法来控制对共享资源的访问,互斥锁只有两种状态,即加锁( lock )和解锁( unlock )。

互斥锁的操作流程如下:

1)在访问共享资源后临界区域前,对互斥锁进行加锁。

2)在访问完成后释放互斥锁导上的锁。

3)对互斥锁进行加锁后,任何其他试图再次对互斥锁加锁的线程将会被阻塞,直到锁被释放。

互斥锁的数据类型是: pthread_mutex_t。

安装对应帮助手册:

sudo apt-get install manpages-posix-dev

互斥锁是独占型锁,对哪一块临界共享内容加锁就要把这一块内容的操作写在加锁和解锁的中间。不同线程间的互斥仅对于同一把锁而言,也就是说如果两个线程进行同一个操作,而加了两把不同的锁,那么将不会其中一个阻塞等待。

总之:对谁加锁,把谁写在加锁解锁中间;互斥对于同一把锁而言。

3. 互斥锁的函数

1. pthread_mutex_init 函数

初始化互斥锁:

#include <pthread.h>

int pthread_mutex_init(pthread_mutex_t *restrict mutex,
    const pthread_mutexattr_t *restrict attr);
功能:
    初始化一个互斥锁。
参数:
    mutex:互斥锁地址。类型是 pthread_mutex_t 。
    attr:设置互斥量的属性,通常可采用默认属性,即可将 attr 设为 NULL。

    可以使用宏 PTHREAD_MUTEX_INITIALIZER 静态初始化互斥锁,比如:
    pthread_mutex_t  mutex = PTHREAD_MUTEX_INITIALIZER;

这种方法等价于使用 NULL 指定的 attr 参数调用 pthread_mutex_init() 来完成动态初始化,不同之处在于 PTHREAD_MUTEX_INITIALIZER 宏不进行错误检查。

返回值:
    成功:0,成功申请的锁默认是打开的。
    失败:非 0 错误码

restrict,C语言中的一种类型限定符(Type Qualifiers),用于告诉编译器,对象已经被指针所引用,不能通过除该指针外所有其他直接或间接的方式修改该对象的内容。

2. pthread_mutex_destroy 函数

#include <pthread.h>

int pthread_mutex_destroy(pthread_mutex_t *mutex);
功能:
    销毁指定的一个互斥锁。互斥锁在使用完毕后,必须要对互斥锁进行销毁,以释放资源。
参数:
    mutex:互斥锁地址。
返回值:
    成功:0
    失败:非 0 错误码

3. pthread_mutex_lock 函数

#include <pthread.h>

int pthread_mutex_lock(pthread_mutex_t *mutex);
功能:
    对互斥锁上锁,若互斥锁已经上锁,则调用者阻塞,直到互斥锁解锁后再上锁。
参数:
    mutex:互斥锁地址。
返回值:
    成功:0
    失败:非 0 错误码

int pthread_mutex_trylock(pthread_mutex_t *mutex);
   	调用该函数时,若互斥锁未加锁,则上锁,返回 0;
   	若互斥锁已加锁,则函数直接返回失败,即 EBUSY。

4. pthread_mutex_unlock函数

#include <pthread.h>

int pthread_mutex_unlock(pthread_mutex_t *mutex);
功能:
    对指定的互斥锁解锁。
参数:
    mutex:互斥锁地址。
返回值:
    成功:0
    失败:非0错误码

5. 示例代码

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

pthread_mutex_t mutex;
pthread_mutex_t mutex2;


void* func1(void* arg)
{
    pthread_mutex_lock(&mutex);
    for(int i=0 ;i<26 ;++i)
    {
        printf("%c ",'a'+i);
        fflush(stdout);
        usleep(100000);
    }
    pthread_mutex_unlock(&mutex);
    return NULL;
}

void* func2(void* arg)
{
    for(int i=0 ;i<26 ;++i)
    {
        printf("%c ",'A'+i);
        fflush(stdout);
        usleep(100000);
    }
    pthread_mutex_unlock(&mutex2);
    return NULL;
}
int main()
{
    int ret = pthread_mutex_init(&mutex,NULL);
    if(0 != ret)
    {
        printf("%s\n",strerror(ret));
        return 1;
    }
    ret = pthread_mutex_init(&mutex2,NULL);
    if(0 != ret)
    {
        printf("%s\n",strerror(ret));
        return 1;
    }

    pthread_t tid1;
    pthread_t tid2;
    ret = pthread_create(&tid1,NULL,func1,NULL);
    if(0 != ret)
    {
        printf("%s\n",strerror(ret));
        return 1;
    }
    ret = pthread_create(&tid2,NULL,func2,NULL);
    if(0 != ret)
    {
        printf("%s\n",strerror(ret));
        return 1;
    }


    ret = pthread_join(tid1,NULL);
    if(0 != ret)
    {
        printf("%s\n",strerror(ret));
        return 1;
    }
    ret = pthread_join(tid2,NULL);
    if(0 != ret)
    {
        printf("%s\n",strerror(ret));
        return 1;
    }

    printf("\n");
    ret = pthread_mutex_destroy(&mutex);
    if(0 != ret)
    {
        printf("%s\n",strerror(ret));
        return 1;
    }
    ret = pthread_mutex_destroy(&mutex2);
    if(0 != ret)
    {
        printf("%s\n",strerror(ret));
        return 1;
    }
    return 0;
}

2. 死锁

1)什么是死锁

死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。

2)死锁引起的原因

竞争不可抢占资源引起死锁

也就是我们说的第一种情况,而这都在等待对方占有的不可抢占的资源。

竞争可消耗资源引起死锁

有p1,p2,p3三个进程,p1向p2发送消息并接受p3发送的消息,p2向p3发送消息并接受p1的消息,p3向p1发送消息并接受p2的消息,如果设置是先接到消息后发送消息,则所有的消息都不能发送,这就造成死锁。

进程推进顺序不当引起死锁

有进程p1,p2,都需要资源A,B,本来可以p1运行A --> p1运行B --> p2运行A --> p2运行B,但是顺序换了,p1运行A时p2运行B,容易发生第一种死锁。互相抢占资源。

3)死锁的必要条件

  1. 互斥条件

    某资源只能被一个进程使用,其他进程请求该资源时,只能等待,直到资源使用完毕后释放资源。

  2. 请求和保持条件

    程序已经保持了至少一个资源,但是又提出了新要求,而这个资源被其他进程占用,自己占用资源却保持不放。

  3. 不可抢占条件

    进程已获得的资源没有使用完,不能被抢占。

  4. 循环等待条件

    必然存在一个循环链。

4)处理死锁的思路

  1. 预防死锁

    破坏死锁的四个必要条件中的一个或多个来预防死锁。

  2. 避免死锁

    和预防死锁的区别就是,在资源动态分配过程中,用某种方式防止系统进入不安全的状态。

  3. 检测死锁

    运行时出现死锁,能及时发现死锁,把程序解脱出来

  4. 解除死锁

    发生死锁后,解脱进程,通常撤销进程,回收资源,再分配给正处于阻塞状态的进程。

5)预防死锁的方法

*破坏请求和保持条件*

协议1:

所有进程开始前,必须一次性地申请所需的所有资源,这样运行期间就不会再提出资源要求,破坏了请求条件,即使有一种资源不能满足需求,也不会给它分配正在空闲的资源,这样它就没有资源,就破坏了保持条件,从而预防死锁的发生。

协议2:

允许一个进程只获得初期的资源就开始运行,然后再把运行完的资源释放出来。然后再请求新的资源。

*破坏不可抢占条件*

当一个已经保持了某种不可抢占资源的进程,提出新资源请求不能被满足时,它必须释放已经保持的所有资源,以后需要时再重新申请。

*破坏循环等待条件*

对系统中的所有资源类型进行线性排序,然后规定每个进程必须按序列号递增的顺序请求资源。假如进程请求到了一些序列号较高的资源,然后有请求一个序列较低的资源时,必须先释放相同和更高序号的资源后才能申请低序号的资源。多个同类资源必须一起请求。

示例代码:

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

pthread_mutex_t mutex1;
pthread_mutex_t mutex2;


void printer(const char* str)
{
    while('\0' != *str)
    {
        putchar(*str);
        fflush(stdout);
        ++str;
        usleep(100000);
    }
}


void* func1(void* arg)
{
    
    pthread_mutex_lock(&mutex1);
    printf("加锁mutex1成功\n");
    sleep(1);
    pthread_mutex_lock(&mutex2);
    printf("加锁mutex2成功\n");
    
    printer("HELLO");

    pthread_mutex_unlock(&mutex1);
    pthread_mutex_unlock(&mutex2);

    return NULL;
}

void* func2(void* arg)
{
    pthread_mutex_lock(&mutex2);
    printf("加锁mutex2成功\n");
    sleep(1);
    pthread_mutex_lock(&mutex1);
    printf("加锁mutex1成功\n");

    printer("hello");

    pthread_mutex_unlock(&mutex2);
    pthread_mutex_unlock(&mutex1);
    return NULL;
}
int main()
{
    int ret = pthread_mutex_init(&mutex1,NULL);
    if(0 != ret)
    {
        printf("%s\n",strerror(ret));
        return 1;
    }
    ret = pthread_mutex_init(&mutex2,NULL);
    if(0 != ret)
    {
        printf("%s\n",strerror(ret));
        return 1;
    }

    pthread_t tid1;
    pthread_t tid2;
    ret = pthread_create(&tid1,NULL,func1,NULL);
    if(0 != ret)
    {
        printf("%s\n",strerror(ret));
        return 1;
    }
    ret = pthread_create(&tid2,NULL,func2,NULL);
    if(0 != ret)
    {
        printf("%s\n",strerror(ret));
        return 1;
    }


    ret = pthread_join(tid1,NULL);
    if(0 != ret)
    {
        printf("%s\n",strerror(ret));
        return 1;
    }
    ret = pthread_join(tid2,NULL);
    if(0 != ret)
    {
        printf("%s\n",strerror(ret));
        return 1;
    }

    printf("\n");
    ret = pthread_mutex_destroy(&mutex1);
    if(0 != ret)
    {
        printf("%s\n",strerror(ret));
        return 1;
    }
    ret = pthread_mutex_destroy(&mutex2);
    if(0 != ret)
    {
        printf("%s\n",strerror(ret));
        return 1;
    }
    return 0;
}

3. 读写锁

在对数据的读写操作中,更多的是读操作,写操作较少,例如对数据库数据的读写应用。为了满足当前能够允许多个读出,但只允许一个写入的需求,线程提供了读写锁来实现。

读写锁的特点如下:

1)如果有其它线程读数据,则允许其它线程执行读操作,但不允许写操作。

2)如果有其它线程写数据,则其它线程都不允许读、写操作。

读写锁分为读锁和写锁,规则如下:

1)如果某线程申请了读锁,其它线程可以再申请读锁,但不能申请写锁。

2)如果某线程申请了写锁,其它线程不能申请读锁,也不能申请写锁。

POSIX 定义的读写锁的数据类型是: pthread_rwlock_t。

总之:

当读者占用时,读锁可以重复添加,读锁会阻塞等待所有读锁释放。

当写者占用时,任何读锁和写锁都会阻塞等待写锁释放。

1. pthread_rwlock_init函数

#include <pthread.h>

int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
    const pthread_rwlockattr_t *restrict attr);
功能:
    用来初始化 rwlock 所指向的读写锁。

参数:
    rwlock:指向要初始化的读写锁指针。
    attr:读写锁的属性指针。如果 attr 为 NULL 则会使用默认的属性初始化读写锁,否则使用指定的 attr 初始化读写锁。

    可以使用宏 PTHREAD_RWLOCK_INITIALIZER 静态初始化读写锁,比如:
    pthread_rwlock_t my_rwlock = PTHREAD_RWLOCK_INITIALIZER;

    这种方法等价于使用 NULL 指定的 attr 参数调用 pthread_rwlock_init() 来完成动态初始化,不同之处在于PTHREAD_RWLOCK_INITIALIZER 宏不进行错误检查。

返回值:
    成功:0,读写锁的状态将成为已初始化和已解锁。
    失败:非 0 错误码。

2. pthread_rwlock_destroy函数

#include <pthread.h>

int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
功能:
    用于销毁一个读写锁,并释放所有相关联的资源(所谓的所有指的是由 pthread_rwlock_init() 自动申请的资源) 。
参数:
    rwlock:读写锁指针。
返回值:
    成功:0
    失败:非 0 错误码

3. pthread_rwlock_rdlock函数

#include <pthread.h>

int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
功能:
    以阻塞方式在读写锁上获取读锁(读锁定)。
    如果没有写者持有该锁,并且没有写者阻塞在该锁上,则调用线程会获取读锁。
    如果调用线程未获取读锁,则它将阻塞直到它获取了该锁。一个线程可以在一个读写锁上多次执行读锁定。
    线程可以成功调用 pthread_rwlock_rdlock() 函数 n 次,但是之后该线程必须调用 pthread_rwlock_unlock() 函数 n 次才能解除锁定。
参数:
    rwlock:读写锁指针。
返回值:
    成功:0
    失败:非 0 错误码

int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
  	用于尝试以非阻塞的方式来在读写锁上获取读锁。
   	如果有任何的写者持有该锁或有写者阻塞在该读写锁上,则立即失败返回。

4. pthread_rwlock_wrlock函数

#include <pthread.h>

int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
功能:
    在读写锁上获取写锁(写锁定)。
    如果没有写者持有该锁,并且没有写者读者持有该锁,则调用线程会获取写锁。
    如果调用线程未获取写锁,则它将阻塞直到它获取了该锁。
参数:
    rwlock:读写锁指针。
返回值:
    成功:0
    失败:非 0 错误码

int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
  	用于尝试以非阻塞的方式来在读写锁上获取写锁。
  	如果有任何的读者或写者持有该锁,则立即失败返回。

5. pthread_rwlock_unlock函数

#include <pthread.h>

int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
功能:
    无论是读锁或写锁,都可以通过此函数解锁。
参数:
    rwlock:读写锁指针。
返回值:
    成功:0
    失败:非 0 错误码

6. 示例代码

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

#include <stdlib.h>

pthread_rwlock_t rwlock;
int num = 0;

void* func_read(void* arg)
{
    while(1)
    {
        //读线程循环读
        //加读锁
        pthread_rwlock_rdlock(&rwlock);
        printf("线程%d,读num = %d\n",(int)arg,num);
        //解锁
        pthread_rwlock_unlock(&rwlock);

        sleep(random() % 3 + 2);
        
    }
    return NULL;
}

void* func_write(void* arg)
{

    while(1)
    {
        //写线程循环读
        //加写锁
        pthread_rwlock_wrlock(&rwlock);
        ++num;
        printf("线程%d,写num = %d\n",(int)arg,num);
        //解锁
        pthread_rwlock_unlock(&rwlock);
        
        sleep(random() % 3 + 2);
    }
    return NULL;
}

int main()
{

    srand(size_t(time(NULL)));

    int ret = pthread_rwlock_init(&rwlock,NULL);
    if(0 != ret)
    {
        printf("%s\n",strerror(ret));
        return 1;
    }
    
    pthread_t tid[8];

    //创建8个线程,6个读,2个写
    for(int i=0 ;i<8 ;++i)
    {
        if(i < 6)
        {
            //创建读线程
            ret = pthread_create(&tid[i],NULL,func_read,(void*)i);
            if(0 != ret)
            {
                printf("%s\n",strerror(ret));
                return 1;
            }
        }
        else
        {
            //创建写线程
            ret = pthread_create(&tid[i],NULL,func_write,(void*)i);
            if(0 != ret)
            {
                printf("%s\n",strerror(ret));
                return 1;
            }
        }
    }

    for(int i=0; i<8 ;++i)
    {
        ret = pthread_join(tid[i],NULL);
        if(0 != ret)
        {
            printf("%s\n",strerror(ret));
            return 1;
        }
    }
    
    ret = pthread_rwlock_destroy(&rwlock);
    if(0 != ret)
    {
        printf("%s\n",strerror(ret));
        return 1;
    }

    return 0;
}

4. 条件变量

条件变量用来自动阻塞一个线程,直到某特殊情况发生为止。通常条件变量和互斥锁同时使用。

条件变量的两个动作:

  • 条件不满, 阻塞线程
  • 当条件满足, 通知阻塞的线程开始工作

条件变量的类型: pthread_cond_t。

条件变量是用来等待的,不是锁。

条件变量用于达到某个条件时来阻塞等待条件的满足来解除阻塞。

需要注意 pthread_cond_wait() 函数的功能,它会阻塞并释放mutex,这是一个原子操作。

1. pthread_cond_init函数

#include <pthread.h>

int pthread_cond_init(pthread_cond_t *restrict cond,
    const pthread_condattr_t *restrict attr);
功能:
    初始化一个条件变量
参数:
    cond:指向要初始化的条件变量指针。
    attr:条件变量属性,通常为默认值,传NULL即可
        也可以使用静态初始化的方法,初始化条件变量:
        pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
返回值:
    成功:0
    失败:非0错误号

2. pthread_cond_destroy函数

#include <pthread.h>

int pthread_cond_destroy(pthread_cond_t *cond);
功能:
    销毁一个条件变量
参数:
    cond:指向要初始化的条件变量指针
返回值:
    成功:0
    失败:非0错误号

3. pthread_cond_wait函数

#include <pthread.h>

int pthread_cond_wait(pthread_cond_t *restrict cond,
    pthread_mutex_t *restrict mutex);
功能:
    阻塞等待一个条件变量
    a) 阻塞等待条件变量cond(参1)满足
    b) 释放已掌握的互斥锁(解锁互斥量)相当于pthread_mutex_unlock(&mutex);
            a) b) 两步为一个原子操作。
    c) 当被唤醒,pthread_cond_wait函数返回时,解除阻塞并重新申请获取互斥锁pthread_mutex_lock(&mutex);

参数:
    cond:指向要初始化的条件变量指针
    mutex:互斥锁

返回值:
    成功:0
    失败:非0错误号

int pthread_cond_timedwait(pthread_cond_t *restrict cond,
    pthread_mutex_t *restrict mutex,
    const struct
                           .*restrict abstime);
功能:
    限时等待一个条件变量

参数:
    cond:指向要初始化的条件变量指针
    mutex:互斥锁
    abstime:绝对时间

返回值:
    成功:0
    失败:非0错误号

abstime补充说明:

struct timespec {
    time_t tv_sec;      /* seconds */ // 秒
    long   tv_nsec; /* nanosecondes*/ // 纳秒
}

time_t cur = time(NULL);        //获取当前时间。
struct timespec t;              //定义timespec 结构体变量t
t.tv_sec = cur + 1;             // 定时1秒
pthread_cond_timedwait(&cond, &t);

4. pthread_cond_signal函数

唤醒至阻塞在条件变量上的线程

#include <pthread.h>

int pthread_cond_signal(pthread_cond_t *cond);
功能:
    唤醒至少一个阻塞在条件变量上的线程
参数:
    cond:指向要初始化的条件变量指针
返回值:
    成功:0
    失败:非0错误号

int pthread_cond_broadcast(pthread_cond_t *cond);
功能:
    唤醒全部阻塞在条件变量上的线程
参数:
    cond:指向要初始化的条件变量指针
返回值:
    成功:0
    失败:非0错误号

5. 示例代码:生产者消费者条件变量模型

假定有两个线程,一个模拟生产者行为,一个模拟消费者行为。两个线程同时操作一个共享资源(一般称之为汇聚),生产向其中添加产品,消费者从中消费掉产品。

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

typedef struct node_t
{   
    int val;
    struct node_t* next;
}node_t;

node_t* head = NULL;

pthread_mutex_t mutex;
pthread_cond_t cond;

void* producer(void *arg)
{
    node_t* newNode = NULL;
    while(1)
    {
        pthread_mutex_lock(&mutex);

        newNode = (node_t*)malloc(sizeof(node_t));
        if(newNode == NULL)
        {
            printf("malloc error\n");
            return NULL;
        }
        //清空
        memset(newNode,0,sizeof(node_t));
        newNode->val = rand() % 100 + 1;
        newNode->next = NULL;

        //头插
        newNode->next = head;
        head = newNode;

        printf("生产者生产了 %d\n",newNode->val);

        pthread_mutex_unlock(&mutex);

        //给消费者发送信号告诉产生了产品
        pthread_cond_signal(&cond);

        sleep(rand() % 5 +1 );
    }


    return NULL;
}

void* customer(void *arg)
{
    while(1)
    {
        pthread_mutex_lock(&mutex);

        if(head == NULL)
        {
            pthread_cond_wait(&cond,&mutex);
        }

        //头删
        node_t* temp = head;
        head = head->next;

        printf("消费者消费了 %d\n",temp->val);

        free(temp);
        temp = NULL;
        
        pthread_mutex_unlock(&mutex);

        sleep(rand()%5 +1);

    }

    return NULL;
}


int main()
{
    pthread_t tid1,tid2;
    int ret = -1;

    ret = pthread_mutex_init(&mutex,NULL);
    if(0 != ret)
    {
        printf("%s\n",strerror(ret));
        return 1;
    }
    ret = pthread_cond_init(&cond,NULL);
    if(0 != ret)
    {
        printf("%s\n",strerror(ret));
        return 1;
    }

    ret = pthread_create(&tid1,NULL,producer,NULL);
    if(0 != ret)
    {
        printf("%s\n",strerror(ret));
        return 1;
    }
    ret = pthread_create(&tid2,NULL,customer,NULL);
    if(0 != ret)
    {
        printf("%s\n",strerror(ret));
        return 1;
    }

    ret = pthread_join(tid1,NULL);
    if(0 != ret)
    {
        printf("%s\n",strerror(ret));
        return 1;
    }
    ret = pthread_join(tid2,NULL);
    if(0 != ret)
    {
        printf("%s\n",strerror(ret));
        return 1;
    }

    pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&cond);

    return 0;
}

6. 条件变量的优缺点

相较于mutex而言,条件变量可以减少竞争。

如直接使用mutex,除了生产者、消费者之间要竞争互斥量以外,消费者之间也需要竞争互斥量,但如果汇聚(链表)中没有数据,消费者之间竞争互斥锁是无意义的。

有了条件变量机制以后,只有生产者完成生产,才会引起消费者之间的竞争。提高了程序效率。

5. 信号量

信号量广泛用于进程或线程间的同步和互斥,信号量本质上是一个非负的整数计数器,它被用来控制对公共资源的访问。

编程时可根据操作信号量值的结果判断是否对公共资源具有访问的权限,当信号量值大于 0 时,则可以访问,否则将阻塞。

PV 原语是对信号量的操作,一次 P 操作使信号量减1,一次 V 操作使信号量加1。

信号量主要用于进程或线程间的同步和互斥这两种典型情况。

信号量数据类型为:sem_t。

1. sem_init函数

初始化信号量:

#include <semaphore.h>

int sem_init(sem_t *sem, int pshared, unsigned int value);
功能:
    创建一个信号量并初始化它的值。一个无名信号量在被使用前必须先初始化。
参数:
    sem:信号量的地址。
    pshared:等于 0,信号量在线程间共享(常用);不等于0,信号量在进程间共享。
    value:信号量的初始值。
返回值:
    成功:0
    失败: - 1

2. sem_destroy函数

销毁信号量:

#include <semaphore.h>

int sem_destroy(sem_t *sem);
功能:
    删除 sem 标识的信号量。
参数:
    sem:信号量地址。
返回值:
    成功:0
    失败: - 1

3. 信号量P操作(减1)

#include <semaphore.h>

int sem_wait(sem_t *sem);
功能:
    将信号量的值减 1。操作前,先检查信号量(sem)的值是否为 0,若信号量为 0,此函数会阻塞,直到信号量大于 0 时才进行减 1 操作。
参数:
    sem:信号量的地址。
返回值:
    成功:0
    失败: - 1

int sem_trywait(sem_t *sem);
  	以非阻塞的方式来对信号量进行减 1 操作。
  	若操作前,信号量的值等于 0,则对信号量的操作失败,函数立即返回。
      
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
 	限时尝试将信号量的值减 1
  	abs_timeout:绝对时间

abs_timeout补充说明:

struct timespec {
    time_t tv_sec;      /* seconds */ // 秒
    long   tv_nsec; /* nanosecondes*/ // 纳秒
}

time_t cur = time(NULL);        //获取当前时间。
struct timespec t;              //定义timespec 结构体变量t
t.tv_sec = cur + 1;             // 定时1秒
sem_timedwait(&cond, &t);

4. 信号量V操作(加1)

#include <semaphore.h>

int sem_post(sem_t *sem);
功能:
    将信号量的值加 1 并发出信号唤醒等待线程(sem_wait())。
参数:
    sem:信号量的地址。
返回值:
    成功:0
    失败:-1

5. 获取信号量的值

#include <semaphore.h>

int sem_getvalue(sem_t *sem, int *sval);
功能:
    获取 sem 标识的信号量的值,保存在 sval 中。
参数:
    sem:信号量地址。
    sval:保存信号量值的地址。
返回值:
    成功:0
    失败:-1

6. 示例程序

打印机模型用信号量解决:

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

sem_t sem;

void* func1(void* arg)
{
    sem_wait(&sem);
    for(int i=0 ;i<26 ;++i)
    {
        printf("%c ",'a'+i);
        fflush(stdout);
        usleep(100000);
    }
    sem_post(&sem);
    return NULL;
}

void* func2(void* arg)
{
    sem_wait(&sem);
    for(int i=0 ;i<26 ;++i)
    {
        printf("%c ",'A'+i);
        fflush(stdout);
        usleep(100000);
    }
    sem_post(&sem);
    return NULL;
}
int main()
{
    int ret = sem_init(&sem,0,1);
    if(0 != ret)
    {
        printf("%s\n",strerror(ret));
        return 1;
    }

    pthread_t tid1;
    pthread_t tid2;
    ret = pthread_create(&tid1,NULL,func1,NULL);
    if(0 != ret)
    {
        printf("%s\n",strerror(ret));
        return 1;
    }
    ret = pthread_create(&tid2,NULL,func2,NULL);
    if(0 != ret)
    {
        printf("%s\n",strerror(ret));
        return 1;
    }


    ret = pthread_join(tid1,NULL);
    if(0 != ret)
    {
        printf("%s\n",strerror(ret));
        return 1;
    }
    ret = pthread_join(tid2,NULL);
    if(0 != ret)
    {
        printf("%s\n",strerror(ret));
        return 1;
    }

    printf("\n");
    ret = sem_destroy(&sem);
    if(0 != ret)
    {
        printf("%s\n",strerror(ret));
        return 1;
    }
    return 0;
}

生产者消费者模型信号量版:

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

typedef struct node_t
{   
    int val;
    struct node_t* next;
}node_t;

node_t* head = NULL;

sem_t sem1;//设置为4  表示当前有4个空的地方待生产
sem_t sem2;//设置为0  表示可以消费的产品资源

pthread_mutex_t mutex;

void* producer(void *arg)
{
    node_t* newNode = NULL;
    while(1)
    {
        //生产者
        sem_wait(&sem1);

        //对数据结构进行操作要加互斥锁
        pthread_mutex_lock(&mutex);

        newNode = (node_t*)malloc(sizeof(node_t));
        if(newNode == NULL)
        {
            printf("malloc error\n");
            return NULL;
        }
        //清空
        memset(newNode,0,sizeof(node_t));
        newNode->val = rand() % 100 + 1;
        newNode->next = NULL;

        //头插
        newNode->next = head;
        head = newNode;

        printf("生产者生产了 %d\n",newNode->val);

        //解锁
        pthread_mutex_unlock(&mutex);

        sem_post(&sem2);

        sleep(rand() % 5 +1 );
    }


    return NULL;
}

void* customer(void *arg)
{
    while(1)
    {
        sem_wait(&sem2);

        //加锁
        pthread_mutex_lock(&mutex);
    
        //头删
        node_t* temp = head;
        head = head->next;

        printf("消费者消费了 %d\n",temp->val);

        free(temp);
        temp = NULL;
        
        //解锁
        pthread_mutex_unlock(&mutex);

        sem_post(&sem1);
        
        sleep(rand()%5 +1);

    }
    return NULL;
}


int main()
{
    srand((size_t)time(NULL));
    pthread_t tid1,tid2;
    int ret = -1;

    //初始化互斥锁
    ret = pthread_mutex_init(&mutex,NULL);
    if(0 != ret)
    {
        printf("%s\n",strerror(ret));
        return 1;
    }

    //初始化信号量
    ret = sem_init(&sem1,0,4);
    if(0 != ret)
    {
        printf("%s\n",strerror(ret));
        return 1;
    }
    ret = sem_init(&sem2,0,0);
    if(0 != ret)
    {
        printf("%s\n",strerror(ret));
        return 1;
    }

    ret = pthread_create(&tid1,NULL,producer,NULL);
    if(0 != ret)
    {
        printf("%s\n",strerror(ret));
        return 1;
    }
    ret = pthread_create(&tid2,NULL,customer,NULL);
    if(0 != ret)
    {
        printf("%s\n",strerror(ret));
        return 1;
    }

    ret = pthread_join(tid1,NULL);
    if(0 != ret)
    {
        printf("%s\n",strerror(ret));
        return 1;
    }
    ret = pthread_join(tid2,NULL);
    if(0 != ret)
    {
        printf("%s\n",strerror(ret));
        return 1;
    }

    //销毁互斥锁
    pthread_mutex_destroy(&mutex);

    //销毁信号量
    sem_destroy(&sem1);
    sem_destroy(&sem2);

    return 0;
} 

6. 哲学家就餐问题

在这里插入图片描述

示例代码:

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

//五个哲学家,五个锁
pthread_mutex_t mutex[5];

void* eat(void* arg)
{
    int num = (int)arg;

    //左手和右手的筷子
    int left = num+1;
    int right = num;

    if(num == 4)
    {
        left = 0;
        right = num;
    }

    while(1){
        pthread_mutex_lock(&mutex[right]);
        if(pthread_mutex_trylock(&mutex[left]) == 0)
        {
            printf("哲学家%d eating ......\n",num);
            pthread_mutex_unlock(&mutex[left]);
        }
        pthread_mutex_unlock(&mutex[right]);

        sleep(random()%5 +1);
    }
    return NULL;
}

int main()
{
    int ret = -1;
    pthread_t tid[5];

    srand((unsigned int)time(NULL));

    //初始化五个锁
    for(int i=0; i<5 ;++i)
    {
        ret = pthread_mutex_init(&mutex[i],NULL);
        if(0 != ret)
        {
            printf("pthread_mutex_init:%s\n",strerror(ret));
            return 1;
        }
    }

    //创建五个哲学家线程
    for(int i=0 ;i<5 ;++i)
    {
        ret = pthread_create(&tid[i],NULL,eat,(void*)i);
        if(0 != ret)
        {
            printf("pthread_create:%s\n",strerror(ret));
            return 1;
        }
    }

    //回收线程
    for(int i=0 ;i<5 ;++i)
    {
        ret = pthread_join(tid[i],NULL);
        if(0 != ret)
        {
            printf("pthread_join:%s\n",strerror(ret));
            return 1;
        }
    }

    //销毁互斥锁
    for(int i=0 ;i<5 ;++i)
    {
        pthread_mutex_destroy(&mutex[i]);
    }

    return 0;
}

猜你喜欢

转载自blog.csdn.net/qq_41775886/article/details/107583676