线程的概念、控制、以及同步与互斥

      在学习了进程过进程之后,我们就该来学习一下线程了,什么是线程呢?接下来我们就来学习一下。

1、线程的概念

         我们都知道,进程就是程序运行起来的实体,包括一大堆的数据结构,而线程是什么呢?我们应该听过一句话“线程是在进程内部运行的”,如何理解这句话呢?接下来带着问题我们来学习。

(1)线程的概念

          什么是线程?简单来说,线程就是一个程序里的执行流,更准确的说:线程是“一个进程内部的控制序列”。一个进程至少有一个执行线程(即主线程)

(2)线程和进程

  • 进程是资源竞争的基本单位;
  • 线程是程序执行的最小单位; 
  • 线程共享数据,但也有自己私有的一部分数据。          

                  >线程ID>一组寄存器>>errno>信号屏蔽字>调度优先级      

          在线程中,线程拥有自己的线程ID在内核中由LWP表示;线程私有的一组寄存器存有独立的上下文数据;以及私有栈空间;当然也就有了私有的调度优先级;

  (3)一进程的多线程共享    

  •  同一地址空间; 既然地址空间都是共享的,那么Text Segment、Data Segment都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各进程中都可以访问到,除此之外,各线程还共享进程资源和环境;  
  • 文件描述符表
  • 每种信号的处理方式(忽略,默认动作,或者自定义)
  • 当前工作目录
  • 用户ID和组ID;           

(4)线程的优点

  • 创建一个新线程的代价比一个新进程小的多;
  • 与线程之间的切换相比,线程之间的切换需要操作系统做的工作要少的多;
  • 线程占用的资源比进程少很多;
  • 能充分利用多处理器的可并行数量;
  • 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务;
  • 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现;
  • I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作;  

(5)线程的缺点

  • 性能损失;
  • 健壮性降低;一个线程崩掉,可能导致整个进程退出
  • 缺乏访问控制;
  • 编程难度提高;

         在了解了线程之后,我们再来谈谈进程。我们在前边学习了进程,在前边,我们知道进程不简单的是一段程序它是一段跑起来的程序,是一个实体,当我们学习了线程之后,进程就不能直接叫进程了。

           在Linux下,进程叫做轻量级进程;Linux下的一个进程由一个或多个PCB、以及资源组成;Linux下进程是承担分配系统资源的实体;而在Linux下,调度的基本单位不再是进程而是线程;“线程是在进程内部运行的”这句话现在我们就能理解了,因为进程和线程共享地址空间,所以线程在进程内部运行也就是线程在进程的地址空间中。在Linux下,进程控制块和线程控制块都由PCB表示,而在Windows操作系统下,线程控制块叫做TCB。

2、可重入函数和线程安全   

   可重入和线程安全的概念?它们两个有什么区别和联系?

       一个函数被称为线程安全的,当且仅当被多个并发进程反复调用时,它会一直产生正确的结果。反之,如果一个函数不是线程安全的,我们就说它是线程不安全的。线程不安全有四种情况:

  • 不保护共享变量的函数;
  • 函数状态随着调用改变的函数;
  • 返回指向静态变量指针的函数;
  • 调用线程不安全函数的函数;

      可重入函数即表示可以被多个执行流重复进入

      可重入函数是线程安全的一种

线程安全和可重入的区别:

      1、可重入函数是线程安全函数的一种,其特点在于它们被多个线程调用时,不会引用任何共享数据。
可重入函数与线程安全的区别与联系:
  2、线程安全是在多个线程情况下引发的,而可重入函数可以在只有一个线程的情况下来说。
  3、线程安全不一定是可重入的,而可重入函数则一定是线程安全的。
  4、如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。
  5.如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的。
  6、线程安全函数能够使不同的线程访问同一块地址空间,而可重入函数要求不同的执行流对数据的操作互不影响使结果是相同的。

3、线程控制

       我们现在所学的线程都是在用户级库(POSIX线程库)中使用,与线程有关的函数构成了一个完整的系列,绝大数函数的名字都是以“pthread_”打头的,要使用这些数据库,要通过引入头文件链接这些线程函数库时要使用编译器命令的“-lpthread”选项。用户级库使用时需要加载到地址空间共享区。

