linux线程同步

同步概念

  • 所谓同步,即同时起步,协调一致。不同的对象,对“同步”的理解方式略有不同。如,设备同步,是指在两个设备之间规定一个共同的时间参考;数据库同步,是指让两个或多个数据库内容保持一致,或者按需要部分保持一致;文件同步,是指让两个或多个文件夹里的文件保持一致。等等
  • 而编程中、通信中所说的同步与生活中大家印象中的同步概念略有差异。“同”字应是指协同、协助、互相配合。主旨在协同步调,按预定的先后次序运行。

线程同步

  • 同步即协同步调,按预定的先后次序运行
  • 线程同步,指一个线程发出某一功能调用时,在没有得到结果之前,该调用不返回。同时其它线程为保证数据一致性,不能调用该功能
  • 举例1: 银行存款 5000。柜台,折:取3000;提款机,卡:取 3000。剩余:2000
  • 举例2: 内存中100字节,线程T1欲填入全1, 线程T2欲填入全0。但如果T1执行了50个字节失去cpu,T2执行,会将T1写过的内容覆盖。当T1再次获得cpu继续 从失去cpu的位置向后写入1,当执行结束,内存中的100字节,既不是全1,也不是全0。
  • 产生的现象叫做“与时间有关的错误”(time related)。为了避免这种数据混乱,线程需要同步。
  • “同步”的目的,是为了避免数据混乱,解决与时间有关的错误。实际上,不仅线程间需要同步,进程间、信号间等等都需要同步机制。因此,所有“多个控制流,共同操作一个共享资源”的情况,都需要同步

数据混乱原因

  1. 资源共享(独享资源则不会)
  2. 调度随机(意味着数据访问会出现竞争)
  3. 线程间缺乏必要的同步机制。

以上3点中,前两点不能改变,欲提高效率,传递数据,资源必须共享。只要共享资源,就一定会出现竞争。只要存在竞争关系,数据就很容易出现混乱。所以只能从第三点着手解决。使多个线程在访问共享资源的时候,出现互斥。

互斥量(mutex)

  • Linux中提供一把互斥锁mutex(也称之为互斥量)。每个线程在对资源操作前都尝试先加锁,成功加锁才能操作,操作结束解锁。资源还是共享的,线程间也还是竞争的, 但通过“锁”就将资源的访问变成互斥操作,而后与时间有关的错误也不会再产生了。
  • 但,应注意:同一时刻,只能有一个线程持有该锁。
  • 当A线程对某个全局变量加锁访问,B在访问前尝试加锁,拿不到锁,B阻塞。C线程不去加锁,而直接访问该全局变量,依然能够访问,但会出现数据混乱
  • 互斥锁实质上是操作系统提供的一把“建议锁”(又称“协同锁”),建议程序中有多线程访问共享资源的时候使用该机制。但,并没有强制限定。
  • 因此,即使有了mutex,如果有线程不按规则来访问数据,依然会造成数据混乱。

主要应用函数:

pthread_mutex_init
pthread_mutex_destroy
pthread_mutex_lock
pthread_mutex_trylock
pthread_mutex_unlock

//以上5个函数的返回值都是:成功返回0, 失败返回错误号。
//pthread_mutex_t 类型,其本质是一个结构体。为简化理解,应用时可忽略其实现细节,简单当成整数看待。
//pthread_mutex_t mutex; 变量mutex只有两种取值1、0。

pthread_mutex_init  初始化一个互斥锁(互斥量) ---> 初值可看作1

  • int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
  • 参数1:传出参数,调用时应传 &mutex
  • restrict关键字:只用于限制指针,告诉编译器,所有修改该指针指向内存中内容的操作,只能通过本指针完成。不能通过除本指针以外的其他变量或指针修改
  • 参2:互斥量属性。是一个传入参数,通常传NULL,选用默认属性(线程间共享)。 参APUE.12.4同步属性
  1. 静态初始化:如果互斥锁 mutex 是静态分配的(定义在全局,或加了static关键字修饰),可以直接使用宏进行初始化。e.g.  pthead_mutex_t muetx = PTHREAD_MUTEX_INITIALIZER;
  2. 动态初始化:局部变量应采用动态初始化。e.g.  pthread_mutex_init(&mutex, NULL)

