Linux快速入门之 线程同步(16) #重点知识点 ,迈向高手的第一步

本文详细介绍了多线程编程中的线程同步技术,包括互斥锁、读写锁、条件变量和信号量。通过实例展示了如何使用这些机制来防止数据混乱,确保线程安全。互斥锁通过锁定代码块实现串行访问,读写锁允许并发读取但独占写入,条件变量用于线程的阻塞与唤醒,而信号量则用于控制资源的数量,协调生产者和消费者的行为。文章还探讨了死锁问题及其避免策略,并提供了具体的示例代码来说明各种同步机制的用法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

线程同步

1.1线程同步概念

假设有 4 个线程 A、B、C、D,当前一个线程 A 对内存中的共享资源进行访问的时候,其他线程 B, C, D 都不可以对这块内存进行操作,直到线程 A 对这块内存访问完毕为止,B,C,D 中的一个才能访问这块内存,剩余的两个需要继续阻塞等待,以此类推,直至所有的线程都对这块内存操作完毕。 线程对内存的这种访问方式就称之为线程同步,通过对概念的介绍,我们可以了解到所谓的同步并不是多个线程同时对内存进行访问,而是按照先后顺序依次进行的。

1.1.1 为什么需要线程同步

设计两个线程交替数数(每个线程数10个数,交替数到20):

#include <stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include <pthread.h>
#include <sys/stat.h>
#include <sys/types.h>

#define MAX 10

//全局变量
int number;

//线程处理函数
void* funcA_num(void* arg)
{
    for (int i =0;i<MAX;i++)
    {
        int cur = number;
        cur++;
       usleep(10);
        number =cur;
        printf("Thread A , ID =%ld ,number =%d\n",pthread_self(),cur);
    }
    return NULL;
}

void* funcB_num(void* arg)
{
    for (int i =0;i<MAX;i++)
    {
        int cur = number;
        cur++;
        number =cur;
        printf("Thread B , ID =%ld ,number =%d\n",pthread_self(),cur);
    }
    return NULL;
}

int main()
{
    pthread_t p1,p2;
    pthread_create(&p1,NULL,funcA_num,NULL);
    pthread_create(&p2,NULL,funcB_num,NULL);

    //阻塞,资源回收
    pthread_join(p1,NULL);
    pthread_join(p2,NULL);

    return 0;
}
liu@liu-Ubuntu:~/vscode$ gcc tong.c -lpthread -o tong
liu@liu-Ubuntu:~/vscode$ ./tong 
Thread A , ID =140377753675520 ,number =1
Thread A , ID =140377753675520 ,number =3
Thread A , ID =140377753675520 ,number =4
Thread A , ID =140377753675520 ,number =5
Thread A , ID =140377753675520 ,number =6
Thread A , ID =140377753675520 ,number =7
Thread A , ID =140377753675520 ,number =8
Thread A , ID =140377753675520 ,number =9
Thread A , ID =140377753675520 ,number =10
Thread A , ID =140377753675520 ,number =11
Thread B , ID =140377745282816 ,number =2
Thread B , ID =140377745282816 ,number =12
Thread B , ID =140377745282816 ,number =13
Thread B , ID =140377745282816 ,number =14
Thread B , ID =140377745282816 ,number =15
Thread B , ID =140377745282816 ,number =16
Thread B , ID =140377745282816 ,number =16
Thread B , ID =140377745282816 ,number =17
Thread B , ID =140377745282816 ,number =18
Thread B , ID =140377745282816 ,number =19
Thread B , ID =140377745282816 ,number =20

可以看出虽然每个线程内部循环了 10 次每次数一个数,,通过输出的结果可以看到,有些数字被重复数了多次,其原因就是没有对线程进行同步处理,造成了数据的混乱。

两个线程在数数的时候分时复用时间片,且其中一个线程调用了sleep()函数导致CPU时间片没用完就被迫挂起了 ,这样就能让CPU的上下文切换 (保存当前状态,下次继续运行的时候需要加载的状态)更加频繁的出现,更容易出现数据混乱的现象;

在这里插入图片描述

CPU对应的寄存器、一级缓存、二级缓存、三级缓存是独占的,用于存储处理的数据和线程的状态信息,数据被 CPU处理完成需要再次写入物理内存中,物理内存数据也可以通过文件IO操作写入磁盘中;

