50.Linux 线程三 同步

  Linux系统中常用实现线程同步的方式有三种,分别为互斥锁、条件变量与信号量。

  下面将对这三种方式逐一进行讲解。

  4.1 互斥锁   

 使用互斥锁实现线程同步时,系统会为共享资源添加一个称为互斥锁的标记,防止多个线程在同一时刻访问相同的共用资源。互斥锁通常也被称为互斥量(mutex),它相当于一把锁,使用互斥锁可以保证以下3点:

        ①原子性:如果在一个线程中设置了一个互斥锁,那么在加锁与解锁之间的操作会被锁定为一个原子操作;这些操作要么全部完成,要么一个也不执行。

        ②唯一性:如果一个线程锁定了一个互斥锁,在解除锁定之前,没有其他线程可以锁定这个互斥量。

        ③非繁忙等待:如果一个线程已经锁定了一个互斥锁,此后第二个线程试图锁定该互斥锁,则第二个线程会被挂起;直到第一个线程解除对互斥锁的锁定时,第二个线程才会被唤醒,同时锁定这个互斥锁。

        使用互斥锁实现线程同步时主要包含四步:初始化互斥锁、加锁、解锁、销毁锁。Linux系统中提供了一组与互斥锁相关的系统调用,分别为:

pthread_mutex_init()、

pthread_mutex_lock()、

pthread_mutex_unlock()、

pthread_mutex_destroy()

这4个系统调用存在于pthread.h中,下面分别对这4个接口进行讲解。

(1)pthread_mutex_init()

pthread_mutex_init()函数的功能为初始化互斥锁,该函数的声明如下:

int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);

pthread_mutex_init()函数中

①参数mutex为一个pthread_mutex_t *类型的传入传出参数,为简单理解,读者可将其视为整数。

②变量mutex只有两种值:0和1。加锁操作可视为mutex-1;解锁操作可视为mutex+1

③参数mutex之前的restrict是一个关键字该关键字用于限制指针,其功能为告诉编译器,所有修改内存中该指针所指向内容的操作只能通过本指针完成

④函数中第二个参数attr同样是一个传入传出参数,代表互斥量的属性,通常传递NULL,表示使用默认属性

若函数pthread_mutex_init()调用成功则返回0,否则返回errno

errno的常见取值为EAGAIN和EDEADLK

EAGAIN表示超出互斥锁递归锁定的最大次数,因此无法获取该互斥锁;

EDEADLK表示当前线程已有互斥锁,二次加锁失败。

        通过pthread_mutex_init()函数初始化互斥量又称为动态初始化,一般用于初始化局部变量,示例如下:

pthread_mutex_init(&mutex,NULL);

此外互斥锁也可以直接使用宏进行初始化,示例如下:

pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;

此条语句与以上动态初始化示例语句功能相同。

(2)pthread_mutex_lock()

当在线程中调用pthread_mutex_lock()函数时,该线程将会锁定指定互斥量。pthread_mutex_lock()函数的声明如下:

int pthread_mutex_lock(pthread_mutex_t *mutex);

该函数中只有一个参数mutex,表示待锁定的互斥量

程序中调用该函数后,直至调用pthread_mutex_unlock()函数之前,此间的代码均被上锁,即在同一时刻只能被一个线程执行。若函数pthread_mutex_lock()调用成功则返回0,否则返回errno。

        若使用的互斥锁正在被使用,调用pthread_mutex_lock()函数的线程会进入阻塞。但有些情况下,我们希望线程可以先去执行其他功能,此时需要使用非阻塞的互斥锁。Linux系统中提供pthread_mutex_trylock()函数,该函数的功能为尝试加锁;若锁正在被使用,则不阻塞等待,而是直接返回并返回错误号。pthread_mutex_trylock()函数的声明如下:

int pthread_mutex_trylock(pthread_mutex_t *mutex);

该函数中的参数mutex同样表示待锁定的互斥量;若函数调用成功则返回0,否则返回errno。其中常见的errno有两个,分别EBUSY和EAGAIN,它们代表的含义如下:

EBUSY:参数mutex指向的互斥锁已锁定。

EAGAIN:超过互斥锁递归锁定的最大次数。