pthread_mutex_destroy  销毁一个互斥锁

  • int pthread_mutex_destroy(pthread_mutex_t *mutex);

pthread_mutex_lock  加锁。可理解为将mutex--(或-1)

  • int pthread_mutex_lock(pthread_mutex_t *mutex);

pthread_mutex_unlock 解锁。可理解为将mutex ++(或+1)

  • int pthread_mutex_unlock(pthread_mutex_t *mutex);

pthread_mutex_trylock  尝试加锁 

  • int pthread_mutex_trylock(pthread_mutex_t *mutex);

加锁与解锁 

 lock与unlock:

  • lock尝试加锁,如果加锁不成功,线程阻塞,阻塞到持有该互斥量的其他线程解锁为止。
  • unlock主动解锁函数,同时将阻塞在该锁上的所有线程全部唤醒,至于哪个线程先被唤醒,取决于优先级、调度。默认:先阻塞、先唤醒。
  • 例如:T1、T2、 T3、 T4 使用一把mutex锁。T1加锁成功,其他线程均阻塞,直至T1解锁。T1解锁后,T2、 T3、 T4均被唤醒,并自动再次尝试加锁。
  • 可假想mutex锁 init成功初值为1。 lock 功能是将mutex--。 unlock将mutex++

lock与trylock:

  • lock加锁失败会阻塞,等待锁释放。
  • trylock加锁失败直接返回错误号(如:EBUSY),不阻塞。

看如下程序:该程序是非常典型的,由于共享、竞争而没有加任何同步机制,导致产生于时间有关的错误,造成数据混乱:

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

void *tfn(void *arg)
{
	srand(time(NULL));
	while (1) 
	{

		printf("hello ");
		sleep(rand() % 3);	/*模拟长时间操作共享资源,导致cpu易主,产生与时间有关的错误*/
		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;
}

修改该程序,使用mutex互斥锁进行同步

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

pthread_mutex_t mutex;      //定义锁

void *tfn(void *arg)
{
	srand(time(NULL));

	while (1) 
	{
		pthread_mutex_lock(&mutex);   // mutex--
		printf("hello ");
		sleep(rand() % 3);	         /* 模拟长时间操作共享资源,导致cpu易主,产生与时间有关的错误 */
		printf("world\n");
		pthread_mutex_unlock(&mutex); // mutex++
		sleep(rand() % 3);
	}

	return NULL;
}

int main(void)
{
	int flg = 5;
	pthread_t tid;
	srand(time(NULL));

	pthread_mutex_init(&mutex, NULL);  // mutex==1
	pthread_create(&tid, NULL, tfn, NULL);
	while (flg--) 
	{
		pthread_mutex_lock(&mutex); // mutex--  
		printf("HELLO ");
		sleep(rand() % 3);
		printf("WORLD\n");
		pthread_mutex_unlock(&mutex);  // mutex++
		sleep(rand() % 3);

	}
	pthread_cancel(tid);
	pthread_join(tid, NULL);

	pthread_mutex_destroy(&mutex);  

	return 0;
}

/* 线程之间共享资源stdout */

运行结果

将unlock挪至第二个sleep后,发现交替现象很难出现。

  • 线程在操作完共享资源后本应该立即解锁,但修改后,线程抱着锁睡眠。睡醒解锁后又立即加锁,这两个库函数本身不会阻塞。所以在这两行代码之间失去cpu的概率很小。因此,另外一个线程很难得到加锁的机会。
  • 在访问共享资源前加锁,访问结束后立即解锁。锁的“粒度”应越小越好。
  • 在使用互斥量的时候,要避免死锁。死锁:(1)线程试图对同一个互斥量加锁两次(解决:写代码时加锁环节不写复杂);(2)交叉锁:线程1拥有A锁,请求获得B锁;线程2拥有B锁,请求获得A锁(解决:每个线程申请锁的顺序要抑制,如果申请一把锁,申请另外一把锁的时候,申请失败,应该释放已经掌握的)。

读写锁

  • 读写锁适合于对数据结构的读次数比写次数多得多的情况。因为,读模式锁定时可以共享以写模式锁住时意味着独占,所以读写锁又叫共享-独占锁(共享互斥锁)

读写锁的行为

 

读写锁状态:

一把读写锁具备三种状态:

  • 1. 读模式下加锁状态 (读锁)
  • 2. 写模式下加锁状态 (写锁)
  • 3. 不加锁状态

读写锁特性

  1. 读写锁是“写模式加锁”时, 解锁前,所有对该锁加锁的线程都会被阻塞。
  2. 读写锁是“读模式加锁”时, 如果线程以读模式对其加锁会成功;如果线程以写模式加锁会阻塞。
  3. 读写锁是“读模式加锁”时, 既有试图以写模式加锁的线程,也有试图以读模式加锁的线程。那么读写锁会阻塞随后的读模式锁请求。优先满足写模式锁。读锁、写锁并行阻塞,写锁优先级高。
  4. 读写锁也叫共享-独占锁。当读写锁以读模式锁住时,它是以共享模式锁住的;当它以写模式锁住时,它是以独占模式锁住的。写独占、读共享。
  5. 读写锁非常适合于对数据结构读的次数远大于写的情况。

主要应用函数

pthread_rwlock_init函数

pthread_rwlock_destroy函数

pthread_rwlock_rdlock函数  

pthread_rwlock_wrlock函数

pthread_rwlock_tryrdlock函数

pthread_rwlock_trywrlock函数

pthread_rwlock_unlock函数

以上7 个函数的返回值都是:成功返回0, 失败直接返回错误号。

pthread_rwlock_t类型 用于定义一个读写锁变量。

pthread_rwlock_t rwlock;

pthread_rwlock_init  初始化一把读写锁

  • int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
  • 参2:attr表读写锁属性,通常使用默认属性,传NULL即可。

pthread_rwlock_destroy  销毁一把读写锁

  • int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

pthread_rwlock_rdlock  以读方式请求读写锁。(常简称为:请求读锁)

  • int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);

