前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Linux笔记(19)| 线程基础(三)

Linux笔记(19)| 线程基础(三)

作者头像
飞哥
发布2020-11-25 10:02:08
4300
发布2020-11-25 10:02:08
举报

前面两节讲了线程的一些基础知识,这一节还是关于线程的内容,主要说一下线程的同步问题。线程的同步是一个很重要的内容,因为这关系到线程之间的协调合作,否则可能会产生冲突。

线程的同步通常可以用互斥锁和条件变量来解决。

1、互斥锁

互斥锁是一个简单的锁定命令,它可以用来锁定对共享资源的访问,对于线程来说,整个地址空间都是共享的资源,所以线程的任何资源都是共享的。

对于互斥锁的理解,我们可以打个这样的比方:

比如厕所就是共享资源,如果你想要上厕所,看到厕所里没人,那么你就可以进去,然后把锁给锁上,这个时候如果别人也想要上厕所,他是没有办法的,他只能等待你出来之后才可以获取厕所这个资源。而你出来之后,这个资源就被释放了,也就是互斥锁的解锁,这个时候别人可以获得这个资源,同时进行上锁,对资源的访问进行保护,防止发生冲突。

在使用互斥锁的时候,首先要进行初始化:

代码语言:javascript
复制
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr);

第一个参数是互斥锁对象,第二个参数是互斥锁的属性,属性我们一般使用默认的就好了。成功返回0,失败返回错误编号。

如果要销毁一个互斥锁,可以使用下面的函数:

代码语言:javascript
复制
int pthread_mutex_destroy(pthread_mutex_t *mutex);

初始化完了之后,我们就可以对互斥锁进行加锁,加锁有两个函数可以调用:

代码语言:javascript
复制
 int pthread_mutex_lock(pthread_mutex_t *mutex);
代码语言:javascript
复制
int pthread_mutex_trylock(pthread_mutex_t *mutex);

如果上锁成功都是返回0,如果上锁失败,第一个函数会被阻塞,而第二个函数会立即返回,并且返回一个错误编号。

解锁函数:

代码语言:javascript
复制
int pthread_mutex_unlock(pthread_mutex_t *mutex);

互斥锁的使用比较简单,它就是用在对某些资源的访问的时候进行保护,在访问某个资源之前先上锁,访问完之后再解锁,这样在访问期间就不会有其他的程序试图访问而造成冲突。

2、条件变量

互斥锁可以解决一些资源竞争的问题,但是互斥锁只有两种状态,这使得它的用途非常有限,条件变量也可以解决线程同步问题,是对互斥锁的补充。条件变量允许线程阻塞并等待另一个线程发送的信号,当收到信号之后,阻塞的线程就被唤醒并试图锁定与之相关的互斥锁。

条件变量是用来等待线程而不是上锁的,条件变量通常和互斥锁一起使用。条件变量之所以要和互斥锁一起使用,主要是因为互斥锁的一个明显的特点就是它只有两种状态:锁定和非锁定,而条件变量可以通过允许线程阻塞和等待另一个线程发送信号来弥补互斥锁的不足,所以互斥锁和条件变量通常一起使用。

接下来看一下和条件变量相关的函数:

初始化:

代码语言:javascript
复制
int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr);

销毁:

代码语言:javascript
复制
int pthread_cond_destroy(pthread_cond_t *cond);

阻塞:

代码语言:javascript
复制
 int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
代码语言:javascript
复制
int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex,
                           const struct timespec *abstime);

阻塞退出:

代码语言:javascript
复制
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);

第一个只会唤醒一个线程,而第二个是唤醒所有线程。

怎么理解这些函数呢?可能要结合例子才能讲清楚。

关于这个有一个经典的问题就是“生产者-消费者”问题。大致就是说,有一块共享内存,生产者往里面写数据,消费者从里面读数据。如果生产者写数据太快了,缓冲区就会满掉,这时候再往里面写数据就会覆盖原来的。同样的,如果消费者读数据太快了,可能所有数据都读完了,导致没有数据可读。

这都不是我们想要的,因此我们需要对他们两个的步调进行一个调控,当写数据写满了之后,就要进行等待(或者说阻塞),这时可以调用pthread_cond_wait函数,这个函数可以将线程阻塞,同时等待一个信号,信号来了再唤醒,等待什么信号呢?这当然是用户指定的,对于生产者来说,它要等待一个“NotFull”的信号,也就是说只要缓冲区不是满的,我就可以往里面写数据,而这个信号可以由消费者发给他,消费者只要读出一个数据,缓冲区就不是满的了,这时可以发一个信号给生产者。

同样的,对于消费者来说,如果缓冲区是空的,也调用wai函数进行阻塞等待,只不过他等待的信号是“NotEmpty”,也就是说只要缓冲区不是空的,我就可以进行读,而这个信号可以是生产者发给他,只要生产者写入了一个数据,消费者就可以读取。

另外,不管是生产者还是消费者,都需要互斥锁来进行保护。举个例子:

生产者先对互斥锁上锁,然后开始写入数据,写完一个数据之后解锁,写第二个数据时也是先上锁,写完之后再解锁。如果在写的时候发现缓冲区满了,就调用wait函数进行等待。

