C Linux系统编程:线程同步详解,锁与信号量解析

🌐引言

在多线程编程中,多个线程共享进程的地址空间与资源,这显著提升了程序的并发性能,但也伴随着更复杂的数据竞争(Data Race)竞态条件(Race Condition)的问题。为了解决这些问题,就必须利用好线程同步(Thread Synchronization)机制这门技术。而异步则是一种提高线程效率和响应能力的策略模型。

想象一下,你和你的朋友共用一个银行账户,相当于讨论的共享资源,同时用手机App进行取款操作。如果没有规则,你们俩都查询到余额有100元,然后各自取出100元,最终银行却只扣了100元!这就是著名的竞争条件问题。

本文介绍的是继前一篇基本线程控制知识之后的线程相关新板块——线程异步与同步,其中介绍的了什么是线程的同步与异步的概念,同时介绍系统编程中常用的几种锁与信号量,如何去使用这些锁与信号量去做到线程的同步问题。

一. 线程同步与异步

1.1 线程同步

✅核心思想:协调与等待,在临界区同一时间只有一个线程访问

线程同步是指多个线程在访问共享资源时,通过某种协调机制来确保操作的有序性和一致性,防止出现数据不一致或竞态条件。

首先介绍一下什么是临界区

临界区是多线程访问的共享资源,而该资源在同一时刻只能有一个线程单独访问,以确保数据一致性与正确性。

📌 典型场景

  • 多个线程同时对全局变量进行增减操作。
  • 多个线程操作同一个链表、队列等数据结构。

1.2 线程异步

✅核心思想:独立与回调

线程异步是指线程之间独立运行,不需要等待彼此完成,通常通过回调、事件通知等方式通信。

📌 特点

  • 目的是实现高并发、高吞吐,提高进程效率和响应。
  • 不需要频繁加锁,但需注意数据可见性(内存屏障)。

二、锁的概念与种类

📌锁的基本原理很简单:在代码的临界区前加锁,在临界区后解锁。当一个线程持有锁时,其他试图获取同一把锁的线程将会被阻塞或等待,直到锁被释放。这就确保了每个线程访问临界区时的单一性。

1. 互斥锁 pthread_mutex_t

pthread_mutex_t 是一个定义在头文件<pthreadtypes.h>中的联合体类型的别名,其声明如下。

typedef union
{
  struct __pthread_mutex_s __data;
  char __size[__SIZEOF_PTHREAD_MUTEX_T];
  long int __align;
} pthread_mutex_t;
  • 特点:最常用的一种锁。它提供了独占的、互斥的访问。一次只有一个线程能持有互斥锁。

  • 适用场景:保护大部分临界区,特别是临界区代码执行时间较长的情况,如文件IO、复杂计算等。

  • 常用API及使用锁流程

    #include <pthread.h>
    
    // 动态初始化
    int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
    // 静态初始化(宏)
    pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
    
    // 加锁(如果锁被占用,则阻塞)
    int pthread_mutex_lock(pthread_mutex_t *mutex);
    // 尝试加锁(如果锁被占用,立即返回错误EBUSY,不阻塞)
    int pthread_mutex_trylock(pthread_mutex_t *mutex);
    // 解锁
    int pthread_mutex_unlock(pthread_mutex_t *mutex);
    // 销毁
    int pthread_mutex_destroy(pthread_mutex_t *mutex);
    
互斥锁初始化注意事项

PTHREAD_MUTEX_INITIALIZER 是 POSIX 线程(Pthreads)库中定义的一个宏,用于静态初始化互斥锁(mutex)。这个宏为互斥锁提供了一个初始状态,使其准备好被锁定和解锁,而不需要在程序运行时显式调用初始化函数。

当我们使用 PTHREAD_MUTEX_INITIALIZER 初始化互斥锁时,实际上是将互斥锁设置为默认属性和未锁定状态这种初始化方式适用于简单的同步问题,我们可以通过以下代码初始化互斥锁