pthread_rwlock_wrlock  以写方式请求读写锁。(常简称为:请求写锁)

  • int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);

pthread_rwlock_unlock  解锁

  • int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

pthread_rwlock_tryrdlock   非阻塞以读方式请求读写锁(非阻塞请求读锁)

  • int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);

pthread_rwlock_trywrlock  非阻塞以写方式请求读写锁(非阻塞请求写锁)

  • int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
/* 3个线程不定时 "写" 全局资源,5个线程不定时 "读" 同一全局资源 */
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>

int counter;                          // 全局资源
pthread_rwlock_t rwlock;

void *th_write(void *arg)
{
	int t;
	int i = (int)arg;

	while (1) 
	{
		t = counter;
		usleep(1000);

		pthread_rwlock_wrlock(&rwlock);  // 请求写锁
		printf("=======write %d: %lu: counter = %d ++counter = %d\n", i, pthread_self(), t, ++counter);
		pthread_rwlock_unlock(&rwlock);

		sleep(1);
	}
    
	return NULL;
}

void *th_read(void *arg)
{
	int i = (int)arg;

	while (1) 
	{
		pthread_rwlock_rdlock(&rwlock);   // 请求读锁
		printf("----------------------------read %d: %lu: %d\n", i, pthread_self(), counter);
		pthread_rwlock_unlock(&rwlock);

		sleep(1);
	}
    
	return NULL;
}

int main(void)
{
	int i;
	pthread_t tid[8];

	pthread_rwlock_init(&rwlock, NULL);

	for (i = 0; i < 3; i++)
		pthread_create(&tid[i], NULL, th_write, (void *)i);

	for (i = 0; i < 5; i++)
		pthread_create(&tid[i+3], NULL, th_read, (void *)i);

	for (i = 0; i < 8; i++)
		pthread_join(tid[i], NULL);

	pthread_rwlock_destroy(&rwlock);    //  释放读写琐

	return 0;
}

运行结果