在测试程序中两个线程共用全局变量 number 当线程变成运行态之后开始数数,从物理内存加载数据,让后将数据放到 CPU 进行运算,最后将结果更新到物理内存中。如果数数的两个线程都可以顺利完成这个流程,那么得到的结果肯定是正确的。

如果线程 A 执行这个过程期间就失去了 CPU 时间片,线程 A 被挂起了最新的数据没能更新到物理内存。线程 B 变成运行态之后从物理内存读数据,很显然它没有拿到最新数据,只能基于旧的数据往后数,然后失去 CPU 时间片挂起。线程 A 得到 CPU 时间片变成运行态,第一件事儿就是将上次没更新到内存的数据更新到内存,但是这样会导致线程 B 已经更新到内存的数据被覆盖,活儿白干了,最终导致有些数据会被重复数很多次。

1.1.2 同步方式

为了放置多线程访问共享资源出现数据混乱的问题,需要进行线程同步。共享资源通常为全局区资源或堆区变量 ,这些变量对应的共享资源被称为临界资源;

常用的线程同步的四种方式: 互斥锁、读写锁、条件变量、信号量

在这里插入图片描述

找到临界资源后,再找临界资源相关的上下文代码,得到一个代码块该代码块称为临界区

临界区越小越好 ,在确定号临界区之后,开始进行线程同步;

  • 在临界区代码的上面,添加加锁函数,对临界区加锁。
    1. 无论那个线程调用这句代码,就会把锁锁上,其他线程只能阻塞在锁上;
  • 在临界区代码的下边,添加解锁函数,对临界区解锁。
    1. 出临界区的线程会将锁定的那把锁打开,其他抢到锁的线程就可以进入临界区了;
  • 通过锁机制能保证临界区代码最多只能同时又一个线程访问,于是并行访问便成了串行访问;

1.2 互斥锁

1.2.1 互斥锁函数

互斥锁是线程同步最常用的一种方式, 通过互斥锁可以锁定一个代码块,被锁定的代码块所有线程只能顺序执行(不能并行处理) ,但是执行效率较低,因为默认临界区多个线程可以并行处理,现在只能串行处理;

在Linux中互斥锁的类型为 pthread_mutex_t ,创建一个该类型对象就得到一把互斥锁:

pthread_mutex_t mutex;

创建的锁对象保存了当前这把锁的状态信息: 锁是否锁定,如果是锁定状态,还会记录下给这把锁加锁的线程信息(线程ID)。一个互斥锁变量只能被一个线程锁定,被锁定之后其他线程在对互斥锁变量加锁会被阻塞,直到互斥锁被解锁 ,被阻塞的线程才被解除阻塞;一般情况下,每一个共享资源对应一把互斥锁,锁的个数和线程的个数无关。

Linux提供互斥锁操作函数,如果函数调用成功返回 0 , 调用失败会返回错误号;

//初始化互斥锁
//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
//修改互斥锁的状态,将其**设定为锁定状态**  ,该参数被写入到 参数mutex中
int pthread_mutex_lock(pthread_mutex_t *mutex);

该函数被调用,会先判断参数mutex互斥锁中状态是否为锁定状态;

  1. 没有被锁定,该线程会被加锁成功,且这个锁对象会记录那个线程加锁成功;
  2. 如果被锁定了,其他线程加锁失败,这些线程会阻塞在这把锁上;
  3. 当把这把锁解开之后,这些阻塞在锁上的线程就解除阻塞了,并通过竞争对这把锁加锁,没抢到锁的线程继续阻塞;
//尝试加锁
int pthread_mutex_trylock(pthread_mutex_t *mutex);

该函数被调用对互斥锁有两种情况:

  1. 如果这把锁没有被锁定 , 线程加锁成功;
  2. 如果锁变量被锁定,调用该函数加锁的线程不会被阻塞,加锁失败直接返回错误号;
//解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);

不是所有的线程都可以对互斥锁解锁,哪个线程加的锁,哪个线程才能解锁成功。

1.2.2 互斥锁使用

将上面多线程交替计数的例子修改一下,使用互斥锁进行线程同步,两个线程共同操作同一个全局变量:

#include <stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include <pthread.h>
#include <sys/stat.h>
#include <sys/types.h>

#define MAX 10

//全局变量
int number;
//全局互斥锁
pthread_mutex_t mutex;