(3)pthread_mutex_unlock()

当在线程中调用pthread_mutex_unlock()函数时,该线程将会为指定互斥量解锁

pthread_mutex_unlock()函数的声明如下:

int pthread_mutex_unlock(pthread_mutex_t *mutex);

函数中的参数mutex表示待解锁的互斥量;若函数调用成功返回0,否则返回errno

(4)pthread_mutex_destroy()

互斥锁也是系统中的一种资源,因此使用完毕后应将其释放。当在线程中调用pthread_mutex_destroy()函数时,该线程将会指定互斥量解锁。pthread_mutex_destroy()函数的声明如下:

int pthread_mutex_destroy(pthread_mutex_t *mutex);

函数中的参数mutex表示待销毁的互斥量。若函数pthread_mutex_lock()调用成功返回0,失败返回errno。

下面通过一个案例来展示互斥锁在程序中的使用方法及功能。

案例9-9:在原线程和新线程中分别进行打印操作使原线程分别打印HELLO、WORLD新线程分别打印hello、world

本案例中将在pthread_share.c文件中实现为添加mutex的程序,在pthread_mutex.c中实现添加互斥锁的程序。

pthread_share.c文件中的代码如下:

描述

C 库函数 void srand(unsigned int seed) 播种由函数 rand 使用的随机数发生器。

pthread_share.c——未添加mutex

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
void *tfn(void *arg)
{
	srand(time(NULL));//随机数发生器
	while(1){
		printf("hello ");	
	    //模拟长时间操作共享资源,导致CPU易主,产生与时间有关的错误
		sleep(rand()%3);
		printf("world\n");
		sleep(rand()%3);
	}
	return NULL;
}
int main(void)
{
	pthread_t tid;
	srand(time(NULL));
	pthread_create(&tid,NULL,tfn,NULL);
	while(1){
		printf("HELLO ");
		sleep(rand()%3);
		printf("WORLD\n");
		sleep(rand()%3);
	}
	pthread_join(tid,NULL);
	return 0;
}

此段为未添加互斥量的程序,执行结果如下:

 观察以上执行结果可知,原线程与新线程中的字符串未能成对打印。

往以上的代码添加互斥量,进行线程同步,将修改后的程序保存在pthread_mutex.c文件中,具体代码如下:

pthread_mutex.c——添加mutex

#include <stdio.h>
#include <string.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
pthread_mutex_t m;    //定义互斥锁
void *err_thread(int ret,char *str)
{
	if(ret!=0){
		fprintf(stderr,"%s:%s\n",str,strerror(ret));
		pthread_exit(NULL);		
	}
}
void *tfn(void *arg)
{
	srand(time(NULL));
	while(1){	
		pthread_mutex_lock(&m);  //加锁:m--
		printf("hello");
		//模拟长时间操作共享资源,导致CPU易主,产生时间有关的错误
		sleep(rand()%3);
		printf("world\n");
		pthread_mutex_unlock(&m);    //解锁:m++
		sleep(rand()%3);
	}
	return NULL;
}
int main(void)
{
	pthread_t tid;
	srand(time(NULL));
	int flag = 5;
	pthread_mutex_init(&m,NULL);    //初始化mutex:m=1
	int ret=pthread_create(&tid,NULL,tfn,NULL);
	err_thread(ret,"pthread_create error");
		while(flag--){
			pthread_mutex_lock(&m);    //加锁:m--
			printf("HELLO");
			sleep(rand()%3);
			printf("WORLD\n");
			pthread_mutex_unlock(&m);    //解锁:m++
			sleep(rand()%3);
		}
	pthread_cancel(tid);
	pthread_join(tid,NULL);
	pthread_mutex_destroy(&m);
	return 0;
}

在pthread_mutex.c中,终端即为共享资源,原线程和新线程在临界区代码中都需要向终端打印数据。为了使两个线程输出的字符串能够匹配,互斥锁将程序中两次访问终端的一段代码绑定为原子操作,因此在获取互斥锁的线程完成两次打印操作之前,其他线程无法获取终端。编译pthread_mutex.c执行程序,执行结果如下:

4.2 条件变量

