线程(五)线程的同步和互斥——线程信号量

线程

线程的同步和互斥

线程的同步和互斥–线程信号量

上边讲了互斥的方式互斥锁、读写锁、自旋锁,同步的方式条件变量,实现线程之间的同步和互斥还有一种方式–信号量

  • 信号量从本质上是一个非负整数计数器,是共享资源的数目,通常被用来控制对共享资源的访问。

  • 信号量可以实现线程的同步和互斥

  • 通过sem_post()sem_wait()函数对信号量进行加减操作从而解决线程的同步和互斥。

  • 信号量数据类型:sem_t

    案例:现图书馆购入10本Unix环境高级编程供学生借阅,这里的数量10就是共享资源的数量即信号量,如果有10位同学借走了这10本书,那么第11位同学想要再借阅的话就必须等待之前的10位同学中的任意一位将书归还否则它只能等待,这里的借书和还书就对应sem_wait()sem_post()两个函数,通过控制信号量的值就可以实现线程之间的同步和互斥。

信号量的初始化和销毁

#include <semaphore.h>

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

/*
		功能:sem_init	对信号量进行初始化
					sem_destroy	对信号量进行销毁
		参数:sem		指向信号量的指针
					pshared		是否在进程间共享的标志,0为不共享(只在当前进程的多个线程中使用),1为共享(可以在多个进程中的多个线程中使用)
					value	信号量的初始值
		返回值:成功执行返回0,否则返回错误编码	
*/

信号量的加和减操作

#include <semaphore.h>

int sem_post(sem_t *sem);
int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);

/*
	功能:sem_post			增加信号量的值
				sem_wait	 	减少信号量的值
				sem_trywait	 sem_wait()的非阻塞版本
				
	参数:sem			指向信号量的指针
	返回值:成功执行返回0,否则返回错误编码
*/
  • 调用sem_post()一次信号量作加1操作
  • 调用sem_wait()一次信号量作减1操作
  • 当线程调用sem_wait()后,若信号量的值小于0则线程阻塞。只有其他线程在调用sem_post()对信号量作加1操作后并且其值大于或等于0时,阻塞的线程才能运行(也就是说:当此时信号量的值为0时,若调用sem_wait()对信号量作减1操作,那么信号量的值小于0线程阻塞,此时必须等待另外的线程调用sem_post()函数对信号量作加1操作才能够使得被阻塞的线程能够对信号量作减1操作)。
示例–使用线程信号量来控制线程执行的先后顺序
#include "header.h"

sem_t sem1;
sem_t sem2;

void* exec_func1(void *arg)
{
    sem_wait(&sem1);
    printf("[thread id:%lx] [fun:func1] is running\n",pthread_self());

    pthread_exit(NULL);
}

void* exec_func2(void *arg)
{
    sem_wait(&sem2);        
    //对信号量进行减1操作,如果减1后值小于0就阻塞等待其他线程调用sem_post对信号量加1操作
    printf("[thread id:%lx] [fun:func2] is running\n",pthread_self());
    sem_post(&sem1);    //唤醒被阻塞的线程1
    pthread_exit(NULL);
}

void* exec_func3(void *arg)
{
    printf("[thread id:%lx] [fun:func3] is running\n",pthread_self());
    sem_post(&sem2);
    //对信号量的值加1,使得被此信号量阻塞的线程能够运行
    pthread_exit(NULL);
}

int main(void)
{
    int err = -1;
    pthread_t func1, func2, func3;

    //初始化信号量,信号量的值为0,只在此进程中的多个线程中共享
    sem_init(&sem1, 0, 0); 
    sem_init(&sem2, 0, 0);  

    if((err = pthread_create(&func1, NULL, exec_func1, NULL)) != 0)
    {
        perror("pthread_create error");
        exit(EXIT_FAILURE);
    }

    if((err = pthread_create(&func2, NULL, exec_func2, NULL)) != 0)
    {
        perror("pthread_create error");
        exit(EXIT_FAILURE);
    }

    if((err = pthread_create(&func3, NULL, exec_func3, NULL)) != 0)
    {
        perror("pthread_create error");
        exit(EXIT_FAILURE);
    }

    pthread_join(func1, NULL);
    pthread_join(func2, NULL);
    pthread_join(func3, NULL);

    sem_destroy(&sem1);      //销毁信号量
    sem_destroy(&sem2);

    return 0;
}

image-20241009101658153