static pthread_mutex_t counter_mutex = PTHREAD_MUTEX_INITIALIZER;
实例代码
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#define THREAD_COUNT 20000

static pthread_mutex_t counter_mutex = PTHREAD_MUTEX_INITIALIZER;

/**
 * @brief 对传入值累加1
 *
 * @param argv 传入指针
 * @return void* 无返回值
 */
void *add_thread(void *argv)
{
    int *p = argv;
    // 累加之前加锁,此时其他获取该锁的线程都会被阻塞
    pthread_mutex_lock(&counter_mutex);
    (*p)++;
    // 累加之后释放锁
    pthread_mutex_unlock(&counter_mutex);
    return (void *)0;
}

int main()
{
    pthread_t pid[THREAD_COUNT];

    int num = 0;

    // 用20000个线程对num作累加
    for (int i = 0; i < THREAD_COUNT; i++)
    {
        pthread_create(pid + i, NULL, add_thread, &num);
    }

    // 等带所有线程结束
    for (int i = 0; i < THREAD_COUNT; i++)
    {
        pthread_join(pid[i], NULL);
    }

    // 打印累加结果
    printf("累加结果:%d\n", num);

    return 0;
}

2. 读写锁 pthread_rwlock_t

📌原理如下:

        读操作:在读写锁的控制下,多个线程可以同时获得读锁。这些线程可以并发地读取共享资源,但它们的存在阻止了写锁的授予。

        写操作:如果至少有一个读操作持有读锁,写操作就无法获得写锁。写操作将会阻塞,直到所有的读锁都被释放。

声明如下:

typedef union
{
  struct __pthread_rwlock_arch_t __data;
  char __size[__SIZEOF_PTHREAD_RWLOCK_T];
  long int __align;
} pthread_rwlock_t;
  • 特点:更高的并发度。它允许多个线程同时读,但只允许一个线程。写操作是独占的。

  • 适用场景读多写少的场景,如缓存、配置项管理。

  • 常用API:

    #include <pthread.h>
    
    int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr);
    pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
    
    // 读加锁(多个读者可以同时获取)
    int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
    // 写加锁(独占)
    int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
    int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
    int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
实例代码:
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

// 静态创建读写锁
// static pthread_rwlock_t rwlock = __PTHREAD_RWLOCK_INITIALIZER;
// 动态创建一个读写锁,必须要手动创建同时手动销毁
pthread_rwlock_t rwlock;
int shared_data = 0;

void *lock_reader(void* arg)
{
    // 读写锁中的读操作是可以有多个线程统一读取的
    // 获取读写
    printf("%s要获取读锁\n",(char*)arg);
    pthread_rwlock_rdlock(&rwlock);
    
    printf("当前线程%s,shared_data = %d\n",(char*)arg,shared_data);
    pthread_rwlock_unlock(&rwlock);
    printf("%s要释放读锁\n",(char*)arg);
}

void *lock_writer(void* arg)
{
    // 在线程中使用读写锁,在写线程中就同步线程
    printf("%s要获取写锁\n",(char*)arg);
    pthread_rwlock_wrlock(&rwlock);
    int tmp = shared_data + 1;
    sleep(1);
    shared_data = tmp;
    printf("当前线程%s,shared_data = %d\n",(char*)arg,shared_data);
    // 释放读写锁
    pthread_rwlock_unlock(&rwlock);
    printf("%s要释放读锁\n",(char*)arg);
}