private  私人的

Linux下C编程的条件变量:条件变量是线程中的东西,就是等待某一条件的发生,和信号一样。

        使用变量控制线程同步时,线程访问共享资源的前提是程序中设置的条件得到满足

条件变量不会对共享资源加锁,但也会使线程阻塞。若条件变量规定的条件不满足,线程就会进入阻塞状态直到条件满足。

        条件变量往往与互斥锁搭配使用在线程需要访问共享资源时,会先绑定一个互斥锁,然后检测条件变量

若条件变量满足,线程就继续执行并在资源访问完成后解开互斥锁;

若条件变量不满足,线程将解开互斥锁,进入阻塞状态,等待变量条件状况发生改变。一般条件变量不满足,线程将解开互斥锁,进入阻塞状态,等待条件状况发生改变。

一般条件变量的状态由其他非阻塞态的线程改变,条件变量满足后处于阻塞状态的线程将被唤醒,这些线程再次争夺互斥锁,对条件变量状况进行测试。

综上所述,条件变量的使用分为以下4个步骤:

①初始化条件变量。

②等待条件变量满足。

③唤醒阻塞线程。

④释放条件变量。

针对以上步骤,Linux状态提供了一组与条件变量相关的系统调用,此组系统调用都存在于函数库pthread.h中,下面将对这些系统调用进行讲解。

(1)初始化条件变量。

Linux系统中用于初始化条件变量的函数为pthread_cond_init(),其声明如下:

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

函数pthread_cond_init()中的参数cond代表条件变量,本质是一个指向pthread_cond_t类型的结构体指针,pthread_cond_t是Linux系统中定义的条件变量类型。参数attr代表条件变量的属性,通常设置为NULL,表示使用默认属性初始化条件变量,

其默认值为PTHREAD_PROCESS_PRIVATE,表示当前进程中的线程共用此条件变量

也可将attr设置为PTHREAD_PROCESS_SHARED,表示当前多个进程间的线程共用条件变量

若函数调用成功返回0,失败返回-1并设置errno。

除使用函数pthread_cond_init()动态初始化条件变量外,也可以使用如下语句以静态方法初始化条件变量

pthread_cond_t cond=PTHREAD_COND_INITIALIZER;

静态初始化条件变量的方式与将attr参数初始化为NULL的pthread_cond_init()函数等效,但是不进行错误检查。

(2)阻塞等待条件变量

Linux中一般通过pthread_cond_wait()函数使线程进入阻塞状态,等待一个条件变量,其声明如下:

int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);

函数pthread_cond_wait()中

①参数cond代表条件变量;

②参数mutex代表与当前线程绑定的互斥锁。

若函数调用成功则返回0,否则返回-1并设置errno。

pthread_cond_wait()类似于互斥锁中的函数pthread_mutex_lock(),但其功能更加丰富,它的工作机制如下:

①阻塞等待条件变量cond满足

②解除已绑定的互斥锁(类似于pthread_mutex_unlock())。

③当线程被唤醒时,pthread_cond_wait()函数返回,该函数同时会解除线程阻塞并使线程重新申请绑定互斥锁。

所谓原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch (切换到另一个线程)。

以上工作机制中的前两条为一个原子操作;需要注意到最后一条,最后一条机制表明:当线程被唤醒后,仍需要重新绑定互斥锁。

除pthread_cond_wait()外,pthread_cond_timedwait()也能使线程阻塞等待条件变量;不同的是,该函数可以指定线程的阻塞时长,若等待超时,该函数便会返回。函数pthread_cond_timedwait()存在于函数库pthread.h中,其声明如下:

int pthread_cond_timedwait(pthread_cond_t * restrict cond,pthread_mutex_t *restrict mutex,const struct timespec *restrict abstime);

函数pthread_cond_timedwait()中

①参数cond代表条件变量;

②参数mutex代表互斥锁

③参数abstime代表绝对时间,用于设置等待时长,该参数是一个传入参数,本质是一个struct timespec类型的结构体指针,该结构体的定义如下:

struct timespec{

        time_t tv_sec;

        long tv_nsec;

}

(3)唤醒条件变量