通过编译执行可以发现使用线程信号量可以用来控制线程执行的先后顺序,代码在刚开始使用sem_init()函数将信号量的值初始化为0,所以一旦线程func1调用sem_wait()函数对信号量进行减1操作就会被阻塞,线程func2也是同样的道理,它们都在等待某一个线程通过调用sem_post()函数对信号量执行加1操作来唤醒被信号量阻塞的线程。通过线程func3调用sem_post()函数来唤醒线程func2,线程func2调用sem_post()函数来唤醒线程func1来控制线程之间的执行顺序。

示例–使用信号量实现线程之间的互斥
//account.c

#include "account.h"
#include "header.h"

Account* create_account(int acc_num, double balance)		//创建账户
{
	Account *a = (Account*)malloc(sizeof(Account));
	assert(a != NULL);

	a->acc_num = acc_num;
	a->balance = balance;
	//pthread_mutex_init(&a->mutex, NULL);
	sem_init(&a->sem, 0, 1);		//初始化线程信号量,信号量值为1

	return a;
}

double withdrawal(Account *a, double amount)
{
	assert(a != NULL);

	//P(1)操作,对信号量作减1操作	
	sem_wait(&a->sem);
//	pthread_mutex_lock(&a->mutex);
	if(amount <= 0 || amount > a->balance)
	{
		//V(1)操作,对信号量作加1操作
		sem_post(&a->sem);
		//pthread_mutex_unlock(&a->mutex);
		return 0.0;
	}

	double balance = a->balance;
	sleep(1);		//模拟ATM机延迟
	balance -= amount;
	a->balance = balance;		//将余额balance取出amount后再存放回a账户                           
	//V(1)操作,对信号量作加1操作
	sem_post(&a->sem);
	//	pthread_mutex_unlock(&a->mutex);

	return amount;
}

double deposit(Account *a, double amount)
{
	assert(a != NULL);
	
	sem_wait(&a->sem);
	//pthread_mutex_lock(&a->mutex);
	if(amount <= 0)
	{
		sem_post(&a->sem);
		//pthread_mutex_unlock(&a->mutex);
		return 0.0;
	}
	double balance = a->balance;
	sleep(1);
	balance += amount;
	a->balance = balance;
	sem_post(&a->sem);
	//pthread_mutex_unlock(&a->mutex);

	return amount;
}

double get_balance(Account *a)
{
	assert(a != NULL);
	sem_wait(&a->sem);
	//pthread_mutex_lock(&a->mutex);
	double balance = a->balance;
	sem_post(&a->sem);
	//pthread_mutex_unlock(&a->mutex);
	return balance;
}

void destroy_account(Account *a)
{
	assert(a != NULL);

	//pthread_mutex_destroy(&a->mutex);
	sem_destroy(&a->sem);		//销毁线程信号量
	free(a);	//将在堆上开辟出来的空间释放
	a = NULL;
}

image-20241009105543329

通过编译执行可以发现通过信号量也能够像之前的互斥锁那样实现线程之间的互斥,在执行对应的函数时对信号量进行减1操作,然后再执行完后对信号量进行加1操作。这样如果中间被别的线程打断执行,那么由于再次执行减1操作后信号量的值小于1,那么对应的线程就会被阻塞,直到前一个线程对信号量执行加1操作后才会继续执行。这样不论哪一个线程先执行,都能够保证同一时间只能有一个线程去操作账户,从而保证了共享资源的安全性。

示例–使用信号量实现线程之间的同步
#include "header.h"

typedef struct
{
	int result;
	sem_t sem;
}OperArg;

void* cal_func(void *arg)
{
	OperArg *s = (OperArg*)arg;
	int i = 1;

	for(; i<= 100; i++)
	{
		s->result += i;
	}
	printf("[cal thread id:%lx] write %d to the structure\n",pthread_self(),s->result);

	sem_post(&s->sem);	//对信号量作加1操作,唤醒被此信号量阻塞的线程
	pthread_exit(NULL);
}

void* get_func(void *arg)
{
	OperArg *s = (OperArg*)arg;

	sem_wait(&s->sem);			//由于将信号量初始化为0,所以当执行此线程时会被阻塞直到另外一个线程调用sem_post将信号量的值加1
	
	printf("[get thread id:%lx] read %d from the structure\n",pthread_self(),s->result);

	pthread_exit(NULL);
}

int main(void)
{
	int err = -1;
	pthread_t cal, get;	
	OperArg arg;

	memset(&arg, 0, sizeof(arg));		//初始化结构体
	sem_init(&arg.sem, 0, 0);			//初始化信号量,信号量的值为0只用于当前进程的若干线程

	if((err = pthread_create(&cal, NULL, cal_func, (void*)&arg)) != 0)
	{
		perror("pthread_create error");
		exit(EXIT_FAILURE);
	}

	if((err = pthread_create(&get, NULL, get_func, (void*)&arg)) != 0)
	{
		perror("pthread_create error");
		exit(EXIT_FAILURE);
	}

	pthread_join(cal, NULL);
	pthread_join(get, NULL);

	sem_destroy(&arg.sem);		//销毁信号量

	return 0;
}