int main(int argc, char const *argv[])
{
    // 因为是动态创建读写锁,所以要显示初始化锁
    pthread_rwlock_init(&rwlock,NULL);

    pthread_t writer1,writer2,reader1,reader2,reader3,reader4;
    // 休眠等待
    // sleep(3);
    pthread_create(&writer1,NULL,lock_writer,"writer1");
    pthread_create(&reader1,NULL,lock_reader,"reader1");
    pthread_create(&reader2,NULL,lock_reader,"reader2");
    // 两组分开进行线程的运行
    // 由于读线程是可以并发的,而写线程之间是互斥的,导致读线程结束后会一直抢占锁,造成写饥饿
    pthread_create(&writer2,NULL,lock_writer,"writer2");
    pthread_create(&reader3,NULL,lock_reader,"reader3");
    pthread_create(&reader4,NULL,lock_reader,"reader4");
    // 主线程等待所有线程结束
    pthread_join(writer1,NULL);
    pthread_join(writer2,NULL);
    pthread_join(reader1,NULL);
    pthread_join(reader2,NULL);
    pthread_join(reader3,NULL);
    pthread_join(reader4,NULL);
    // 动态创建锁,需要显示销毁读写锁
    pthread_rwlock_destroy(&rwlock);
    return 0;
}

可以看到代码的输出结果。

可以看到上面的实例代码不能正确的输出我们想要的结果,这就是读写锁独有的写饥饿问题。

读写锁独有的问题:写饥饿
 ① 问题描述

        读写锁的写饥饿问题( Writer Starvation )是指在使用读写锁时,写线程可能无限期地等待获取写锁,因为读线程持续地获取读锁而不断地推迟写线程的执行。这种情况通常在读操作远多于写操作时出现。

② 解决方案

Linux 提供了可以修改的属性 pthread_rwlockattr_t,默认情况下,属性中指定的策略为“读优先”,当写操作阻塞时,读线程依然可以获得读锁,从而在读操作并发较高时导致写饥饿问题。我们可以尝试将策略更改为“写优先”当写操作阻塞时,读线程无法获取锁,避免了写线程持有锁的时间持续延长,使得写线程获取锁的等待时间显著降低,从而避免写饥饿问题。

声明如下:
typedef union
{
  char __size[__SIZEOF_PTHREAD_RWLOCKATTR_T];
  long int __align;
} pthread_rwlockattr_t;


#include <pthread.h>

/**
 * @brief 用所有属性的默认值初始化attr指向的属性对象
 * 
 * @param attr 读写锁属性对象指针
 * @return int 成功返回0,失败返回错误码
 */
int pthread_rwlockattr_init(pthread_rwlockattr_t *attr);


/**
 * @brief 将attr指向的属性对象中的"锁类型"属性设置为pref规定的值
 * 
 * @param attr 读写锁属性对象指针
 * @param pref 希望设置的锁类型,可以被设置为以下三种取值的其中一种
 * PTHREAD_RWLOCK_PREFER_READER_NP: 默认值,读线程拥有更高优先级。当存在阻塞的写线程时,读线程仍然可以获得读写锁。只要不断有新的读线程,写线程将一直保持"饥饿"。
 * PTHREAD_RWLOCK_PREFER_WRITER_NP: 写线程拥有更高优先级。这一选项被glibc忽略。
 * PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP: 写线程拥有更高优先级,在当前系统环境下,它是有效的,将锁类型设置为该值以避免写饥饿。
 * @return int 成功返回0,失败返回非零的错误码
 */
int pthread_rwlockattr_setkind_np(pthread_rwlockattr_t *attr, int pref);
实例解决:
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

// 静态创建读写锁
// static pthread_rwlock_t rwlock = __PTHREAD_RWLOCK_INITIALIZER;
// 动态创建一个读写锁,必须要手动创建同时手动销毁
pthread_rwlock_t rwlock;
int shared_data = 0;

void *lock_reader(void* arg)
{
    // 读写锁中的读操作是可以有多个线程统一读取的
    // 获取读写
    printf("%s要获取读锁\n",(char*)arg);
    pthread_rwlock_rdlock(&rwlock);
    
    printf("当前线程%s,shared_data = %d\n",(char*)arg,shared_data);
    pthread_rwlock_unlock(&rwlock);
    printf("%s要释放读锁\n",(char*)arg);
}