pthread_cond_signal()函数会在条件变量满足之后,以信号的形式唤醒阻塞在该条件变量上的一个线程

处于阻塞状态中的线程的唤醒顺序由调度策略决定。pthread_cond_signal()函数存在于函数库pthread.h中,其声明如下:

int pthread_cond_signal(pthread_cond_t *cond);

函数pthread_cond_broadcast()中的参数cond代表条件变量,若该函数调用成功则返回0,否则返回-1并设置errno。

(4)销毁条件变量

pthread_cond_destroy()函数用于销毁条件变量 ,该函数的声明如下:

int pthread_cond_destroy(pthread_cond_t *cond);

需要注意的是,只有当没有线程在等待参数cond指定的条件变量时,才可以销毁条件变量,否则该函数会返回EBUSY。

下面通过一个案例来展示使用条件变量实现线程同步的方法。

案例9-10:生产者-消费者模型是线程同步中的一个经典案例。假设有两个线程,这两个线程同时操作一个共享资源(一般称为“汇聚”)。其中一个模拟生产者行为,生产共享资源,当容器存满时,生产者无法向其中放入产品 ; 另一个线程模拟消费者行为,消费共享资源,当产品数量为0时,消费者无法获取产品,应阻塞等待。显然,为防止数据混乱,每次只能由生产者或消费者中的一个操作共享资源。

案例实现如下:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
struct msg{
	struct msg *next;	
	int num;
};
struct msg *head;
pthread_cond_t has_product=PTHREAD_COND_INITIALIZER;
pthread_mutex_t lock=PTHREAD_MUTEX_INITIALIZER;

void *consumer(void *p)
{
	struct msg *mp;
	for(;;){
		pthread_mutex_lock(&lock);
		
		while(head==NULL){
			pthread_cond_wait(&has_product,&lock);
		}
	

		mp=head;
		head=mp->next;
		pthread_mutex_unlock(&lock);
		printf("-Consume---%d\n",mp->num);
		free(mp);
		sleep(rand()%5);
	}
}

void *producer(void *p)
{
	struct msg *mp;
	while(1){
		mp=malloc(sizeof(struct msg));	
		mp->num=rand()%1000+1;
		printf("-Produce---%d\n",mp->num);
		pthread_mutex_lock(&lock);
		mp->next=head;
		head=mp;
		pthread_mutex_unlock(&lock);
		pthread_cond_signal(&has_product);
		sleep(rand()%5);
	}
}
int main(int argc,char *argv[])
{
	pthread_t pid,cid;
	srand(time(NULL));
	
	pthread_create(&pid,NULL,producer,NULL);
	pthread_create(&cid,NULL,consumer,NULL);
	
	pthread_join(pid,NULL);
	pthread_join(cid,NULL);
	return 0;
}

执行结果如下:

POSIX信号量基本概念

信号量(Semaphore)是一种实现进程/线程间通信的机制,可以实现进程/线程之间同步或临界资源的互斥访问, 常用于协助一组相互竞争的进程/线程来访问临界资源。在多进程/线程系统中, 各进程/线程之间需要同步或互斥实现临界资源的保护,信号量功能可以为用户提供这方面的支持。

在 POSIX标准中,信号量分两种,一种是无名信号量,一种是有名信号量。 无名信号量一般用于进程/线程间同步或互斥,而有名信号量一般用于进程间同步或互斥。 有名信号量和无名信号量的差异在于创建和销毁的形式上,但是其他工作一样,无名信号量则直接保存在内存中, 而有名信号量则要求创建一个文件。

正如其名,无名信号量没有名字,它只能存在于内存中,这就要求使用信号量的进程/线程必须能访问无名信号量所在的这一块内存, 所以无名信号量只能应用在同一进程内的线程之间同步或者互斥。相反,有名信号量可以通过名字访问, 因此可以被任何知道它们名字的进程或者进程/线程使用。单个进程中使用POSIX 信号量时,无名信号量更简单, 多个进程间使用 POSIX信号量时,有名信号量更简单。

在多进程/多进程/线程操作系统环境下,多个进程/线程会同时运行,并且一些进程/线程之间可能存在一定的关联。 多个进程/线程可能为了完成同一个进程/线程会相互协作,这样形成进程/线程之间的同步关系,因此可以使用信号量进行同步。