(1)创建线程

          #include

          int pthread_creat(pthread_t *thraed , const  pthread_attr_*attr ,

                                        void *(*start_routine)(void*) , void *arg);

         参数:thread  返回线程ID;

                   attr  设置线程属性,默认为NULL;

                   start_routine  为回调函数地址,线程启动后执行;

                   arg  传给回调函数的参数;

         返回值:成功返回0,失败返回错误码; 

错误检查:

  • 传统的函数一般是成功返回0,失败返回-1.并且对全局变量errno赋值以指示错误。
  • pthreads函数出错时不会设置群居变量errno(而大部分其他的POSIX函数会这样做),而是将错误码通过返回值返回。
  • pthreads同样也提供了线程内的erron变量,以支持其他使用errno的代码。对于pthreads函数的错误,建议通过返回值来确定,因为读取返回值要比读取线程内的errno变量的开销更小。

     下面用实例来展示一下:

creat.c

#include
#include
#include
#include
#include

void *Pthread_run(void *arg)
{
	int i=0;
	for(;;)
	{
		printf("I'm thread 1 \n");
		sleep(1);
	}
}

int main()
{
	pthread_t tid;
	int ret=pthread_create(&tid,NULL,Pthread_run,NULL);
	if(ret!=0)
	{
		fprintf(stderr,"pthread_creat :%s \n",strerror(ret));
		exit(EXIT_FAILURE);
	}
	int i=0;
	for(;;)
	{
		printf("I'm main thread\n");
		sleep(1);
	}
	return 0;
}

makefile

creat:creat.c
	gcc -o $@ $^ -lpthread

.PHONY:clean
clean:
	rm -f creat

(2)进程ID和进程ID

       在Linux中,目前的线程实现是Native Thread Libaray,简称NPTL。在这种实现下,线程又称为轻量级进程,每一个用户态的线程,在内核中都对应一个调度实体,因为拥有自己的进程描述符。

      没有线程之前,一个进程对应内核里的一个进程描述符,对应一个进程ID、但是在引入线程概念之后,情况发生了变化,一个用户进程下管理N个用户态线程,每个线程作为一个独立的调度实体在内核中拥有自己的进程描述符,进程和内核的描述符一下子变成了1:N的关系。

       Linux内核引入了线程组的概念。

线程的概念、控制、以及同步与互斥_第1张图片

      多线程的进程,又被称为线程组,线程组内的每一个线程在内核之中都存在一个进程描述符与之对应。进程描述符结构体中的pid,表面上看对应的是进程ID,其实不然,它对应的是线程ID;进程描述符中的tgid,含义是Thread  Group  ID ,该值对应的是用户层面的进程ID。

用户态 系统调用 内核进程描述符中对应的结构
线程ID pid_t  gettid(void) pid_t  pid
进程ID pid_t  getpid(void) pid_t  tgid

      现在说的进程ID,不同于pthread_t 类型的线程ID,和进程ID一样,线程ID是pid_t类型的变量,而且是用来唯一标识线程的一个整形变量。

      查看一个线程的ID:

线程的概念、控制、以及同步与互斥_第2张图片

 (3)线程ID及进程地址空间布局

  •  pthread_create函数会产生一个线程ID,存放在第一个参数指向的地址中。该线程ID和前面说的线程ID不是一回事。
  •  前面讲的线程ID属于进程调度的范畴。因为线程是轻量级进程,是操作系统调度器的最小单位,所以需要一个数值来唯一标识该线程。
  •  pthread_create函数产生并标记在第一个参数指向的地址中的线程ID中,属于NPTL线程库的范畴。线程库的后序操作,就是根据该线程ID来操作线程的。
  •  线程库NPTL提供了pthread_self函数,可以获得自身的ID。

         pthread_t   pthread_self(void);

   pthread_t的类型取决于实现,对于Linux目前实现的NPTL实现而言,pthread_t类型的线程ID,本质就是一个进程地址空间上的一个地址。

线程的概念、控制、以及同步与互斥_第3张图片