void *lock_writer(void* arg)
{
    // 在线程中使用读写锁,在写线程中就同步线程
    printf("%s要获取写锁\n",(char*)arg);
    pthread_rwlock_wrlock(&rwlock);
    int tmp = shared_data + 1;
    sleep(1);
    shared_data = tmp;
    printf("当前线程%s,shared_data = %d\n",(char*)arg,shared_data);
    // 释放读写锁
    pthread_rwlock_unlock(&rwlock);
    printf("%s要释放读锁\n",(char*)arg);
}

int main(int argc, char const *argv[])
{
    // 创建读写锁属性对象
    pthread_rwlockattr_t attr;
    pthread_rwlockattr_init(&attr);
    // 修改读锁属性对象的参数,设置写优先
    pthread_rwlockattr_setkind_np(&attr,PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP);
    // 因为是动态创建读写锁,所以要显示初始化锁,同时写入锁属性,第二参数是属性
    pthread_rwlock_init(&rwlock,&attr);

    pthread_t writer1,writer2,reader1,reader2,reader3,reader4;
    // 休眠等待
    // sleep(3);
    pthread_create(&writer1,NULL,lock_writer,"writer1");
    pthread_create(&reader1,NULL,lock_reader,"reader1");
    pthread_create(&reader2,NULL,lock_reader,"reader2");
    // 两组分开进行线程的运行
    // 由于读线程是可以并发的,而写线程之间是互斥的,导致读线程结束后会一直抢占锁,造成写饥饿
    pthread_create(&writer2,NULL,lock_writer,"writer2");
    pthread_create(&reader3,NULL,lock_reader,"reader3");
    pthread_create(&reader4,NULL,lock_reader,"reader4");
    // 主线程等待所有线程结束
    pthread_join(writer1,NULL);
    pthread_join(writer2,NULL);
    pthread_join(reader1,NULL);
    pthread_join(reader2,NULL);
    pthread_join(reader3,NULL);
    pthread_join(reader4,NULL);
    // 动态创建锁,需要显示销毁读写锁
    pthread_rwlock_destroy(&rwlock);
    return 0;
}

3. 自旋锁

在 Linux 内核中,自旋锁是一种用于多处理器系统中的低级同步机制,主要用于保护非常短的代码段或数据结构,以避免多个处理器同时访问共享资源。自旋锁相对于其他锁的优点是它们在锁被占用时会持续检查锁的状态(即“自旋”),而不是让线程进入休眠。这使得自旋锁在等待时间非常短的情况下非常有效,因为它避免了线程上下文切换的开销。

自旋锁主要用于内核模块或驱动程序中,避免上下文切换的开销。不能在用户空间使用。

三、条件变量pthread_cond_t

定义:

        pthread_cond_t 是一个条件变量,它是线程间同步的另一种机制。与pthread_mutex_t相同,它也定义在头文件<pthreadtypes.h>中,其声明如下。

typedef union
{
  struct __pthread_cond_s __data;
  char __size[__SIZEOF_PTHREAD_COND_T];
  __extension__ long long int __align;
} pthread_cond_t;

条件变量允许线程挂起执行并释放已持有的互斥锁,等待某个条件变为真。条件变量总是需要与互斥锁一起使用,以避免出现竞态条件。

重要API

#include <pthread.h>

/**
 * @brief 调用该方法的线程必须持有mutex锁。调用该方法的线程会阻塞并临时释放mutex锁,并等待其他线程调用pthread_cond_signal或pthread_cond_broadcast唤醒。被唤醒后该线程会尝试重新获取mutex锁。
 * 
 * @param cond 指向条件变量的指针。条件变量用于等待某个条件的发生。通过某一cond等待的线程需要通过同一cond的signal唤醒
 * @param mutex 与条件变量配合使用的互斥锁的指针。在调用pthread_cond_wait之前,线程必须已经获得了这个互斥锁。
 * @return int 成功时返回0;失败时返回错误码,而非-1。错误码可能包括EINVAL、EPERM等,具体取决于错误的性质。
 */
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);