//线程处理函数
void* funcA_num(void* arg)
{
    for (int i =0;i<MAX;++i)
    {
        //如果线程A加锁成功 ,不阻塞
        //如果线程B加锁成功 ,不阻塞
        pthread_mutex_trylock(&mutex);
        int cur = number;
        cur++;
        number =cur;
        //解锁
        pthread_mutex_unlock(&mutex);
        printf("Thread A , ID =%ld ,number =%d\n",pthread_self(),number);
    }
    return NULL;
}

void* funcB_num(void* arg)
{
    for (int i =0;i<MAX;++i)
    {
        //如果线程A加锁成功 ,不阻塞
        //如果线程B加锁成功 ,不阻塞
        pthread_mutex_trylock(&mutex);
        int cur = number;
        cur++;
        number =cur;
        pthread_mutex_unlock(&mutex);
        printf("Thread B , ID =%ld ,number =%d\n",pthread_self(),number);
    }
    return NULL;
}

int main()
{
    pthread_t p1,p2;
    //初始化互斥锁
    pthread_mutex_init(&mutex, NULL);

    pthread_create(&p1,NULL,funcA_num,NULL);
    pthread_create(&p2,NULL,funcB_num,NULL);

    //阻塞,资源回收
    pthread_join(p1,NULL);
    pthread_join(p2,NULL);

    // 销毁互斥锁
    // 线程销毁之后, 再去释放互斥锁
    pthread_mutex_destroy(&mutex);

    return 0;
}

1.3 死锁

当多个线程访问共享资源,需要加锁,如果使用锁不当会造成死锁现象

死锁的后果: 所有线程都被阻塞,且线程的阻塞无法解开(可以解锁的线程也被阻塞了)。

造成死锁的场景:

  • 加锁后忘记解锁:
// 场景1
void func()
{
    for(int i=0; i<6; ++i)
    {
        // 当前线程A加锁成功, 当前循环完毕没有解锁, 在下一轮循环的时候自己被阻塞了
        // 其余的线程也被阻塞
    	pthread_mutex_lock(&mutex);
    	....
    	.....
        // 忘记解锁
    }
}

// 场景2
void func()
{
    for(int i=0; i<6; ++i)
    {
        // 当前线程A加锁成功
        // 其余的线程被阻塞
    	pthread_mutex_lock(&mutex);
    	....
    	.....
        if(xxx)
        {
            // 函数退出, 没有解锁(解锁函数无法被执行了)
            return ;
        }
        
        pthread_mutex_lock(&mutex);
    }
}
  • 反复加锁,造成死锁
void func()
{
    for(int i=0; i<6; ++i)
    {
        // 当前线程A加锁成功
        // 其余的线程阻塞
    	pthread_mutex_lock(&mutex);
        // 锁被锁住了, A线程阻塞
        pthread_mutex_lock(&mutex);
    	....
    	.....
        pthread_mutex_unlock(&mutex);
    }
}

// 隐藏的比较深的情况
void funcA()
{
    for(int i=0; i<6; ++i)
    {
        // 当前线程A加锁成功
        // 其余的线程阻塞
    	pthread_mutex_lock(&mutex);
    	....
    	.....
        pthread_mutex_unlock(&mutex);
    }
}