然后消费者从里面读数据。这时可能会有一个疑问,生产者写数据之前不是上了锁吗,消费者怎么可以访问数据?没错,生产者是上了锁,但是当你调用wai函数的时候,会先解锁,然后阻塞等待,直到满足:

1、获得相应的条件变量(信号)

2、可以对互斥锁上锁

这两个条件才可以被唤醒。

也就是说生产者发现缓冲区满了,调用wait函数进行阻塞等待,这个函数会自动解锁,解锁完了之后,消费者就可以上锁,然后读数据,当消费者发现缓冲区为空的时候,也是调用wait函数,然后自动解锁,生产者获得锁。

条件变量可能一下子不是很好理解,大家可以多看几篇博客内容,

https://www.cnblogs.com/harlanc/p/8596211.html

大家也可以看一看b站上关于这个的讲解:

https://www.bilibili.com/video/BV1z5411W7aQ?from=search&seid=11144669287333149745

我认为需要着重理解一下pthread_cond_wait函数的工作,要特别注意:

1、调用这个函数会发生什么?线程将会被阻塞,而且会释放互斥锁。

2、什么时候会被唤醒?等到了那个条件变量,并且可以对互斥锁上锁。这两者缺一不可。

接下来是生产者消费者模型的代码:

代码语言:javascript
复制
#include<stdio.h>
#include<pthread.h>
#include <unistd.h>

#define BUFFER_SIZE 4
#define OVER -1

struct producers
{
  int buffer[BUFFER_SIZE];
  pthread_mutex_t lock;
  int readpos,writepos;
  pthread_cond_t NotEmpty;
  pthread_cond_t NotFull;
} buffer;

void init(struct producers *b)
{
  pthread_mutex_init(&b->lock,NULL);
  pthread_cond_init(&b->NotEmpty,NULL);
  pthread_cond_init(&b->NotFull,NULL);
  b->readpos=b->writepos=0;
}

void put(struct producers *b,int data)
{
  int ret;
  ret=pthread_mutex_lock(&b->lock);
  if(ret==0)
  {
    printf("生产者上锁成功\n");
  }
  else{
    printf("生产者上锁失败\n");
  }
  
  while((b->writepos+1)%BUFFER_SIZE==b->readpos )
  {
    printf("缓冲区满了,此时writepos=%d,readpos=%d\n",b->writepos,b->readpos);
//    sleep(1);
    pthread_cond_wait(&b->NotFull,&b->lock);
    printf("生产者从阻塞处唤醒\n");
  }
  b->buffer[b->writepos]=data;
  printf("向位置%d处写入了数据%d,readpos=%d\n",b->writepos,data,b->readpos);
  b->writepos++;
  if(b->writepos>=BUFFER_SIZE)
  {
    b->writepos=0;
  }
  pthread_cond_signal(&b->NotEmpty);
  pthread_mutex_unlock(&b->lock);
  printf("生产者已解锁\n");
}

int get(struct producers*b)
{
  int data,ret;
  ret=pthread_mutex_lock(&b->lock);
  if(ret==0)
  {
    printf("消费者上锁成功\n");
  }
  else{
    printf("消费者上锁失败\n");
  }
  while(b->writepos==b->readpos)
  {
    pthread_cond_wait(&b->NotEmpty,&b->lock);
    printf("消费者从阻塞处唤醒\n");
  }
  data=b->buffer[b->readpos];
  printf("消费者从%d处获取了数据%d\n",b->readpos,data);
  b->readpos++;
  if(b->readpos==BUFFER_SIZE)
  {
    b->readpos=0;
  }
  pthread_cond_signal(&b->NotFull);
  pthread_mutex_unlock(&b->lock);
  printf("消费者已解锁\n\n");
  return data;
}

void *producer(void* data)
{
  printf("进入生产者处理函数\n");
  for(int n=0;n<10;n++)
  {
//    printf("生产者生产了数据%d \n",n);
    printf("放入%d前\n",n);
    put(&buffer,n);
    printf("放入%d后\n\n",n);
  }
  put(&buffer,OVER);
  return NULL;
}

void *consumer(void *data)
{
  printf("进入消费者处理函数\n");
  int d;
  while(1)
  {
    d=get(&buffer);
    if(d==OVER)
    {
      break;
    }
//    printf("消费者取出了数据:%d \n",d);
  }
  return NULL;
}



int main(int argc,char* argv[])
{
  pthread_t thproducer,thconsumer;
  void *retval;
  init(&buffer);
  pthread_create(&thproducer,NULL,producer,0);
  pthread_create(&thconsumer,NULL,consumer,0);
  pthread_join(thproducer,&retval);
  pthread_join(thconsumer,&retval);
  return 0;
}

这段代码理解起来不是很容易,为了搞清楚程序是怎么执行的,我加入了很多的打印信息,并且使用了gdb调试。

实际上,这段代码每次执行的结果都可能不一样,因为这和线程的调度有关,线程的调度在这里是比较随机的,并不是唯一确定的,因此,同样的代码打印的结果不一样也是正常的。

来看一下两次执行的结果:

好了,这就是今天要分享的内容。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-11-16,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 电子技术研习社 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档