/**
 * @brief 同pthread_cond_wait相似,但是它添加了超时机制。如果在指定的abstime时间内条件变量没有被触发,函数将返回一个超时错误(ETIMEDOUT)。
 * 
 * @param cond 指向条件变量的指针
 * @param mutex 与条件变量配合使用的互斥锁的指针
 * @param abstime 指向timespec结构的指针,表示等待条件变量的绝对超时时间。timespec结构包含秒和纳秒两部分,指定了从某一固定点(如UNIX纪元,1970年1月1日)开始的时间。
 * @return int 成功时返回0;如果超时则返回ETIMEDOUT;其他错误情况返回相应的错误码。
 */
int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);

/**
 * @brief 唤醒因cond而阻塞的线程,如果有多个线程因为cond阻塞,那么随机唤醒一个。如果没有线程在等待,这个函数什么也不做。
 * 
 * @param cond 指向条件变量的指针
 * @return int 成功时返回0;失败时返回错误码
 */
int pthread_cond_signal(pthread_cond_t *cond);

/**
 * @brief 唤醒所有正在等待条件变量cond的线程。如果没有线程在等待,这个函数什么也不做。
 * 
 * @param cond 指向条件变量的指针。
 * @return int 成功时返回0;失败时返回错误码。
 */
int pthread_cond_broadcast(pthread_cond_t *cond);
说明:

使用条件变量时,通常涉及到一个或多个线程等待“条件变量”代表的条件成立,而另外一些线程在条件成立时触发条件变量。

条件变量的使用必须与互斥锁配合,以保证对共享资源的访问是互斥的。

条件变量提供了一种线程间的通信机制,允许线程以无竞争的方式等待特定条件的发生。

示例代码(使用条件变量实现生产者消费者模型)

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

#define BUFFER_SIZE 5

int buffer[BUFFER_SIZE];
int count = 0;

// 初始化互斥锁
static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
// 初始化条件变量
static pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

/*
 向buf中写入数据
*/
void * producer(void * arg)
{
    int item = 1;
    // 使用共同的条件变量,使用互斥锁,首先获取锁
    pthread_mutex_lock(&mutex);
    while (1)
    {
        // 如果缓冲区写满,使用条件变量暂停当前线程
        if (count == BUFFER_SIZE)
        {
            // 暂停线程
            pthread_cond_wait(&cond,&mutex); 
            // 唤醒消费者
            pthread_cond_signal(&cond);   
        }
        // 缓冲区还没有满,则将item写入缓冲区
        buffer[count++] = item++;
        printf("生产者线程向消费者线程发送数字:%d\n",buffer[count - 1]);
    }
    // 最后释放锁
    pthread_mutex_unlock(&mutex);
}

/*
 向buf中读取数据
*/
void * consumer(void * arg)
{
    // 使用共同的变量,使用互斥锁,首先获取锁
    pthread_mutex_lock(&mutex);
    while (1)
    {
        if (count == 0)
        {
            // 缓冲区没有可以读取的消息,暂停线程
            pthread_cond_wait(&cond,&mutex);
            // 通知生产者可以继续写了
            pthread_cond_signal(&cond);
        }
        // 缓冲区中有可以读取的消息
        printf("消费者收到生产者发送的数字:%d\n",buffer[--count]);
        
    }
    // 最后释放锁
    pthread_mutex_unlock(&mutex);
}

int main(int argc, char const *argv[])
{
    // 创建两个线程,一个向buffer中写入书记,一个从buffer中读取数据
    pthread_t producer_thread,consumer_thread;

    // 创建写入数据线程
    pthread_create(&producer_thread,NULL,producer,NULL);
    pthread_create(&producer_thread,NULL,consumer,NULL);
    
    pthread_join(producer_thread,NULL);
    pthread_join(consumer_thread,NULL);
    return 0;
}

🔄 工作流程图解

线程A:检查 condition → 不满足 → cond_wait(解锁+等待)
                             ↓
线程B:修改共享数据 → 设置 condition 为真 → cond_signal()
                             ↓
线程A:被唤醒 → 重新获取 mutex → 返回 → 再次检查 condition