void funcB()
{
    for(int i=0; i<6; ++i)
    {
        // 当前线程A加锁成功
        // 其余的线程阻塞
    	pthread_mutex_lock(&mutex);
        funcA();		// 重复加锁
    	....
    	.....
        pthread_mutex_unlock(&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锁

在这里插入图片描述

使用多线程编程的时候,如何避免死锁:

  1. 避免多次锁定,多检测;
  2. 对共享资源访问完毕之后,一定要解锁 ,或在加锁使用的时候使用 trylock;
  3. 如果程序中有多把锁,可以控制对锁的访问顺序(顺序访问有时无法实现),或可以在对其他互斥锁加锁之钱,先释放当前线程拥有的互斥锁;
  4. 项目中引入专门用于检测死锁的模块

1.4 读写锁

1.4.1 读写锁函数

读写锁事互斥锁的升级版, 在读操作的时候可以提高程序的执行效率,如果所有的线程都是做读操作,那么读是并行的,但是使用互斥锁,读操作是串行的。

读写锁类型为 :pthread_rwlock_t :

pthread_rwlock_t rwlock;

该锁既可锁住读操作,也能锁定写操作。在该锁中记录了以下信息:

  1. 锁的状态 :锁定 / 打开
  2. 锁定的是什么操作:读操作 / 写操作 ,使用读写锁锁定了读操作,需要先解锁才能去锁定写操作,反之同理;
  3. 那个线程讲锁锁上了;

读写锁的特点:

  1. 使用读锁 锁定临界区,线程对临界区的访问是并行的读锁时共享的;
  2. 使用写锁 锁定临界区,线程对临界区的访问是串行的写锁是独占的;
  3. 使用读写锁分别对两个临界区加了读锁和写锁,两个线程要同时访问者两个临界区,访问写锁临界区的线程继续运行,访问读锁临界区的线程阻塞,因为写锁比读锁的优先级高;

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

Linux系统下读写锁操作函数原型如下,函数调用成功返回 0, 失败返回错误号;

#include <pthread.h>
pthread_rwlock_t rwlock;
//初始化读写锁
pthread_rwlock_init(pthread_rwlock_t *restrict rwlock ,const pthread_rwlockattr_t *restrict attr);

//释放读写锁占用的系统资源
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

参数:

  • rwlock: 读写锁的地址,传出参数;
  • attr: 读写锁属性,默认使用 NULL;
//对读写锁加读锁 ,锁定的是读操作
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);

调用该函数,如果读写锁是打开的,那么加锁成功;如果读写锁已经锁定了读操作,调用该函数依然可以加锁成功(因为读锁是共享的);如果读写锁锁定了写操作,调用这个函数的线程会被阻塞。

//此函数可以避免死锁
//如果读写锁失败,不会阻塞当前线程,直接返回错误号
int pthread_rwlock_tryrdlock(pthread_t *rwlock);

调用这个函数,如果读写锁是打开的,那么加锁成功;如果读写锁已经锁定了读操作,调用这个函数依然可以加锁成功,因为读锁是共享的;如果读写锁已经锁定了写操作,调用这个函数加锁失败,对应的线程不会被阻塞,可以在程序中对函数返回值进行判断,添加加锁失败之后的处理动作。

//对读写锁加写锁,锁定写操作
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);

调用该函数,如果读写锁是打开的,那么加锁成功;如果读写锁已经锁定了读操作或写操作,调用该函数的线程会被阻塞。

//此函数可以有效避免死锁
//如果加写锁失败,不会阻塞当前线程,直接返回错误号
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);

调用这个函数,如果读写锁是打开的,那么加锁成功;如果读写锁已经锁定了读操作或者锁定了写操作,调用这个函数加锁失败,但是线程不会阻塞,可以在程序中对函数返回值进行判断,添加加锁失败之后的处理动作。

//解锁 。无论锁定读还是锁定写都可以解
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
1.4.2 读写锁使用

示例 6个线程操作同一个全局变量,3个线程不定时写同一全局资源 ,3个线程不定时读同一个全局资源

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

//全局变量
int number =0;
//定义读写锁
pthread_rwlock_t rwlock;

//线程处理函数
void* writeNum(void* arg)
{
    while (1)
    {
        //加写锁
        pthread_rwlock_trywrlock(&rwlock);
        int cur =number;
        cur++;
        number =cur;
        printf("写操作完毕 ,number: %d ,tid =%ld \n",number,pthread_self());
        pthread_rwlock_unlock(&rwlock);
        //添加sleep() ,为了看到线程交替工作
        usleep(rand()%50);
    }
    return NULL;
}

//多线程如果处理动作相同 ,可以使用相同的处理函数
//每个线程中的栈资源是独享的
void* readNUm(void* arg)
{
    while (1)
    {
        //加读锁
        pthread_rwlock_rdlock(&rwlock);
        printf("全局变量number =%d ,tid =%ld \n",number ,pthread_self());
        //解读锁
        pthread_rwlock_unlock(&rwlock);
        usleep(rand()%50);
    }
    return NULL;
}


int main()
{
    //初始化读写锁
    pthread_rwlock_init(&rwlock,NULL);

     // 3个写线程, 3个读的线程
    pthread_t wtid[3];
    pthread_t rtid[3];
    for(int i=0; i<3; ++i)
    {
        pthread_create(&wtid[i], NULL, writeNum, NULL);
    }

    for(int i=0; i<3; ++i)
    {
        pthread_create(&rtid[i], NULL, readNUm, NULL);
    }

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

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

    // 销毁读写锁
    pthread_rwlock_destroy(&rwlock);

    return 0;
}

1.5 条件变量

1.5.1 条件变量函数

条件变量的主要作用不是处理线程同步,而是进行线程的阻塞在多线程中使用条件变量无法实现线程同步,必须要配合互斥锁使用