条件变量

  • 与互斥锁不同,条件变量是用来等待而不是用来上锁的。条件变量用来自动阻塞一个线程,直到某特殊情况发生为止。通常条件变量和互斥锁同时使用。
  • 条件变量使我们可以睡眠等待某种条件出现。条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:一个线程等待"条件变量的条件成立"而挂起;另一个线程使"条件成立"(给出条件成立信号)
  • 条件的检测是在互斥锁的保护下进行的。如果一个条件为假,一个线程自动阻塞,并释放等待状态改变的互斥锁。如果另一个线程改变了条件,它发信号给关联的条件变量,唤醒一个或多个等待它的线程,重新获得互斥锁,重新评价条件。如果两进程共享可读写的内存,条件变量可以被用来实现这两进程间的线程同步。
  • 使用条件变量之前要先进行初始化。可以在单个语句中生成和初始化一个条件变量如:pthread_cond_t my_condition=PTHREAD_COND_INITIALIZER;(用于进程间线程的通信)。可以利用函数pthread_cond_init动态初始化。

条件变量分为两部分: 条件和变量。

  • 条件本身是由互斥量保护的.。
  • 线程在改变条件状态前先要锁住互斥量.。
  • 它利用线程间共享的全局变量进行同步的一种机制。

主要应用函数

pthread_cond_init函数

pthread_cond_destroy函数

pthread_cond_wait函数

pthread_cond_timedwait函数

pthread_cond_signal函数

pthread_cond_broadcast函数

以上6 个函数的返回值都是:成功返回0, 失败直接返回错误号。

pthread_cond_t类型 用于定义条件变量

pthread_cond_t cond;

pthread_cond_init  初始化一个条件变量

  • int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
  • 参2:attr表条件变量属性,通常为默认值,传NULL即可,也可以使用静态初始化的方法,初始化条件变量:
  • pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

pthread_cond_destroy  销毁一个条件变量

  • int pthread_cond_destroy(pthread_cond_t *cond);

pthread_cond_wait   阻塞等待一个条件变量

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

函数作用:

  1. 阻塞等待条件变量cond(参1)满足
  2. 释放已掌握的互斥锁(解锁互斥量)相当于pthread_mutex_unlock(&mutex); 1.2.两步为一个原子操作。
  3. 当被唤醒,pthread_cond_wait函数返回时,解除阻塞并重新申请获取互斥锁pthread_mutex_lock(&mutex);

pthread_cond_timedwait   限时等待一个条件变量

  • int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime)
  • 参3: 参看man sem_timedwait函数,查看struct timespec结构体。
​struct timespec
{
    time_t tv_sec; /* seconds */ 秒

    long   tv_nsec; /* nanosecondes*/ 纳秒
}
  • 形参abstime:绝对时间。
  • 如:time(NULL)返回的就是绝对时间。而alarm(1)是相对时间,相对当前时间定时1秒钟。
  • struct timespec t = {1, 0};
  • pthread_cond_timedwait (&cond, &mutex, &t); 只能定时到 1970年1月1日 00:00:01秒(早已经过去) 

正确用法:

  • time_t cur = time(NULL); 获取当前时间。
  • struct timespec t; 定义timespec 结构体变量t
  • t.tv_sec = cur+1; 定时1秒
  • pthread_cond_timedwait (&cond, &mutex, &t); 传参 参APUE.11.6线程同步条件变量小节

在讲解setitimer函数时我们还提到另外一种时间类型:     

struct timeval 
{
    time_t      tv_sec;  /* seconds */ 秒
    
    suseconds_t tv_usec; /* microseconds */ 微秒
};

pthread_cond_signal  唤醒至少一个阻塞在条件变量上的线程

  • int pthread_cond_signal(pthread_cond_t *cond);

pthread_cond_broadcast   唤醒全部阻塞在条件变量上的线程

  • int pthread_cond_broadcast(pthread_cond_t *cond);

生产者消费者条件变量模型

  • 线程同步典型的案例即为生产者消费者模型,而借助条件变量来实现这一模型,是比较常见的一种方法。假定有两个线程,一个模拟生产者行为,一个模拟消费者行为。两个线程同时操作一个共享资源(一般称之为汇聚),生产向其中添加产品,消费者从中消费掉产品。
/*借助条件变量模拟 生产者-消费者 问题*/
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <stdio.h>

/*链表作为公享数据,需被互斥量保护*/
struct msg 
{
	struct msg *next;
	int num;
};

struct msg *head;
struct msg *mp;

/* 静态初始化 一个条件变量和一个互斥量*/
pthread_cond_t has_product = PTHREAD_COND_INITIALIZER;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;