(4)线程终止

        我们知道,进程退出有三种情况,那么线程呢?线程退出和进程退出有一点不同,就是线程退出不存在代码没跑完异常退出的情况,也就是说,线程退出有两种情况,一是代码跑完结果正确,二是代码跑完结果不正确。

       线程退出的三种方式:(值终止线程,而不终止进程)

  • 从线程函数return(void *),这种方法对主线程不适用,从main函数return相当于调用exit。
  • 线程可以调用pthread_exit(void *)终止自己,线程不能调用exit。
  • 线程是可以被取消的,一个线程可以调用函数pthread_cancel  终止同一进程中的另一个线程。    

     下面说一说线程退出的两个函数:

        void pthread_exit(void  *value_ptr);       线程终止函数

         参数:value_ptr 不要指向一个局部变量;

         无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身);

      注意:pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是malloc分配的,不能在线程函数的栈上分配,因为当其他线程得到这个返回指针时线程函数已经退出了。

         int  pthread_cancel(pthread_t  thread);取消一个执行中的线程

         参数:thread  线程ID

         返回值: 成功返回0,失败返回错误码;

                        线程被取消,返回-1             

4、线程等待与分离

(1)线程等待

       为什么需要线程等待?

       已经退出的线程所占有的空间未被释放,任然存在于进程的地址空间内,造成内存泄漏;创建新的线程时是不会复用刚才退出的线程的地址空间;如果不等待,主线程直接退出,就标志着进程退出,线程根本没有机会来执行代码,就会被释放。

     线程等待函数:

     int  pthread_join(pthread_t  thread,  void  **value_ptr );

     参数:thread  线程ID

               value_ptr指向一个指针,而该指针指向线程的返回值;

     返回值:成功返回0,失败返回错误码;

     调用该函数的线程将挂起等待,知道ID为thread的线程终止。thread线程以不同的方法终止,通过pthread_join得到的进程终止状态是不同的:

  • 如果thread线程通过return返回,value_ptr所指向的单元里存放的是thread线程函数的返回值。
  • 如果thread线程被别的线程调用pthread_cancel异常终止,value_ptr所指向的单元里存放的是常熟PTHREAD_CABCELED(定义的宏,为(void *)-1)。
  • 如果对thread线程的终止状态不感兴趣,可以传NULL给value_ptr参数。

(2)分离线程

       默认情况下,一个新创建的线程是可结合的(joinable),意思就是在线程结束后,需要调用pthread_join函数进行等待回收,否则无法回收释放资源,从而造成内存泄漏。

      如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出后,自动释放线程资源。

      int pthread_detachn(pthread_t  thread);

      分离线程可以是自己提出的,也可以由主线程对该线程进行分离

      pthread_detach(pthread_self);

     一个线程被分离,就不需要被pthread_join,joinable和分离是冲突的,一个线程不能既是joinable又是分离的。

   举栗子说明自己分离:

#include
#include
#include
#include
#include

void *thread_run(void *arg)
{
	pthread_detach(pthread_self());
	printf("%s \n",(char *)arg);
	return NULL;
}

int main()
{
	pthread_t tid;
	int id=pthread_create(&tid,NULL,thread_run,"thread 1 is running\n");

	if(id!=0)
	{
		printf("creat thread failure\n");
		return 1;
	}
	int ret=0;
	sleep(1);

	if(pthread_join(tid,NULL)==0)
	{
		printf("waiting succeed\n");
		ret=0;
	}
	else
	{
		printf("waiting failed\n");
		ret=1;
	}
	return ret;
}

  结果是先出现一行“thread 1 is  running”,然后一秒后,等待失败。原因就是在那一秒中,thread 1已经把自己给分离了。

   一个线程出错挂掉,则相对应的进程也会挂掉;一个被分离的线程异常挂掉,进程依旧也会退出。

5、线程同步与互斥

       在学习进程间通信时,我们知道要想让进程间进行通信,就必须让两个进程同时看到同一份公共资源,也就是临界资源,然后才有可能进行进程间通信,但是,当两个进程进行通信时会产生一些小问题,所以我们就说到了同步与互斥,当然两个线程间进行通信时,不必那么麻烦,因为它们两个本来就在同一个进程中,大多数资源都是共享的,但是只要有共享的地方,肯定就会出现这样那样的问题,所以,线程间也需要同步与互斥机制。

 (1)mutex互斥量

        大部分情况下,线程使用的数据都是局部变量,变量的地址空间在线程自己私有栈空间内,这种情况,变量归属于单个线程,其他线程无法获得这些变量。但有时候,很多变量需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。但是,多个线程并发的操作共享变量,会会带来一些问题。

      例如下面操作共享变量而出现问题的售票系统:

#include
#include
#include
#include
#include

int ticket=100;