条件变量和互斥锁都能阻塞线程 ,但是二者的区别:

  1. 假设有A-Z 26个线程,这26个线程共同访问同一把互斥锁,如果线程A加锁成功,那么其余25个线程访问互斥锁都阻塞,所有的线程只能顺序访问临界区
  2. 条件变量只有在满足指定条件下才会阻塞线程,如果条件不满足,多个线程可以同时进入临界区,同时读写临界资源,此情况下可能会出现数据混乱;

一般情况下条件变量用于处理生产消费者模型 ,并配合互斥锁使用 。 条件变量类型对应的类型为:pthread_cond_t ;

pthread_cond_t cond;

被条件变量阻塞的线程的线程信息会被记录到这个变量中 ,以便在解除阻塞的时候使用。

#include <pthread.h>
pthread_cond_t cond;
//初始化
pthread_cond_init(pthread_cond_t *restrict cond ,const pthread_condattr *restrict attr);

//释放资源
pthread_cond_destroy(pthread_cond_t *cond);

参数:

  • cond: 条件变量的地址;

  • attr: 条件变量属性 ,默认 NULL;

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

该函数调用需要互斥锁,互斥锁进行线程同步,让线程顺序进入临界区避免出现共享资源的数据混乱;

该函数会对互斥锁做以下事:

  1. 在阻塞线程时,如果线程已经对互斥锁mutex上锁,那就会将锁打开避免死锁;
  2. 当线程解除阻塞的时候,函数内部会帮助线程再次将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);

该函数前两个参数和 pthread_cond_wait 函数是一样的,第三个函数表示线程阻塞的时长 ;注意:struct_timespec 这个结构体中记录的时间是从1971.1.1到某个时间点的时间,总长度使用时间秒表示 。赋值相对麻烦一点。

time_t mytim = time(NULL);	// 1970.1.1 0:0:0 到当前的总秒数
struct timespec tmsp;
tmsp.tv_nsec = 0;
tmsp.tv_sec = time(NULL) + 100;	// 线程阻塞100s
// 唤醒阻塞在条件变量上的线程, 至少有一个被解除阻塞
int pthread_cond_signal(pthread_cond_t *cond);
// 唤醒阻塞在条件变量上的线程, 被阻塞的线程全部解除阻塞
int pthread_cond_broadcast(pthread_cond_t *cond);

调用以上两个函数的任意一个,唤醒pthread_cond_waitpthread_cond_timedwait 阻塞的线程,区别在与 pthread_cond_sognal 唤醒一个被阻塞的线程pthread_cond_broadcast 唤醒所有被阻塞的线程;

1.5.2 生产者和消费者模型

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

  1. 若干个生产者线程:

    — 生产商品(任务)放到任务队列中;

    — 任务队列满了就会阻塞 ,不满的时候就继续工作;

    — 通过一个生产者的条件变量控制生产者线程阻塞和非阻塞;

  2. 若干个消费者线程:

    — 读任务队列 ,将任务取出;

    — 任务队列中有数据就消费,没有数据就阻塞;

    — 通过一个消费者条件变量控制消费者线程阻塞和非阻塞;

  3. 任务队列(存储任务/数据),为了读写访问可以通过一个数据结构维护这块内存:

    — 数组、链表,或者使用STL容器: queue/stack/list/vector;

在这里插入图片描述

场景描述: 使用条件变量实现生产者和消费者模型,生产者有 5 个,往链表头部添加节点,消费者也有 5 个,删除链表头部的节

1.6 信号量

在使用信号量进行多线程同步时,一个线程完成了某一个动作就通过信号量告诉别的线程,别的线程再进行某些动作。信号量不一定是锁定某一个资源,而是流程上的概念,例如:又有A 、B两个线程,B线程要等A线程完成某一任务之后再进行自己的步骤,这个任务不一定是锁定某一个资源,还要进行一些计算或数据处理;

信号量(信号灯)与互斥锁和条件变量的主要不同在于**“灯”的概念** ,灯亮意味资源可用,灯灭则意味着不可用。信号量只要用来阻塞线程,不能保证线程安全,如需要保证线程安全需要信号量和互斥锁一起使用。

1.6.1 信号量函数

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

#include <semaphore>
sem_t sem;

Linux系统提供的信号量操作函数原型:

#include <semaphore>
//初始化信号量
int sem_init(sem_t *sem ,int pshared ,unsigned int value);
//资源释放 ,线程销毁后再调用此函数
int sem_destroy(sem_t *sem);