void *consumer1(void *p)
{
	for (;;)
	{
		pthread_mutex_lock(&lock);
		while (head == NULL) 
		{     //头指针为空,说明没有节点
			pthread_cond_wait(&has_product, &lock);
		}
		mp = head;      
		head = head->next;    // 模拟消费掉一个产品
		pthread_mutex_unlock(&lock);

		printf("-Consume1 ---%d\n", mp->num);
		free(mp);
		mp = NULL;
		sleep(rand() % 5);
	}
}

void *consumer2(void *p)
{
	for (;;)
	{
		pthread_mutex_lock(&lock);
		while (head == NULL)
		{     //头指针为空,说明没有节点   
			pthread_cond_wait(&has_product, &lock);
		}
		mp = head;
		head = head->next;    // 模拟消费掉一个产品
		pthread_mutex_unlock(&lock);

		printf("-Consume2 ---%d\n", mp->num);
		free(mp);
		mp = NULL;
		sleep(rand() % 5);
	}
}

void *producer(void *p)
{
	for (;;) 
	{
		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[2];
	srand(time(NULL));

	pthread_create(&pid, NULL, producer, NULL);
	pthread_create(&cid[0], NULL, consumer1, NULL);
	pthread_create(&cid[1], NULL, consumer2, NULL);

	pthread_join(pid, NULL);
	pthread_join(cid[0], NULL);
	pthread_join(cid[1], NULL);

	return 0;
}

运行结果

条件变量的优点

  • 相较于mutex而言,条件变量可以减少竞争。
  • 如直接使用mutex,除了生产者、消费者之间要竞争互斥量以外,消费者之间也需要竞争互斥量,但如果汇聚(链表)中没有数据,消费者之间竞争互斥锁是无意义的。有了条件变量机制以后,只有生产者完成生产,才会引起消费者之间的竞争。提高了程序效率。 

信号量

  • 进化版的互斥锁(1 --> N
  • 由于互斥锁的粒度比较大,如果我们希望在多个线程间对某一对象的部分数据进行共享,使用互斥锁是没有办法实现的,只能将整个数据对象锁住。这样虽然达到了多线程操作共享数据时保证数据正确性的目的,却无形中导致线程的并发性下降。线程从并行执行,变成了串行执行。与直接使用单进程无异。
  • 信号量,是相对折中的一种处理方式,既能保证同步,数据不混乱,又能提高线程并发。

主要应用函数

sem_init函数

sem_destroy函数

sem_wait函数

sem_trywait函数

sem_timedwait函数

sem_post函数

以上6 个函数的返回值都是:成功返回0, 失败返回-1,同时设置errno。(注意,它们没有pthread前缀)

sem_t类型,本质仍是结构体。但应用期间可简单看作为整数,忽略实现细节(类似于使用文件描述符)。

sem_t sem; 规定信号量sem不能 < 0。头文件 <semaphore.h>

信号量基本操作:

sem_wait:            1. 信号量大于0,则信号量-- (类比pthread_mutex_lock)

  |                          2. 信号量等于0,造成线程阻塞

对应

  |

sem_post: 将信号量++,同时唤醒阻塞在信号量上的线程 (类比pthread_mutex_unlock)

但,由于sem_t的实现对用户隐藏,所以所谓的++、--操作只能通过函数来实现,而不能直接++、--符号。

信号量的初值,决定了占用信号量的线程的个数。

sem_init  初始化一个信号量

  • int sem_init(sem_t *sem, int pshared, unsigned int value);
  • 参1:sem信号量
  • 参2:pshared取0用于线程间;取非0(一般为1)用于进程间
  • 参3:value指定信号量初值

sem_destroy  销毁一个信号量

  • int sem_destroy(sem_t *sem);

sem_wait  给信号量加锁 --

  • int sem_wait(sem_t *sem);

sem_post  给信号量解锁 ++

  • int sem_post(sem_t *sem);

sem_trywait  尝试对信号量加锁 -- (与sem_wait的区别类比lock和trylock)

  • int sem_trywait(sem_t *sem);

sem_timedwait  限时尝试对信号量加锁 

  • int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
  • 参2:abs_timeout采用的是绝对时间。
  • 定时1秒:
  • time_t cur = time(NULL); 获取当前时间。
  • struct timespec t; 定义timespec 结构体变量t
  • t.tv_sec = cur+1; 定时1秒
  • t.tv_nsec = t.tv_sec +100;
  • sem_timedwait(&sem, &t); 传参
/*信号量实现 生产者 消费者问题*/
#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);                    // 生产者将空格子数--,为0则阻塞等待
		queue[i] = rand() % 1000 + 1;               // 生产一个产品
		printf("----Produce---%d\n", queue[i]);        
		sem_post(&product_number);                  // 将产品数++

		i = (i+1) % NUM;                            // 借助下标实现环形
		sleep(rand()%3);
	}
}