void *sellticket(void *arg)
{
   char *tid=(char *)arg;
   while(1)
   {
      if(ticket>0)
      {
         usleep(1000);
	 printf("%s sells ticket:%d \n",tid,ticket);
	 ticket--;
      }
      else
      { 
         break;
      }
   }
}


int main()
{
   pthread_t t1,t2,t3,t4;

   pthread_create(&t1,NULL,sellticket,"thread 1");
   pthread_create(&t2,NULL,sellticket,"thread 2");
   pthread_create(&t3,NULL,sellticket,"thread 3");
   pthread_create(&t4,NULL,sellticket,"thread 4");

   pthread_join(t1,NULL);
   pthread_join(t2,NULL);
   pthread_join(t3,NULL);
   pthread_join(t4,NULL);
}
线程的概念、控制、以及同步与互斥_第4张图片

      你会发现结果有些不对劲,怎么还会有负数的票,用实际来说就是一张票卖给了不同的两个人,这个怎么可能?根本不符合现实,为什么会这样呢?

      if语句判断条件为真以后,代码可以并发的切换到其他线程;在usleep 的过程中,可能会有很多个线程会进入该代码段;而且ticket--这个操作也不是一个原子操作,ticket--这一句C语言代码对应的汇编代码有三条,先将ticket变量由内存加载到寄存器中,然后更新寄存器里面的值(即进行-1操作),再将减完后的值写回到内存中。

         要解决以上的问题,就要做到:

  • 代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区;
  • 如果多个线程同时要求执行临界区的代码,并且临界区没有线程执行,那么只能运行一个线程进入该临界区;
  • 如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区;

要做到这三点,本质上就需要一把锁,而在Linux中提供的这把锁叫互斥量。

而锁要在进入临界区之前加,在出了临界区之后就解锁。

(2)互斥量的接口

     初始化互斥量:

           静态分配:定义一个宏,直接赋值

                pthread_mutex_t  mutex = PTHREAD_MUTEX_INITIALIZER;

           动态分配:使用库函数进行初始化

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

               参数:mutex 需要初始化的互斥量

                         attr  默认为NULL

    销毁互斥量:

         销毁互斥量需要注意:静态分配的互斥量不需要销毁;不要销毁一个已经加锁的互斥量;已经销毁的互斥量,要确保后面不会有线程再尝试加锁。

           int  pthread_mutex_destroy(pthread_mutex_t  * mutex );

     互斥量加锁和解锁:

           int  pthread_mutex_lock(pthread_mutex_t  * mutex );

           int  pthread_mutex_unlock(pthread_mutex_t  * mutex );

       返回值:成功返回0,失败返回错误码;

调用pthread_lock时,可能会遇到以下情况:

互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功;

发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没竞争到互斥量,那么pthread_lock调用会陷入阻塞,等待互斥量解锁。  

    给上面的例子加个互斥量改进一下:

#include
#include
#include
#include
#include
#include

int ticket=100;
pthread_mutex_t mutex;

void *sellticket(void *arg)
{
   char *tid=(char *)arg;
   while(1)
   {  
      pthread_mutex_lock(&mutex);
      if(ticket>0)
      {
         usleep(1000);
	 printf("%s sells ticket:%d \n",tid,ticket);
	 ticket--;
	 pthread_mutex_unlock(&mutex);
      }
      else
      { 
         pthread_mutex_unlock(&mutex);
         break;
      }
   }
}


int main()
{
   pthread_t t1,t2,t3,t4;

   pthread_mutex_init(&mutex,NULL);

   pthread_create(&t1,NULL,sellticket,"thread 1");
   pthread_create(&t2,NULL,sellticket,"thread 2");
   pthread_create(&t3,NULL,sellticket,"thread 3");
   pthread_create(&t4,NULL,sellticket,"thread 4");

   pthread_join(t1,NULL);
   pthread_join(t2,NULL);
   pthread_join(t3,NULL);
   pthread_join(t4,NULL);

   pthread_mutex_destroy(&mutex);
}