参数:

sem: 信号量变量地址;

pshared: 0 :线程同步 ; 非0 :进程同步;

value: 初始化当前信号量拥有的资源数(>=0),当资源数为0 ,线程被阻塞;

当线程调用此函数,且sem中的资源数>0 ,线程不会阻塞,线程会占用sem中的一个资源,因此资源-1,直到sem中的资源数减为0时 ,线程被阻塞;

//函数被调用sem中的资源就会消耗1 ,资源数-1
int sem_wait(sem_t* sem);

当线程调用此函数,且sem中的资源数>0 ,线程不会阻塞,线程会占用sem中的一个资源,因此资源-1,直到sem中的资源数减为0时 ,资源被耗尽但是线程不会被阻塞,直接返回错误号 ,因此可以在程序中添加判断分支,用于处理获取资源失败之后的情况;

//函数被调用sem中的资源数被消耗1 ,资源数-1
int sem_trywait(sem_t* sem);
// 表示的时间是从1971.1.1到某个时间点的时间, 总长度使用秒/纳秒表示
struct timespec {
	time_t tv_sec;      /* Seconds */
	long   tv_nsec;     /* Nanoseconds [0 .. 999999999] */
};
// 调用该函数线程获取sem中的一个资源,当资源数为0时,线程阻塞,在阻塞abs_timeout对应的时长之后,解除阻塞。
// abs_timeout: 阻塞的时间长度, 单位是s, 是从1970.1.1开始计算的
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);

该函数的参数 abs_timeoutpthread_cond_timedwait 的最后一个参数是一样的,使用方法不再过多赘述。当线程调用这个函数,并且 sem 中的资源数 >0线程不会阻塞 ,线程会占用 sem 中的一个资源,因此资源数 - 1,直到 sem 中的资源数减为 0 时,资源被耗尽,线程被阻塞当阻塞指定的时长之后,线程解除阻塞。

调用此函数将sem中的资源数+1 ,如果有线程在调用sem_wait 、sem_trywait、sem_timedwait时因为sem中的资源数为0被阻塞了,此时这些线程会解除阻塞 ,获取资源之后继续向下运行

//调用此函数给sem中资源数+1
int sem_post(sem_t* sem);

此函数可以查看sem中现有拥有的资源数 ,通过第二个参数sval将数据传出 ;第二参数和返回值一样。

//查看信号量 sem中的整形数的当前值,这个值会被写入sval指针对应的内存中
int sem_getvalue(sem_t *sem ,int *sval);
1.6.2 生产者和消费者

由于生产者和消费者是两类线程,且在还没有产生任务之前不能消费。在使用信号量处理此类问题的时候可以定义两个信号量,分别用于记录生产者和消费者线程拥有的总资源数;

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

// 信号量初始化
sem_init(&psem, 0, 5);    // 5个生产者可以同时生产
sem_init(&csem, 0, 0);    // 消费者线程没有资源, 因此不能消费

// 生产者线程
// 在生产之前, 从信号量中取出一个资源
sem_wait(&psem);	
// 生产者商品代码, 有商品了, 放到任务队列
......	 
......
......
// 通知消费者消费,给消费者信号量添加资源,让消费者解除阻塞
sem_post(&csem);
	



// 消费者线程
// 消费者需要等待生产, 默认启动之后应该阻塞
sem_wait(&csem);
// 开始消费
......
......
......
// 消费完成, 通过生产者生产,给生产者信号量添加资源
sem_post(&psem);

始化信号量的时候没有消费者分配资源,消费者线程启动之后由于没有资源自然就被阻塞了,等生产者生产出产品之后,再给消费者分配资源,这样二者就可以配合着完成生产和消费流程了。

1.6.3信号量使用

场景描述:使用信号量实现生产者和消费者模型,生产者有 5 个,往链表头部添加节点,消费者也有 5 个,删除链表头部的节点。

  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);
        // 创建一个链表的新节点
        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());

        // 通知消费者消费, 给消费者加信号灯
        sem_post(&csem);
        

        // 生产慢一点
        sleep(rand() % 3);
    }
    return NULL;
}

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

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

int main()
{
    // 初始化信号量
    // 生产者和消费者拥有的信号灯的总和为1
    sem_init(&psem, 0, 1);  // 生成者线程一共有1个信号灯
    sem_init(&csem, 0, 0);  // 消费者线程一共有0个信号灯

    // 创建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);

    return 0;
}

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

  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;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值