而且在不同进程/线程之间,为了争夺有限的系统资源(硬件或软件资源)会进入竞争状态,这就是进程/线程之间的互斥关系。 为了防止出现因多个程序同时访问一个共享资源而引发的一系列问题,我们需要一种方法,它可以通过生成并使用令牌来授权, 在任一时刻只能有一个执行进程/线程访问代码的临界区域。

临界区域是指执行数据更新的代码需要独占式地执行。 而信号量就可以提供这样的一种访问机制,让一个临界区同一时间只有一个进程/线程在访问它, 因此信号量是可以用来调协进程/线程对共享资源的访问的。进程/线程之间的互斥与同步关系存在的根源在于临界资源。 临界资源是在同一个时刻只允许有限个(通常只有一个)进程/线程可以访问(读)或修改(写)的资源, 通常包括硬件资源(处理器、内存、存储器以及其他外围设备等)和软件资源(共享代码段,共享结构和变量等)。

抽象的来讲,信号量中存在一个非负整数,所有获取它的进程/线程都会将该整数减一(获取它当然是为了使用资源), 当该整数值为零时,所有试图获取它的进程/线程都将处于阻塞状态。通常一个信号量的计数值用于对应有效的资源数, 表示剩下的可被占用的互斥资源数。其值的含义分两种情况:

  • 0:表示没有可用的信号量,进程/线程进入睡眠状态,直至信号量值大于 0。

  • 正值:表示有一个或多个可用的信号量,进程/线程可以使用该资源。进程/线程将信号量值减1, 表示它使用了一个资源单位。

对信号量的操作可以分为两个:

  • P 操作:如果有可用的资源(信号量值大于0),则占用一个资源(给信号量值减去一,进入临界区代码); 如果没有可用的资源(信号量值等于0),则被阻塞到,直到系统将资源分配给该进程/线程(进入等待队列, 一直等到资源轮到该进程/线程)。这就像你要把车开进停车场之前,先要向保安申请一张停车卡一样, P操作就是申请资源,如果申请成功,资源数(空闲的停车位)将会减少一个,如果申请失败,要不在门口等,要不就走人。

  • V 操作:如果在该信号量的等待队列中有进程/线程在等待资源,则唤醒一个阻塞的进程/线程。如果没有进程/线程等待它, 则释放一个资源(给信号量值加一),就跟你从停车场出去的时候一样,空闲的停车位就会增加一个。

举个例子,就是两个进程/线程共享信号量sem,sem可用信号量的数值为1,一旦其中一个进程/线程执行了P(sem)操作, 它将得到信号量,并可以进入临界区,使sem减1。而第二个进程/线程将被阻止进入临界区,因为当它试图执行P(sem)操作时, sem为0,它会被挂起以等待第一个进程/线程离开临界区域并执行V(sem)操作释放了信号量,这时第二个进程/线程就可以恢复执行。

8.2. POSIX有名信号量

如果要在Linux中使用信号量同步,需要包含头文件 semaphore.h 。

有名信号量其实是一个文件,它的名字由类似 “sem.[信号量名字]” 这样的字符串组成,注意看文件名前面有 “sem.” , 它是一个特殊的信号量文件,在创建成功之后,系统会将其放置在 /dev/shm 路径下, 不同的进程间只要约定好一个相同的信号量文件名字,就可以访问到对应的有名信号量, 并且借助信号量来进行同步或者互斥操作,需要注意的是,有名信号量是一个文件,在进程退出之后它们并不会自动消失, 而需要手工删除并释放资源。

主要用到的函数:

sem_t *sem_open(const char *name, int oflag, mode_t mode, unsigned int value);
int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);
int sem_post(sem_t *sem);
int sem_close(sem_t *sem);
int sem_unlink(const char *name);
  • sem_open()函数用于打开/创建一个有名信号量,它的参数说明如下:

    • name:打开或者创建信号量的名字。

    • oflag:当指定的文件不存在时,可以指定 O_CREATE 或者 O_EXEL进行创建操作, 如果指定为0,后两个参数可省略,否则后面两个参数需要带上。

    • mode:数字表示的文件读写权限,如果信号量已经存在,本参数会被忽略。

    • value:信号量初始的值,这这个参数只有在新创建的时候才需要设置,如果信号量已经存在,本参数会被忽略。

    • 返回值:返回值是一个sem_t类型的指针,它指向已经创建/打开的信号量, 后续的函数都通过改信号量指针去访问对应的信号量。

  • sem_wait()函数是等待(获取)信号量,如果信号量的值大于0,将信号量的值减1,立即返回。如果信号量的值为0, 则进程/线程阻塞。相当于P操作。成功返回0,失败返回-1。

  • sem_trywait()函数也是等待信号量,如果指定信号量的计数器为0,那么直接返回EAGAIN错误,而不是阻塞等待。

  • sem_post()函数是释放信号量,让信号量的值加1,相当于V操作。成功返回0,失败返回-1。

  • sem_close()函数用于关闭一个信号量,这表示当前进程/线程取消对信号量的使用,它的作用仅在当前进程/线程, 其他进程/线程依然可以使用该信号量,同时当进程结束的时候,无论是正常退出还是信号中断退出的进程, 内核都会主动调用该函数去关闭进程使用的信号量,即使从此以后都没有其他进程/线程在使用这个信号量了, 内核也会维持这个信号量。

  • sem_unlink()函数就是主动删除一个信号量,直接删除指定名字的信号量文件。

8.3. POSIX无名信号量

无名信号量的操作与有名信号量差不多,但它不使用文件系统标识,直接存在程序运行的内存中, 不同进程之间不能访问,不能用于不同进程之间相互访问。同样的一个父进程初始化一个信号量, 然后fork其副本得到的是该信号量的副本,这两个信号量之间并不存在关系。

主要用到的函数:

int sem_init(sem_t *sem, int pshared, unsigned int value);
int sem_destroy(sem_t *sem);
int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);
int sem_post(sem_t *sem);
  • sem_init():初始化信号量。

    • 其中sem是要初始化的信号量,不要对已初始化的信号量再做sem_init操作,会发生不可预知的问题。

    • pshared表示此信号量是在进程间共享还是线程间共享,由于目前Linux 还没有实现进程间共享无名信号量, 所以这个值只能够取0,表示这个信号量是当前进程的局部信号量。

    • value是信号量的初始值。

    • 返回值:成功返回0,失败返回-1。

  • sem_destroy():销毁信号量,其中sem是要销毁的信号量。只有用sem_init初始化的信号量才能用sem_destroy()函数销毁。 成功返回0,失败返回-1。

  • sem_wait()、sem_trywait()、sem_post()等函数与有名信号量的使用是一样的。

案例9-11:本案例也来实现一个生产者-消费者模型,但对生产者进行限制:若容器已满,生产者不能生产,需要等待消费者消费。案例实现如下:

#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <stdio.h>
#include <semaphore.h>
#define NUM 5
int queue[NUM];
sem_t blank_number,product_number;
void *producer(void *arg)
{
	int i=0;
	while(1){
		sem_wait(&blank_number);
		queue[i]=rand()%1000+1;
		printf("----Produce---%d\n",queue[i]);
		sem_post(&product_number);
		i=(i+1)%NUM;
		sleep(rand()%1);
	}
}
void *consumer(void *arg)
{
	int i=0;
	while(1){
		sem_wait(&product_number);
		printf("-Consume---%d      %lu\n",queue[i],pthread_self());
		queue[i]=0;
		sem_post(&blank_number);
		i=(i+1)%NUM;
		sleep(rand()%1);
	}
}
int main(int argc,char *argv[])
{
	pthread_t pid,cid;
	sem_init(&blank_number,0,NUM);
	sem_init(&product_number,0,0);
	pthread_create(&pid,NULL,producer,NULL);
	pthread_create(&cid,NULL,consumer,NULL);
	pthread_create(&cid,NULL,consumer,NULL);
	pthread_join(pid,NULL);
	pthread_join(cid,NULL);
	sem_destroy(&blank_number);
	sem_destroy(&product_number);
	return 0;
}

执行结果如下:

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值