image-20241009114122777

通过编译执行可以发现使用信号量可以用来实现线程之间的同步。在代码中要cal_func线程先计算出结果存放到result中才轮到get_func线程获取result的值。具体的做法就是阻塞get_func线程直到cal_func线程将结果计算出来,也就是说刚开始的信号量的值为0,那么当get_func线程调用sem_wait()去对信号量作减1操作的时候就会被阻塞直到cal_func线程调用sem_post()函数对信号量作加1操作才会执行,那么此时也就意味着已经将结果计算出来并放入到结构体中可以由get_func线程获取了,由此实现了线程之间的同步。

死锁

死锁的产生原因:

  1. 资源竞争
    • 多个线程需要竞争有限的资源(如锁、信号量、内存等),并且它们以不同的顺序请求这些资源,可能导致死锁,例如:线程A持有资源1并请求资源2,而线程B持有资源2并请求资源1,形成循环等待
  2. 持有并等待
    • 一个线程在已持有某个资源的同时请求其他的资源,这种情况很容易导致死锁,因为其他线程可能会占用这些请求的资源,形成等待状态。例如:线程A已经获得了资源X,并请求资源Y,而线程B已经获得了资源Y,并请求资源X。
  3. 不可剥夺性
    • 线程持有的资源不能被强制剥夺,只有当线程释放它们时,其他线程才能获取这些资源。这样的情况下,如果多个线程相互等待彼此持有的资源,就可能导致死锁。
  4. 循环等待
    • 一组线程形成一个等待环,其中每个线程都在等待下一个线程所持有的资源。这种条件是死锁的必要条件。例如:线程1等待线程2持有的资源,线程2等待线程3持有的资源,而线程3又在等待线程1持有的资源。

示例–死锁的产生

#include "header.h"

typedef struct
{
	int value;
	pthread_mutex_t mutex;
}ResourceA;

typedef struct
{
	int value;
	pthread_mutex_t mutex;
}ResourceB;

typedef struct
{
	ResourceA *a;
	ResourceB *b;
}Resource;

void* exec_func1(void *arg)
{
	Resource *s = (Resource*)arg;

	//对共享资源A进行上锁
	pthread_mutex_lock(&s->a->mutex);
	sleep(1);
	printf("func1 thread id:%lx waiting for resourceB....\n",pthread_self());
	//对共享资源B进行上锁
	pthread_mutex_lock(&s->b->mutex);	
	printf("ResourceA value = %d\n",s->a->value);
	printf("ResourceB value = %d\n",s->b->value);
	pthread_mutex_unlock(&s->b->mutex);
	pthread_mutex_unlock(&s->a->mutex);

	pthread_exit(NULL);
}

void* exec_func2(void *arg)
{
	Resource *s = (Resource*)arg;

	//对共享资源B进行上锁
	pthread_mutex_lock(&s->b->mutex);
	sleep(1);
	printf("func2 thread id:%lx waiting for resourceA....\n",pthread_self());
	//对共享资源A进行上锁
	pthread_mutex_lock(&s->a->mutex);

	printf("ResourceA value = %d\n",s->a->value);
	printf("ResourceB value = %d\n",s->b->value);

	pthread_mutex_unlock(&s->a->mutex);
	pthread_mutex_unlock(&s->b->mutex);
	pthread_exit(NULL);
}

int main(void)
{
	int err = -1;
	pthread_t func1, func2;
	ResourceA a;
	ResourceB b;
	Resource arg = {&a, &b};
	
	a.value = 100;
	b.value = 200;
	pthread_mutex_init(&a.mutex, NULL);
	pthread_mutex_init(&b.mutex, NULL);

	if((err = pthread_create(&func1, NULL, exec_func1, (void*)&arg)) != 0)
	{
		perror("pthread_create error");
		exit(EXIT_FAILURE);
	}

	if((err = pthread_create(&func2, NULL, exec_func2, (void*)&arg)) != 0)
	{
		perror("pthread_create error");
		exit(EXIT_FAILURE);
	}

	pthread_join(func1, NULL);
	pthread_join(func2, NULL);

	pthread_mutex_destroy(&a.mutex);
	pthread_mutex_destroy(&b.mutex);

	return 0;
}

image-20241009171651310