四、信号量(Semaphore)

📌 什么是信号量?

由荷兰科学家 Dijkstra 提出,是一种计数器 + 等待队列的同步机制,用于控制对有限资源的访问。

类比:停车场有 N 个车位,每辆车进入减1,离开加1。

它不仅仅用于互斥,更核心的思想是管理一个有限的资源池

  • 核心:信号量维护了一个非负整数计数器。

  • 操作

    • P操作 (sem_waitsem_trywait):意为“尝试获取资源”。计数器减1。如果计数器值已为0,则调用线程会阻塞,直到有其他线程增加计数器(释放资源)。

    • V操作 (sem_post):意为“释放资源”。计数器加1。如果有其他线程正在因等待这个资源而阻塞,它们中的一个将被唤醒。

信号量可以从两个维度进行分类:

  1. 按功能:二进制信号量、计数信号量。

  2. 按作用域与生命周期:无名信号量、有名信号量。

1. 按功能分类

a. 二进制信号量
  • 计数器值:只能是 0 或 1

  • 功能:非常类似于一个互斥锁。用于实现互斥访问,保护临界区。

  • 初始值:通常初始化为 1,表示资源可用。

b. 计数信号量
  • 计数器值:可以大于 1 的任何非负整数。

  • 功能:用于控制对多个 identical(相同的)资源的访问。例如:连接池中有10个连接,缓冲区有N个空位。

  • 初始值:初始化为可用资源的数量(例如,缓冲区的最大容量)。

2. 按作用域与生命周期分类

这是理解信号量在不同场景下使用的关键。

a. 无名信号量(匿名信号量 / Unnamed Semaphore)
  • 内存位置:位于内存中,通常是程序的全局变量或堆内存。

  • 共享范围

    • 线程间共享:默认方式。所有线程都能看到存放该信号量的内存地址。

    • 进程间共享也可以,但需要将信号量放在一个所有相关进程都能访问的共享内存区域中。

  • 生命周期:与它所处的内存区域共存亡。如果是全局变量,则随程序结束而销毁。如果在共享内存中,则随共享内存的销毁而销毁。

  • API:使用 sem_init() 和 sem_destroy()

    #include <semaphore.h>
    
    // 初始化无名信号量
    // pshared: 0 -> 线程间共享;非0 -> 进程间共享(需放在共享内存)
    // value:   信号量的初始值
    int sem_init(sem_t *sem, int pshared, unsigned int value);
    
    // 销毁信号量
    int sem_destroy(sem_t *sem);
    b. 有名信号量(命名信号量 / Named Semaphore)
  • 标识方式:通过一个名字(一个以 / 开头的字符串,如 /my_sem)来标识,而不是内存地址。这个名字在文件系统中通常有一个对应的条目(在 /dev/shm 或类似位置),但其实现是内核级的,可移植代码不应将其当作普通文件来操作

  • 共享范围主要用于进程间共享。任何知道该名字的无关进程都可以打开并使用同一个信号量。

  • 生命周期内核持久性。即使创建它的进程退出,信号量仍然存在,除非被显式地卸载或系统重启。这既是优点(允许无关进程同步),也是风险(可能导致资源泄漏,需要小心管理)。

  • API:使用 sem_open()sem_close()sem_unlink()

    #include <semaphore.h>
    #include <fcntl.h>       // For O_* constants
    
    // 打开或创建一个有名信号量
    // name:  信号量的名字(如 "/my_semaphore")
    // oflag: O_CREAT, O_EXCL 等标志
    // mode:  文件权限(如0644),在创建时需要
    // value: 初始值,在创建时需要
    sem_t *sem_open(const char *name, int oflag, ... /* mode_t mode, unsigned int value */ );
    
    // 关闭信号量。进程结束时也应调用,但不会销毁信号量本身
    int sem_close(sem_t *sem);
    
    // 从系统中删除信号量名字,当所有进程都关闭它后,内核会销毁它
    int sem_unlink(const char *name);

通用信号量操作API

