线程同步

一、概述

  • 线程的最大特点是资源的共享性,但资源共享中的同步问题是多线程编程的难点。
  • linux下提供了多种方式来处理线程同步,最常用的是互斥锁条件变量信号量

二、互斥锁(mutex)

2.1 - 互斥锁常用函数

在编程中,引入了对象互斥锁的概念,来保证共享数据操作的完整性。每个对象都对应于一个可称为" 互斥锁" 的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。

  • 初始化锁。在Linux下,线程的互斥量数据类型是pthread_mutex_t。在使用前,要对它进行初始化。可以用静态和动态两种j方式初始化:

    • 静态分配:POSIX定义了一个来静态初始化互斥锁,pthread_mutex_t是一个结构,而PTHREAD_MUTEX_INITIALIZER则是一个结构常量。

       pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
      
    • 动态分配 :采用 pthread_mutex_init() 函数来初始化互斥锁,中mutexattr用于指定互斥锁属性(见下),如果为NULL则使用缺省属性

      	int pthread_mutex_init(pthread_mutex_t *mutex, 
      							const pthread_mutex_attr_t *mutexattr);
      
  • 互斥锁属性:互斥锁的属性在创建锁的时候指定,在LinuxThreads实现中仅有一个锁类型属性,不同的锁类型在试图对一个已经被锁定的互斥锁加锁时表现不同。当(glibc2.2.3,linuxthreads0.9)有四个值可供选择:

    • PTHREAD_MUTEX_TIMED_NP,这是缺省值,也就是普通锁。当一个线程加锁以后,其余请求锁的线程将形成一个等待队列,并在解锁后按优先级获得锁。这种锁策略保证了资源分配的公平性。
    • PTHREAD_MUTEX_RECURSIVE_NP嵌套锁,允许同一个线程对同一个锁成功获得多次,并通过多次unlock解锁。如果是不同线程请求,则在加锁线程解锁时重新竞争。
    • PTHREAD_MUTEX_ERRORCHECK_NP检错锁,如果同一个线程请求同一个锁,则返回EDEADLK,否则与PTHREAD_MUTEX_TIMED_NP类型动作相同。这样就保证当不允许多次加锁时不会出现最简单情况下的死锁。
    • PTHREAD_MUTEX_ADAPTIVE_NP适应锁,动作最简单的锁类型,仅等待解锁后重新竞争。
  • 加锁。不论哪种类型的锁,都不可能被两个不同的线程同时得到,而必须等待解锁。

  • 普通加锁:对共享资源的访问,要对互斥量进行加锁,如果互斥量已经上了锁,调用线程会阻塞,直到互斥量被解锁。

		int pthread_mutex_lock(pthread_mutex *mutex);
  • 测试加锁:pthread_mutex_trylock()语义与pthread_mutex_lock()类似,不同的是在锁已经被占据时返回EBUSY而不是挂起等待。
		int pthread_mutex_trylock(pthread_mutex_t *mutex);
  • 解锁。在完成了对共享资源的访问后,要对互斥量进行解锁。
		 int pthread_mutex_unlock(pthread_mutex_t *mutex);
  • 销毁锁。锁在是使用完成后,需要进行销毁以释放资源。
		int pthread_mutex_destroy(pthread_mutex *mutex);

2.2 - 互斥锁应用示例

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

//定义累加次数
#define NLOOP 5000

//定义累加的全局变量
int counter;

//静态分配 定义是一个默认锁
pthread_mutex_t counter_mutex = PTHREAD_MUTEX_INITIALIZER;

//两个线程执行的函数
void *doit(void *);


int main(int argc, char **argv) {
	
	//创建两个线程
	pthread_t tidA, tidB;
	pthread_create(&tidA, NULL, doit, NULL);
	pthread_create(&tidB, NULL, doit, NULL);
	
	//等待两个线程结束
	pthread_join(tidA, NULL);
	pthread_join(tidB, NULL);
	return 0;
}
void *doit(void *vptr) {
	int i, val;
	//累加NLOOP次
	for (i = 0; i < NLOOP; i++) {
		
		//加锁,
		pthread_mutex_lock(&counter_mutex);
		
		val = counter;
		printf("%x: %d\n", (unsigned int)pthread_self(), val + 1);
		counter = val + 1;
		
		//解锁
		pthread_mutex_unlock(&counter_mutex);
	}
	return NULL;
}

运行结果

三、条件变量(cond)

条件变量是利用线程间共享全局变量进行同步的一种机制。条件变量上的基本操作有:触发条件(当条件变为 true 时);等待条件,挂起线程直到其他线程触发条件。

  1. 初始化条件变量
    条件变量和互斥锁一样,都有静态和动态两种创建方式。
  • 静态方式使用PTHREAD_COND_INITIALIZER常量进行初始化,如下:
		pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
  • 动态方式调用pthread_cond_init()函数,API定义如下:
		int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr)