通过编译执行可以发现线程陷入了死循环,分析其代码造成死循环的主要原因是:线程func1对资源A进行上锁后延时1s,此时会轮到线程func2执行,然后线程func2会对资源B进行上锁然后延时1s,当再次轮到线程func1运行的时候,线程func1尝试去获取资源B的锁,但是资源B的锁已经被func2所获取,所以此时线程func1就会被阻塞,而线程func2同理想要获取资源A的锁也会被阻塞,所以两个线程在已经持有一个锁的情况下想要获取对方的锁就造成了死锁。

解决死锁的办法:

  1. 按相同的次序锁定相应的共享资源
  2. 使用函数pthread_mutex_trylock(),当它首次上锁的时候,如果该互斥锁没有被别的线程锁定,则调用成功,锁会被获取。如果再次调用的时候,如果该互斥锁已经被其他的线程锁定,pthread_mutex_trylock()会立即返回,并不会阻塞。此时,函数返回的错误码是EBUSY,表示互斥锁正在被使用,当前线程无法获取互斥锁。

示例–使用相同次序锁定解决死锁

#include "header.h"

typedef struct
{
	int value;
	pthread_mutex_t mutex;
}ResourceA;

typedef struct
{
	int value;
	pthread_mutex_t mutex;
}ResourceB;

typedef struct
{
	ResourceA *a;
	ResourceB *b;
}Resource;

void* exec_func1(void *arg)
{
	Resource *s = (Resource*)arg;

	//对共享资源A进行上锁
	pthread_mutex_lock(&s->a->mutex);
	sleep(1);
	printf("func1 thread id:%lx waiting for resourceB....\n",pthread_self());
	//对共享资源B进行上锁
	pthread_mutex_lock(&s->b->mutex);	
	printf("ResourceA value = %d\n",s->a->value);
	printf("ResourceB value = %d\n",s->b->value);
	pthread_mutex_unlock(&s->b->mutex);
	pthread_mutex_unlock(&s->a->mutex);

	pthread_exit(NULL);
}

void* exec_func2(void *arg)
{
	Resource *s = (Resource*)arg;

	pthread_mutex_lock(&s->a->mutex);
	sleep(1);
	printf("func2 thread id:%lx waiting for ResourceB\n",pthread_self());
	pthread_mutex_lock(&s->b->mutex);

	printf("ResourceA value = %d\n",s->a->value);
	printf("ResourceB value = %d\n",s->b->value);

	pthread_mutex_unlock(&s->b->mutex);
	pthread_mutex_unlock(&s->a->mutex);

/*
	//对共享资源B进行上锁
	pthread_mutex_lock(&s->b->mutex);
	sleep(1);
	printf("func2 thread id:%lx waiting for resourceA....\n",pthread_self());
	//对共享资源A进行上锁
	pthread_mutex_lock(&s->a->mutex);

	printf("ResourceA value = %d\n",s->a->value);
	printf("ResourceB value = %d\n",s->b->value);

	pthread_mutex_unlock(&s->a->mutex);
	pthread_mutex_unlock(&s->b->mutex);
*/
	pthread_exit(NULL);
}

int main(void)
{
	int err = -1;
	pthread_t func1, func2;
	ResourceA a;
	ResourceB b;
	Resource arg = {&a, &b};
	
	a.value = 100;
	b.value = 200;
	pthread_mutex_init(&a.mutex, NULL);
	pthread_mutex_init(&b.mutex, NULL);

	if((err = pthread_create(&func1, NULL, exec_func1, (void*)&arg)) != 0)
	{
		perror("pthread_create error");
		exit(EXIT_FAILURE);
	}

	if((err = pthread_create(&func2, NULL, exec_func2, (void*)&arg)) != 0)
	{
		perror("pthread_create error");
		exit(EXIT_FAILURE);
	}

	pthread_join(func1, NULL);
	pthread_join(func2, NULL);

	pthread_mutex_destroy(&a.mutex);
	pthread_mutex_destroy(&b.mutex);

	return 0;
}

image-20241009173516117

通过编译执行可以看出通过修改锁定的次序后线程没有再次进入到死锁状态了。分析代码:当线程func1开始执行的时候,先对资源A进行上锁,然后进入到睡眠状态轮到线程func2执行,此时线程func2想要获取互斥锁,但是互斥锁已经被线程func1获取并锁定,所以线程func2没有获取到互斥锁进入到阻塞状态,直到线程func1执行完释放锁以后线程func2才能够获取锁并执行。经过这个操作以后就能够实现避免死锁现象,并且保证线程的安全性。

线程状态转换

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

日落星野

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值