以下API对有名无名信号量都适用:

#include <semaphore.h>

// P操作 (等待,获取资源)
int sem_wait(sem_t *sem);    // 阻塞直到资源可用
int sem_trywait(sem_t *sem); // 非阻塞,立即返回
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout); // 带超时等待

// V操作 (发布,释放资源)
int sem_post(sem_t *sem);

// 获取当前信号量的值(注意:值可能在被获取后立即被其他线程改变,此函数主要用于调试)
int sem_getvalue(sem_t *sem, int *sval);

信号量实现生产者消费者模型

无名信号量

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

sem_t *full;
sem_t *empty;

int shared_num;

int rand_num()
{
    srand(time(NULL));

    return rand();
}

void* producer(void* argv)
{
    for (int i = 0; i < 5; i++)
    {
        /* code */
        sem_wait(empty);
        printf("\n=====> 第%d轮数据传输 <======\n",i);
        sleep(1);
        shared_num = rand_num();
        printf("producer has sent data \n");
        sem_post(full);
    }
    
}

void* consumer(void* argv)
{
    for (int i = 0; i < 5; i++)
    {
        /* code */
        sem_wait(full);
        printf("consumer has sent data \n");
        printf("the shared_num is %d\n",shared_num);
        sleep(1);
        sem_post(empty);
    }
    
}
int main()
{
    full = malloc(sizeof(sem_t));
    empty = malloc(sizeof(sem_t));

    sem_init(empty,0,1);
    sem_init(full,0,0);

    pthread_t producer_id,consumer_id;
    pthread_create(&producer_id,NULL,producer,NULL);
    pthread_create(&consumer_id,NULL,consumer,NULL);

    pthread_join(producer_id,NULL);
    pthread_join(consumer_id,NULL);

    sem_destroy(empty);
    sem_destroy(full);

    return 0;
}

有名信号量

#include <stdio.h>
#include <semaphore.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>

int main(int argc, char const *argv[])
{
    // char *shm_name = "unamed_sem_shm";
    // // 创建共享内存
    // int fd = shm_open(shm_name,O_CREAT | O_RDWR,0666);
    // // 调整共享内存大小
    // ftruncate(fd,sizeof(sem_t));
    // // 完成映射,得到信号量
    // sem_t *sem = mmap(NULL,sizeof(sem_t),PROT_READ | PROT_WRITE, MAP_SHARED,fd,0);
    
    // 初始化信号量
    char *sem_name = "/named_sem";
    // sem_init(sem,1,0);
    sem_t *sem = sem_open(sem_name,O_CREAT,0666,0);

    // 创建父子进程
    pid_t pid = fork();

    if (pid < 0)
    {
        perror("fork");
    }else if (pid == 0)
    {
        /*子进程*/
        sleep(1);
        printf("这是子进程\n");
        sem_post(sem);
    }else
    {
        // 父进程
        // 子进程睡眠1s 保证父进程执行wait操作
        // sem_wait sem = 0 无法减一  父进程被阻塞只能等待子进程释放信号量
        sem_wait(sem);
        printf("这是父进程\n");
        waitpid(pid,NULL,0);
    }

    // 回收资源
    if (pid > 0)
    {
        // /*由父进程回收摧毁信号量*/
        // if (sem_destroy(sem) == -1)
        // {
        //     /* code */
        //     perror("sem_destroy");
        // }
        sem_close(sem);

        if (sem_unlink(sem_name) == -1)
        {
            /* code */
            perror("sem_unlink");
        }
        
    }

//     // 父子进程都需要关闭文件描述符
//     if (munmap(sem,sizeof(sem)) == -1)
//     {
//         /* code */
//         perror("munmap");
//     }
//     if (close(fd) == -1)
//     {
//         /* code */
//         perror("close");
//     }
    
    
//    if (pid > 0)
//    {
//         /* code */
//         if (shm_unlink(shm_name))
//         {
//             /* code */
//             perror("shm_unlink");
//         }
//    }
    return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值