void *consumer1(void *arg)
{
	int i = 0;

	while (1) 
	{
		sem_wait(&product_number);                  // 消费者将产品数--,为0则阻塞等待
		printf("-Consume1---%d\n", queue[i]);
		queue[i] = 0;                               // 消费一个产品 
		sem_post(&blank_number);                    // 消费掉以后,将空格子数++

		i = (i+1) % NUM;
		sleep(rand()%3);
	}
}

void *consumer2(void *arg)
{
	int i = 0;

	while (1)
	{
		sem_wait(&product_number);                  // 消费者将产品数--,为0则阻塞等待
		printf("-Consume2---%d\n", queue[i]);
		queue[i] = 0;                               // 消费一个产品 
		sem_post(&blank_number);                    // 消费掉以后,将空格子数++

		i = (i + 1) % NUM;
		sleep(rand() % 3);
	}
}

int main(int argc, char *argv[])
{
	pthread_t pid, cid;

	sem_init(&blank_number, 0, NUM);                // 初始化空格子信号量为5
	sem_init(&product_number, 0, 0);                // 产品数为0

	pthread_create(&pid, NULL, producer, NULL);
	pthread_create(&cid, NULL, consumer1, NULL);
	pthread_create(&cid, NULL, consumer2, NULL);


	pthread_join(pid, NULL);
	pthread_join(cid, NULL);

	sem_destroy(&blank_number);
	sem_destroy(&product_number);

	return 0;
}

条件变量与互斥锁、信号量的区别

  1. 互斥锁必须总是由给它上锁的线程解锁,信号量的挂出即不必由执行过它的等待操作的同一进程执行。一个线程可以等待某个给定信号灯,而另一个线程可以挂出该信号灯。
  2. 互斥锁要么锁住,要么被解开(二值状态,类型二值信号量)。
  3. 由于信号量有一个与之关联的状态(它的计数值),信号量挂出操作总是被记住。然而当向一个条件变量发送信号时,如果没有线程等待在该条件变量上,那么该信号将丢失。
  4. 互斥锁是为了上锁而设计的,条件变量是为了等待而设计的,信号灯即可用于上锁,也可用于等待,因而可能导致更多的开销和更高的复杂性。

哲学家用餐模型分析

多线程版:

选用互斥锁mutex,如创建5个, pthread_mutex_t m[5];

模型抽象:

  • 5个哲学家 --> 5个线程    5支筷子 --> 5把互斥锁    食物:共享资源   int left(左手), right(右手)
  • 5个哲学家使用相同的逻辑,可通用一个线程主函数,void *tfn(void *arg),使用参数来表示线程编号:int i = (int)arg;
  • 哲学家线程根据编号知道自己是第几个哲学家,而后选定锁,锁住,吃饭。否则哲学家thinking。

       A   B   C   D   E

      5支筷子,在逻辑上形成环: 0   1   2   3   4   分别对应5个哲学家:

所以有:

if(i == 4)

        left = i, right = 0;

else

        left = i, right = i+1;

振荡:如果每个人都攥着自己左手的锁,尝试去拿右手锁,拿不到则将锁释放。过会儿五个人又同时再攥着左手锁尝试拿右手锁,依        然拿不到。如此往复形成另外一种极端死锁的现象——振荡。

避免振荡现象:只需5个人中,任意一个人,拿锁的方向与其他人相逆即可(如:E,原来:左:4,右:0 现在:左:0, 右:4)。

所以以上if else语句应改为:

if(i == 4)

        left = 0, right = i;

else

        left = i, right = i+1;

而后, 首先应让哲学家尝试加左手锁:

while {

pthread_mutex_lock(&m[left]); 如果加锁成功,函数返回再加右手锁,

                                                 如果失败,应立即释放左手锁,等待。

若,左右手都加锁成功 --> 吃 --> 吃完 --> 释放锁(应先释放右手、再释放左手,是加锁顺序的逆序)

}

主线程(main)中,初始化5把锁,销毁5把锁,创建5个线程(并将i传递给线程主函数),回收5个线程。

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

pthread_mutex_t m[5];

void *tfn(void *arg)
{
	int i, l, r;

	srand(time(NULL));
	i = (int)arg;

	if (i == 4)
		l = 0, r = i;
	else
		l = i; r = i+1;

	while (1) 
	{
		pthread_mutex_lock(&m[l]);
		if (pthread_mutex_trylock(&m[r]) == 0) 
		{
			printf("\t%c is eating \n", 'A'+i);
			pthread_mutex_unlock(&m[r]);
		}
		pthread_mutex_unlock(&m[l]);
		sleep(rand() % 5);
}

	return NULL;
}

int main(void)
{
	int i;
	pthread_t tid[5];
	
	for (i = 0; i < 5; i++)
		pthread_mutex_init(&m[i], NULL);

	for (i = 0; i < 5; i++)
		pthread_create(&tid[i], NULL, tfn, (void *)i);

	for (i = 0; i < 5; i++)
		pthread_join(tid[i], NULL);

	for (i = 0; i < 5; i++)
		pthread_mutex_destroy(&m[i]);

	return 0;
}

避免死锁的方法:

  • 1. 当得不到所有所需资源时,放弃已经获得的资源,等待。
  • 2. 保证资源的获取顺序,要求每个线程获取资源的顺序一致。如:A获取顺序1、2、3;B顺序应也是1、2、3。若B为3、2、1则易出现死锁现象。

多进程版

相较于多线程需注意问题:

  • 需注意如何共享信号量 (注意:坚决不能使用全局变量 sem_t s[5])

实现:

main函数中:

循环 sem_init(&s[i], 0, 1); 将信号量初值设为1,信号量变为互斥锁。

循环 sem_destroy(&s[i]);

循环创建 5 个子进程。 if(i < 5) 中完成子进程的代码逻辑。

循环回收 5 个子进程。

子进程中:

if(i == 4)  

       left = 0, right == 4;

else

       left = i, right = i+1;

while (1) {

使用 sem_wait(&s[left]) 锁左手,尝试锁右手,若成功 --> 吃; 若不成功 --> 将左手锁释放。

吃完后, 先释放右手锁,再释放左手锁。

}

【重点注意】:

直接将sem_t s[5]放在全局位置,试图用于子进程间共享是错误的!应将其定义放置与mmap共享映射区中。main中:

sem_t *s = mmap(NULL, sizeof(sem_t) * 5, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANON, -1, 0);

使用方式:将s当成数组首地址看待,与使用数组s[5]没有差异。

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <stdlib.h>
#include <semaphore.h>
#include <sys/mman.h>
#include <sys/wait.h>

int main(void)
{
	int i;
	pid_t pid;

	sem_t *s;
	s = mmap(NULL, sizeof(sem_t)*5, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANON, -1, 0);
	if (s == MAP_FAILED) 
	{
		perror("fail to mmap");
		exit(1);
	}

	for (i = 0; i < 5; i++)
		sem_init(&s[i], 0, 1);  // 信号量初值制定为1,信号量,变成了互斥锁

	for (i = 0; i < 5; i++)
		if ((pid = fork()) == 0)
			break;

	if (i < 5) 
	{				// 子进程
		int l, r;
		srand(time(NULL));

		if (i == 4) 
			l = 0, r = 4;
		else
			l = i, r = i+1;
		while (1) 
		{
			sem_wait(&s[l]);
			if (sem_trywait(&s[r]) == 0) 
			{
				printf(" %c is eating\n", 'A'+i);
				sem_post(&s[r]);
			}
			sem_post(&s[l]);
			sleep(rand() % 5);
		}
		exit(0);
	} 

	for (i = 0; i < 5; i++)
		wait(NULL);	

	for (i = 0; i < 5; i++)
		sem_destroy(&s[i]);

	munmap(s, sizeof(sem_t)*5);

	return 0;
}

                                                                                                                                                  

  • 4
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值