尽管POSIX标准中为条件变量定义了属性,但在Linux中没有实现,因此cond_attr值通常为NULL,且被忽略。

  • 有两个等待函数

    (1)无条件等待
       int pthread_cond_wait(pthread_cond_t *cond,pthread_mutex_t *mutex);
    (2)计时等待
       int pthread_cond_timewait(pthread_cond_t *cond,pthread_mutex *mutex,const timespec *abstime);

    • 如果在给定时刻前条件没有满足,则返回ETIMEOUT,结束等待,其中abstime以与time()系统调用相同意义的绝对时间形式出现,0表示格林尼治时间1970年1月1日0时0分0秒。

    • 无论哪种等待方式,都必须和一个互斥锁配合,以防止多个线程同时请求(用 pthread_cond_wait() 或 pthread_cond_timedwait() 请求 竞争条件 。mutex互斥锁必须是普通锁 或者适应锁

    • 且在调用pthread_cond_wait()前必须由本线程加锁(pthread_mutex_lock()),而在更新条件等待队列以前,mutex保持锁定状态,并在线程挂起进入等待前解锁。在条件满足从而离开pthread_cond_wait()之前,mutex将被重新加锁,以与进入pthread_cond_wait()前的加锁动作对应。

  • 激发条件

    (1)激活一个等待该条件的线程(存在多个等待线程时按入队顺序激活其中一个)  
       int pthread_cond_signal(pthread_cond_t *cond);
    (2)激活所有等待线程
       int pthread_cond_broadcast(pthread_cond_t *cond);

  • 销毁条件变量
       int pthread_cond_destroy(pthread_cond_t *cond);
    只有在没有线程在该条件变量上等待的时候才能销毁这个条件变量,否则返回EBUSY

3.1 - 生产消费者模型(条件变量与互斥量)


生产者与消费应用示例(条件变量与互斥量):

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

//生产者消费者模型

pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;//互斥锁
pthread_cond_t has_product = PTHREAD_COND_INITIALIZER;//条件变量

struct msg{
	struct msg* next;
	int num;	
};
struct msg *head;

void *customer(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("customer %d\n",mp->num);
		free(mp);
		
		sleep(1);
	}
}
void *product(void *p)//生产者
{
	
	struct msg *mp;
	for( ; ; )
	{
		mp = malloc(sizeof(struct msg));
		
		//模拟一张饼
		mp->num = rand()%1000+1;
		
		printf("product %d\n",mp->num);
		
		//向链表加入节点
		pthread_mutex_lock(&lock);
		//头插法
		mp->next = head;
		head = mp;
		
		//唤醒阻塞在条件上的进程
		pthread_cond_signal(&has_product);
		
		pthread_mutex_unlock(&lock);
		
		sleep(1);
	}
	
}

int main()
{
	pthread_t pid ,cid;
	pthread_create(&pid, NULL, &customer, NULL);
	pthread_create(&cid, NULL, &product, NULL);
	pthread_join(pid, NULL);
	pthread_join(cid, NULL);

	return 0;
}

运行结果

四、 信号量

如同进程一样,线程也可以通过信号量来实现通信,虽然是轻量级的。
线程使用的基本信号量函数有四个:
头文件:#include <semaphore.h>

  • 初始化信号量
       int sem_init (sem_t *sem , int pshared, unsigned int value);

    参数

    • sem - 指定要初始化的信号量;
    • pshared - 信号量 sem 的共享选项,linux只支持0,表示它是当前进程的局部信号量;
    • value - 信号量 sem 的初始值。
  • 信号量值加1
    给参数sem指定的信号量值加1。
       int sem_post(sem_t *sem);

  • 信号量值减1

    给参数sem指定的信号量值减1。
       int sem_wait(sem_t *sem);
    如果sem所指的信号量的数值为0,函数将会等待直到有其它线程使它不再是0为止。

  • 销毁信号量
    毁指定的信号量。
      int sem_destroy(sem_t *sem);

4.1 - 生产消费者模型(信号量)

生产者与消费应用示例(信号量):

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

//定义最大产品数 5
#define NUM 5

//存放产品的数组
int queue[NUM];

//定义空格数 产品数
sem_t blank_number, product_number;

//生产者操作
void *producer(void *arg) {
	int p = 0;
	while (1) {
		//空格数-1
		sem_wait(&blank_number);
		
		//给产品随机赋一个值
		queue[p] = rand() % 1000 + 1;
		printf("Produce %d\n", queue[p]);
		
		//产品数+1
		sem_post(&product_number);
		
		//数组到底的时候重新回到开头
		p = (p+1)%NUM;
		sleep(rand()%5);
	}
}

//消费者操作
void *consumer(void *arg) {
	int c = 0;
	while (1) {
		//产品数-1
		sem_wait(&product_number);
		printf("Consume %d\n", queue[c]);
		queue[c] = 0;
		//空格数+1
		sem_post(&blank_number);
		c = (c+1)%NUM;
		sleep(rand()%5);
	}
}
int main(int argc, char *argv[]) {
	pthread_t pid, cid;
	
	//初始化时,空格数为NUM,产品为0个
	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_join(pid, NULL);
	pthread_join(cid, NULL);
	
	//销毁两个进程,并释放资源
	sem_destroy(&blank_number);
	sem_destroy(&product_number);
	return 0;
}

应用示例:

参考来源:
https://blog.csdn.net/liu0808/article/details/80509255
https://baike.baidu.com/item/互斥锁/841823?fr=aladdin

Logo

更多推荐