(3)条件变量

      当一个线程互斥地访问某个变量时,它可能发现在其他线程改变状态之前,它什么也做不了。例如当一个线程线程访问队列时,发现队列为空,它只能等待,只到其他线程将一个节点添加到队列中,而这种时候就需要用到条件变量。这个条件变量将相当于给这个线程发送的信号一样,当该线程接收到信号时,就知道队列不为空了,然后就可以访问了。

   条件变量函数:

       int  pthread_cond_init(pthread_cond_t  *restrict cond, const pthread_condattr_t                                               *restrict  attr);     初始化

        参数:cond  要初始化的变量 

                   attr  默认为NULL;

         int pthread_cond_destroy(pthread_cond_t  *cond);  销毁

         int  pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t  

                                                *restrict  mutex);   等待条件满足   等待时释放锁

         

         int  pthread_cond_broadcast(pthread_cond_t  *cond );广播给全部线程通知

         int  pthread_cond_signal(pthread_cond_t  *cond );给某个线程单独通知

  简单案例

#include
#include
#include
#include
#include

pthread_cond_t cond;
pthread_mutex_t mutex;

void *r1(void *arg)
{
   while(1)
   {  
      pthread_cond_wait(&cond,&mutex);
	  printf("active \n");
   }
}

void *r2(void *arg)
{
   while(1)
   {  
      pthread_cond_signal(&cond);
	  sleep(1);
   }
}

int main()
{
   pthread_t t1,t2;

   pthread_cond_init(&cond,NULL);
   pthread_mutex_init(&mutex,NULL);

   pthread_create(&t1,NULL,r2,"thread 1");
   pthread_create(&t2,NULL,r1,"thread 2");

   pthread_join(t1,NULL);
   pthread_join(t2,NULL);
  
   pthread_mutex_destroy(&mutex);
   pthread_cond_destroy(&cond);
}

 结果就是每隔一秒打印一个active。

为什么pthread_cond_wait需要互斥量?

  • 等待条件是线程间同步的一种手段,如果只有一个线程,条件不满足,一直等下去都不会满足,所有必须要有一个线程通过某些操作,改变共享变量,使原先不满足的条件变得满足,并且友好的通知等待在条件变量上的线程。
  • 条件不会无缘无故的变得满足,必然会牵扯到共享数据的变化。所有一定要用互斥锁来保护,没有互斥锁就无法安全的获取和修改数据。 

  其实简单来说就是用来释放锁,监测条件变量。但必须是原子的。

(4)条件变量使用规范

       等待条件代码:

pthread_mutex_lock(&mutex);
while(条件为假)
	pthread_cond_wait(cond,mutex);
修改条件
pthread_mutex_unlock(&mutex);

          给条件发送信号代码:

pthread_mutex_lock(&mutex);
 设置条件为真
pthread_cond_signal(cond);
pthread_mutex_inlock(&mutex);

(5)生产者消费者模型

  生产者消费者模型简单来说就是“321”原则,“321”原则就是三种关系(生产者与生产者之间互斥关系,消费者与消费者之间互斥关系,消费者与生产者之间互斥且同步),两种角色(生产者与消费者),一个交易场所(可以是各种数据结构,链表,数组)。

  最简单的就是一个生产者一个消费者,然后再加一个交易场所。(两个线程来模拟生产者消费者模型)---基于链表

#include
#include
#include
#include
#include
#include

typedef struct  linklistNode
{
	int key;
	struct linklistNode *Next;
}Node,*pNode,**ppNode;

pNode head;
pthread_cond_t cond=PTHREAD_COND_INITIALIZER;
pthread_mutex_t lock=PTHREAD_MUTEX_INITIALIZER;
void Initlinklist(ppNode head)
{
	assert(head);
    (*head)->Next=NULL;
}
int isEmpty(pNode head)
{
	return head->Next==NULL;
}
pNode NewNode(int key)
{
	pNode node=(pNode)malloc(sizeof(Node));
	if(node!=NULL)
	{
	    node->key=key;
    	node->Next=NULL;
	}
	return node;
}

void PushFront(pNode head,int key)
{
	assert(head);
	if((head->Next)==NULL)
	{
		head->Next=NewNode(key);
		return;
	}
	pNode newnode=NewNode(key);
	newnode->Next=head->Next;
	head->Next=newnode;
}
void PopFront(pNode head,int * key)
{
	assert(head);
	if(head==NULL)
	{
		return;
	}
	pNode del=head->Next;
	*key=del->key;
	head->Next=del->Next;
	free(del);
}
void Destroy(pNode head)
{
	assert(head);
    pNode cur=head;
	pNode del=NULL;
	while(cur!=NULL)
	{
		del=cur;
		cur=cur->Next;
        free(del);
	}
	head->Next=NULL;
}


void *Productor(void *arg)
{
	int key=0;
	while(1)
	{
		pthread_mutex_lock(&lock);
		key=rand()%101;
		PushFront(head,key);
		printf("Productor push %d\n",key);
		pthread_cond_signal(&cond);
		pthread_mutex_unlock(&lock);
		sleep(5);
	}
}
void *Customer(void *arg)
{
	int key=0;
	while(1)
	{
		pthread_mutex_lock(&lock);
		while(head->Next==NULL)
		{
			printf("waiting for date\n");
            pthread_cond_wait(&cond,&lock);
		}
		PopFront(head,&key);
		printf("Customer pop %d\n",key);
		pthread_mutex_unlock(&lock);
		//sleep(1);
	}
}
int main()
{

    head=(pNode)malloc(sizeof(Node));
	Initlinklist(&head);
	pthread_t t1,t2;
	srand(time(NULL));
	pthread_create(&t1,NULL,Productor,NULL);
	pthread_create(&t2,NULL,Customer,NULL);
	Destroy(head);
	pthread_join(t1,NULL);
	pthread_join(t2,NULL);
    pthread_mutex_destroy(&lock);
	pthread_cond_destroy(&cond);
	return 0;

}
 我实现的是消费者快,而生产者慢的实例,所以当生产者生成一个,消费者拿走之后,其余四秒钟消费者都在等待生产者生产。
线程的概念、控制、以及同步与互斥_第5张图片

(6)POSIX信号量

      POSIX信号量和SystemV信号量作用相同,都是用于同步操作,达到无冲突的访问共享资源的目的,单POSIX可以用于线程间同步。  

       初始化信号量:

        #include

        int sem_init(sem_t  *sem, int pshared,unsigned int value);

       参数: pshared  0表示线程间共享,非0表示进程间共享;默认为0;

                  value  信号量初始值;

       销毁信号量:

        int  sem_destroy(sem_t  *sem);

        等待信号量:

         int sem_wait(sem_t  *sem);   等待信号量,若是等待成功信号量的值减1,相当于P操作;

        发布信号量:

        int   sem_post(sem_t *sem);  发布信号量,表示资源使用完毕,可以归还资源了,将信号量值加1;

       上边的消费者生产者模型是基于链表的,其空间可以等他分配,现在写一个基于固定大小的循环队列重写消费者生产者模型。(循环结构可以用数组+模运算实现)

#include
#include
#include
#include
#define SIZE 1024
//环形队列
int arr[SIZE] = {0};
sem_t sem_pro;      //描述环形队列中的空位置
sem_t sem_con;      //描述唤醒队列中的数据
//生产者,只要环形队列有空位,便不断生产
void*productor(void*arg){
    int data = 0;
    int proIndex = 0;
    while(1){
        //有空位便生产,没空位便阻塞等消费者消费
        sem_wait(&sem_pro);
        data = rand()%1234;
        arr[proIndex] = data;
        printf("product done %d\n",data);
        proIndex = (proIndex+1)%SIZE;
        //供消费者消费的数据加1
        sem_post(&sem_con);
    }
}
//消费者,只要环形队列中有数据,就不断消费
void*consumer(void*arg){
    int data = 0;
    int conIndex = 0;
    while(1){
        //环形队列中存在数据则消费,不存在数据则阻塞,直到有数据为止
        sem_wait(&sem_con);
        data = arr[conIndex];
        printf("consume done %d\n",data);
        conIndex = (conIndex+1)%SIZE;
        //最后,消费了一个数据,空位加1
        sem_post(&sem_pro);
    }
}
 
int main(){
    pthread_t pro,con;
    sem_init(&sem_pro,0,SIZE-1);        //一开始有很多空位置
    sem_init(&sem_con,0,0);         //但并没有数据
 
    pthread_create(&pro,NULL,productor,NULL);
    pthread_create(&con,NULL,consumer,NULL);
    pthread_join(pro,NULL);
    pthread_join(con,NULL);
 
    sem_destroy(&sem_pro);
    sem_destroy(&sem_con);
    return 0;
}
线程的概念、控制、以及同步与互斥_第6张图片

 以上实现的都是单消费者单生产者mo





















你可能感兴趣的:(Linux)