线程(三) 线程的互斥

news2024/11/15 10:11:56

文章目录

  • 线程
    • 线程的同步和互斥
      • 线程同步
      • 线程互斥
      • 为什么要使用线程互斥
      • 什么是线程同步
        • 示例--线程操作共享资源引发问题
      • 线程互斥--互斥锁
        • 示例--使用互斥锁来保证取款操作
      • 互斥锁的属性
        • 示例--创建不同的属性的互斥锁后进行加锁操作
      • 线程互斥--读写锁
        • 示例--对读写锁进行使用以观察和互斥锁的区别
        • 示例--验证读写锁的特性
        • 示例--使用读写锁对上边的案例ATM进行修改

线程

线程的同步和互斥

线程同步

  • 是一个宏观概念,在微观上包含线程的相互排斥和线程先后执行的约束问题
  • 解决同步方式
    • 条件变量
    • 线程信号量

线程互斥

  • 线程执行的相互排斥
  • 解决互斥方式
    • 互斥锁
    • 读写锁
    • 线程信号量

为什么要使用线程互斥

之前线程所执行的函数里边的变量都属于局部变量(例如前边的argi等局部变量),而线程在执行的过程中都会有自己的栈空间,所以这些局部变量每个线程内部都各自拥有一份,在操作这些局部变量的时候是安全的。但是实际中可能会用到全局变量或者静态变量,全局变量和静态变量位于内存中的数据段,而线程又共享进程的内存空间,所以所有的线程都可以去操作这些资源,这些大家都能操作的资源叫做共享资源。因为所有线程都可以修改共享资源,所以后一个线程看到的是前一个线程修改以后的结果,这就会导致数据的不统一。

举个例子:假如去ATM机去存款,存款的金额是1000元,而此时有一个线程负责将你存款的金额写入到账户中去,在要将数据写入到数据库的时候突然被另一个线程打断,将你的存款金额变成了100元,导致你最终存入卡里的金额为100元,那么这就是当多个线程操作同一个共享资源可能会造成的风险。
其解决的方法也很简单,当你在存款的时候保证存款的这个线程不能被打断,别的线程想要操作这个账户必须被在这个线程结束以后才能进行,只要这个线程没有将存款金额写入到数据库中其他线程的操作都将会被阻塞,这样就能够保证你存款的金额是正确的。这就是线程的互斥,同一时间保证只能有一个线程执行,其他的线程都将会阻塞

什么是线程同步

举个例子:假如现在要做一款产品,在产品发行之前要进行研发和测试两个环节。现在将研发和测试看成两个线程,将产品看作两个线程操作的共享资源。对产品的测试只能是当产品研发完成以后才能进行测试,这里其实就是涉及到了线程的互斥和同步,两个线程同一时间之间只能有一个线程进行操作,而测试又只能基于研发线程操作以后的结果才能进行测是,所以这里其实包含线程先后执行的约束问题。如果这里单单是线程的互斥,那么只能保证一个同一时间内只能有一个线程执行,然后另外一个线程可以继续执行,线程的互斥并不强调线程的先后执行顺序,但是线程的同步建立在线程互斥的基础上还要注重线程先后执行的约束问题,谁先执行,谁后执行,哪个线程依赖哪一个线程执行的结果,在这个结果上继续操作,这是线程同步关心的问题。

示例–线程操作共享资源引发问题

场景:现有一个账户,账户内有10000元,然后分别设有主卡和副卡,然后两个人在同一时间点在ATM机上取钱,然后会出现什么情况?使用程序不加任何操作去操作这个账户,使用代码复刻这个场景。

//account.c

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

//创建账户
Account *create_account(int acc_num, double balance)
{
	Account *a = (Account*)malloc(sizeof(Account));	//在堆上开辟空间,防止函数退出造成栈空间被回收
	assert(a != NULL);		//使用断言判断a不为NULL,否则中止程序

	a->account_number = acc_num;
	a->balance = balance;

	return a;
}

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

	if(amount <= 0 || amount > a->balance)	//若取款金额小于等于0或者大于账户金额就直接返回
	{
		return 0.0;
	}
	double balance = a->balance;	//获取账户余额
	sleep(1);
	balance -= amount;
	a->balance = balance;	//将操作过后的账户余额再赋值给账户余额

	return amount;		//返回操作的金额						
}

//存款
double deposit(Account *a, double amount)
{
	assert(a != NULL);

	if(amount <= 0)		//若存款金额小于等于0就直接返回0
	{
		return 0.0;
	}

	double balance = a->balance;
	sleep(1);
	balance += amount;
	a->balance = balance;		//将存款后的金额再赋值给账户
								
	return amount;
}

//获取账户余额
double get_balance(Account *a)
{
	assert(a != NULL);

	double balance = a->balance;

	return balance;		//将账户余额返回给调用者
}

//销毁账户
void destroy_account(Account *a)
{
	assert(a != NULL);

	free(a);	//将a释放以后a并不会立即变成空指针
	a = NULL;
}
//account.h

#ifndef _ACCOUNT_H
#define _ACCOUNT_H

typedef struct
{
    int account_number;
    double balance;
}Account;

Account *create_account(int acc_num, double balance);
double withdrawal(Account *a, double amount);
double deposit(Account *a, double amount);
double get_balance(Account *a);
void destroy_account(Account *a);

#endif
//account_test.c

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

typedef struct
{
	char name[32];
	Account *a;
	double amount;
}OperArg;

void* withdrawal_func(void *arg)
{
	OperArg *r = (OperArg*)arg;

	double amount = withdrawal(r->a, r->amount);

	//将线程的id,操作者的名字,操作账户,操作金额以及最后从账户里拿到的钱打印出来
	printf("[Thread id:%lx Oper_name:%s Oper_account:%d]: [Oper_amount:%f get_amount:%f]\n",
				pthread_self(),r->name,r->a->account_number,r->amount,amount);

	pthread_exit(NULL);
}

void* deposit_func(void *arg)
{
	OperArg *r = (OperArg*)arg;

	double amount = deposit(r->a, r->amount);

	//将线程的id,操作者的名字,操作账户,操作金额以及最后向账户里存的钱打印出来
	printf("[Thread id:%lx Oper_name:%s Oper_account:%d]: [Oper_amount:%f get_amount:%f]\n",
				pthread_self(),r->name,r->a->account_number,r->amount,amount);

}

int main(void)
{
	int err = -1;
	pthread_t oper1,oper2;
	OperArg arg_oper1,arg_oper2;

	Account *a = create_account(633522411, 10000);

	//给两个操作者赋值,操作的都是同一个用户,而且操作的金额都是10000元,最后通过执行结果看他们能不能都取到10000元
	strcpy(arg_oper1.name, "operator1");
	arg_oper1.a = a;
	arg_oper1.amount = 10000;

	strcpy(arg_oper2.name, "operator2");
	arg_oper2.a = a;
	arg_oper2.amount = 10000;

	if((err = pthread_create(&oper1, NULL, withdrawal_func, (void*)&arg_oper1)) != 0)
	{
		perror("pthread_create error");
		exit(EXIT_FAILURE);
	}

	if((err = pthread_create(&oper2, NULL, withdrawal_func, (void*)&arg_oper2)) != 0)
	{
		perror("pthread_create error");
		exit(EXIT_FAILURE);
	}

	//由于没有设置线程为分离线程,所以需要使用pthread_join函数来回收子线程的资源
	pthread_join(oper1, NULL);
	pthread_join(oper2, NULL);
	
	//将账户的余额打印出来
	printf("the balance of account is %f\n",get_balance(a));

	destroy_account(a);

	return 0;
}

image-20240919175320325

通过编译执行可以发现,两个线程对同一个账户操作都取到了10000元,这显然是有问题的,这其实就是多线程对于共享资源的访问是有风险的,所以要使用互斥锁、读写锁、线程信号量来对其共享资源进行操作,使得同一时间只能够有一个线程操作,其他想要操作这个资源的线程都会被阻塞。
这里说一下出现这个问题的原因,在account.c代码中有一个sleep函数,这个模拟的是当进行存取款操作的时候会存在一定的延时,因为数据库和本地数据进行交换的时候存在延时。因为这个sleep函数导致此现象的发生,详细来说线程的执行顺序是(假如这里是线程1先执行):线程1执行到了double balance = a->balance将账户里的数据取出来了,但此时有一个sleep函数将此线程阻塞挂起轮到另外一个线程执行,另外一个线程也会执行double balance = a->balance,由于此时线程1被阻塞,所以账户里的余额并没有被改变,所以线程1和线程2取到的账户的余额balance都是10000,接下来它们都在各自的栈空间开始运行,所以它们两个线程都能够拿到10000,而账户里边的余额被两个线程刷新了两次。这个解决方法上边已经提到了,当一个线程操作的时候另外一个线程阻塞直到当前这个线程操作完毕以后,回来代码中,当任意一个线程操作完它的余额都是0,所以另外一个线程并不会再次取10000出来,通过这个方法就能解决这个问题。

线程互斥–互斥锁

  • 互斥锁(mutex)是一种简单的加锁的放大来控制对共享资源的访问,在同一时刻只能由一个线程掌握某个互斥锁,拥有上锁状态的线程能够对共享资源进行访问。若其他线程希望上锁一个已经被上了互斥锁的资源,则该线程挂起,直到上锁的线程释放互斥锁为止。

  • 互斥锁的数据类型

    • pthread_mutex_t
  • 互斥锁的创建和销毁

    #include <pthread.h>
    
    int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr);
    
    /*
    功能:对互斥锁进行初始化
    参数1:指向要初始化的互斥锁对象的指针
    参数2:指向互斥锁属性对象的指针,如果传递NULL,则使用默认属性
    mutexattr:互斥锁的属性
    		PTHREAD_MUTEX_INITIALIZER   创建快速互斥锁
    		PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP			创建递归互斥锁
    		PTHREAD_ERRORCHECK_MUTEX_INITIALIZER_NP			创建检错互斥锁
    
    返回值:成功时返回0,失败时返回错误码。
    */
    
    #include <pthread.h>
    
    int pthread_mutex_destroy(pthread_mutex_t *mutex);
    
    /*
    功能:销毁一个已初始化的互斥锁对象,释放其占用的资源
    参数:指向要销毁的互斥锁对象的指针
    
    返回值:成功时返回0,失败时返回错误码。
    */
    
  • 互斥锁上锁和解锁

    #include <pthread.h>
    
    int pthread_mutex_lock(pthread_mutex_t *mutex);
    //功能:上锁,拿不到锁阻塞	当第一个线程已经上锁以后,剩余的线程若想要上锁都会被阻塞,直到第一个线程释放锁
    int pthread_mutex_trylock(pthread_mutex_t *mutex);
    //功能:上锁,拿不到锁返回出错信息	当第一个线程已经上锁以后,剩余的线程若想要上锁都返回出错信息,相当于第一个函数的非阻塞版本
    int pthread_mutex_unlock(pthread_mutex_t *mutex);
    //功能:释放锁
    
    //参数:指向一个已经初始化的互斥锁对象
    //返回值:成功时返回0,失败时返回错误码。
    

    注意:在创建互斥锁的时候,建议互斥锁和共享资源进行绑定,一把互斥锁对应一个共享资源。尽量不设置成全局变量,否则可能会出现一把锁锁很多共享资源,导致并发性能降低。

针对上边的这个场景可以这样子做:无论哪个线程先去执行取款操作,在执行的时候都给它加锁,取款完成后再释放锁,那么当另外一个线程尝试去给一个已经上锁的资源加锁的时候就会被阻塞,直到前边的线程释放锁它才能能够对共享资源进行加锁。那么当线程释放锁的时候就说明它的取款操作已经完成,账户的余额变为0了,那么另外一个线程就不可能取款成功。

示例–使用互斥锁来保证取款操作
//account.c
#include "header.h"
#include "account.h"

Account *create_account(int acc_num, double balance)
{
	Account *a = (Account*)malloc(sizeof(Account));	//在堆上开辟空间,防止函数退出造成栈空间被回收
	assert(a != NULL);		//使用断言判断a不为NULL,否则中止程序

	a->account_number = acc_num;
	a->balance = balance;
	pthread_mutex_init(&a->mutex, NULL);	//对互斥锁进行初始化

	return a;
}

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

	pthread_mutex_lock(&a->mutex);		//对共享资源进行上锁
	if(amount <= 0 || amount > a->balance)	//若取款金额小于等于0或者大于账户金额就直接返回
	{
		pthread_mutex_unlock(&a->mutex);		//释放互斥锁
		return 0.0;
	}
	double balance = a->balance;	//获取账户余额
	sleep(1);
	balance -= amount;
	a->balance = balance;	//将操作过后的账户余额再赋值给账户余额

	pthread_mutex_unlock(&a->mutex);		//释放互斥锁

	return amount;		//返回操作的金额						
}

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

	pthread_mutex_lock(&a->mutex);		//对共享资源进行上锁
	if(amount <= 0)		//若存款金额小于等于0就直接返回0
	{
		pthread_mutex_unlock(&a->mutex);		//释放互斥锁
		return 0.0;
	}

	double balance = a->balance;
	sleep(1);
	balance += amount;
	a->balance = balance;		//将存款后的金额再赋值给账户
								
	pthread_mutex_unlock(&a->mutex);		//释放互斥锁

	return amount;
}

double get_balance(Account *a)
{
	assert(a != NULL);

	pthread_mutex_lock(&a->mutex);		//对共享资源进行上锁
	double balance = a->balance;
	pthread_mutex_unlock(&a->mutex);		//释放互斥锁

	return balance;		//将账户余额返回给调用者
}

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

	pthread_mutex_destroy(&a->mutex);		//销毁互斥锁,注意上下这两句话不能颠倒,要先释放互斥锁再释放空间
	free(a);	//将a释放以后a并不会立即变成空指针
	a = NULL;
}

//account.h

#ifndef _ACCOUNT_H
#define _ACCOUNT_H

typedef struct
{
    int account_number;
    double balance;
    pthread_mutex_t mutex;      //定义互斥锁类型
}Account;

extern Account *create_account(int acc_num, double balance);
extern double withdrawal(Account *a, double amount);
extern double deposit(Account *a, double amount);
extern double get_balance(Account *a);
extern void destroy_account(Account *a);

#endif

image-20240919202645427

通过互斥锁对共享资源进行上锁,当线程去执行函数的时候,不管是哪个线程先执行,都会将共享资源进行上锁,保证取款这一操作不会被其他的线打断导致数据不安全,当一个线程已经对共享资源上锁以后,其他线程想要再获取这个锁就会被阻塞,直到当前线程已经对共享资源操作完毕以后释放锁别的线程才能够拿到锁进行一系列的操作。

互斥锁的属性

在前边有讲过线程的属性,互斥锁的属性和线程的属性类似,通过设置线程的属性能够创建不同属性的线程,例如正常启动的线程和以分离状态启动的线程。那么对于互斥锁来说也有它的属性,通过设置互斥锁的属性就能够创建不同属性的互斥锁,它们的功能也各不相同。如果在创建的时候传入NULL表示创建默认类型的互斥锁和线程。之前使用的线程初始化函数pthread_mutex_init的第二个参数传入的是NULL表示创建默认的互斥锁,但是可以通过一些系统调用来改变互斥锁的属性,然后将参数传进去就可以创建不同于默认互斥锁的类型。

互斥锁属性创建和销毁

#include <pthread.h>

int pthread_mutexattr_init(pthread_mutexattr_t *mutexattr);
//功能:初始化一个互斥锁属性对象
//参数:指向要初始化互斥锁属性的指针
//返回值:成功执行返回0,失败返回错误码
#include <pthread.h>

int pthread_mutexattr_destroy(pthread_mutexattr_t *mutexattr);
//功能:销毁一个已经初始化的互斥锁属性对象
//参数:指向一个要销毁的互斥锁属性的指针
//返回值:成功执行返回0,失败返回错误码

互斥锁属性–进程共享属性

#include <pthread.h>

int pthread_mutexattr_getpshared(pthread_mutexattr_t *restrict attr, int *restrict pshared);
int pthread_mutexattr_setpshared(pthread_mutexattr_t *restrict attr, int pshared);

/*
	功能:getpshared函数用于获取进程的共享属性
				setpshared函数用于设置进程的共享属性(作用和之前的线程分离属性函数类似:pthread_attr_getdetachstate  pthread_attr_setdetachstate)
	  
	参数1:pthread_mutexattr_t *restrict attr	指向要操作的进程共享属性指针
	参数2:int *restrict pshared 指向用来存放进程共享属性的指针
	参数3:int pshared 要设置的进程共享属性
	
	pshared:
					PTHREAD_PROCESS_PRIVATE(默认属性)
					锁只能用于一个进程内部的两个线程进行互斥
					PTHREAD_PROCESS_SHARED
					锁可以用于两个不同进程中的线程进行互斥
					
	互斥锁属性中的这个成员很少用,一般互斥锁都是用在同一个进程之间的不同线程之间的互斥
*/

互斥锁属性–类型

#include <pthread.h>

int pthread_mutexattr_gettype(const pthread_mutexattr_t *attr, int *kind);
int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int kind);

/*
	功能:gettype获取互斥锁属性对象中指定的互斥锁类型
				settype设置互斥锁属性对象中指定的互斥锁类型
				
	参数attr:指向要获取类型的互斥锁属性对象的指针
	参数*kind:指向存储互斥锁属性对象中类型的指针
	参数kind:表示要设置的互斥锁类型
	
	kind:
				标准互斥锁:PTHREAD_MUTEX_NORMAL
					第一次上锁成功,第二次上锁会阻塞(最常用的就是这种)
				递归互斥锁:PTHREAD_MUTEX_RECURSIVE
					第一次上锁成功,第二次以后上锁还是成功,内部会做一个计数
				检错互斥锁:PTHREAD_MUTEX_ERRORCHECK
					第一次上锁成功,第二次上锁会直接出错
				默认互斥锁:PTHREAD_MUTEX_DEFAULT(标准互斥锁就是默认互斥锁)
*/
示例–创建不同的属性的互斥锁后进行加锁操作
#include "header.h"

int main(int argc, char **argv)
{
	if(argc < 2)
	{
		fprintf(stderr,"usage:%s [normal | error | recursive]\n",argv[0]);
		exit(EXIT_FAILURE);
	}

	int err = -1;
	pthread_mutexattr_t mutexattr;
	pthread_mutex_t mutex;

	pthread_mutexattr_init(&mutexattr);		//初始化互斥锁属性
	
	if(!strcmp(argv[1], "normal"))
		pthread_mutexattr_settype(&mutexattr, PTHREAD_MUTEX_NORMAL);		//设置互斥锁属性为标准互斥锁
	else if(!strcmp(argv[1], "error"))
		pthread_mutexattr_settype(&mutexattr, PTHREAD_MUTEX_ERRORCHECK);		//设置互斥锁属性为检错互斥锁
	else if(!strcmp(argv[1], "recursive"))	
		pthread_mutexattr_settype(&mutexattr, PTHREAD_MUTEX_RECURSIVE);			//设置互斥锁属性为递归互斥锁	
	else
		fprintf(stdout, "unknown type\n");

	pthread_mutex_init(&mutex, &mutexattr);		//按上边的互斥锁属性创建互斥锁

	//创建不同的互斥锁后,第二次上锁有不同的结果
	puts("first locked...");	
	if((err = pthread_mutex_lock(&mutex)) != 0)
		fprintf(stderr,"failed to lock shared recource\n");
	else
		fprintf(stdout,"successfully locked\n");
	
	puts("second locked....");
	if((err = pthread_mutex_lock(&mutex)) != 0)
		fprintf(stderr,"failed to lock shared recource\n");
	else
		fprintf(stdout,"successfully locked\n");
	
	pthread_mutex_unlock(&mutex);		//释放互斥锁	
	pthread_mutex_unlock(&mutex);		//释放互斥锁	
	

	pthread_mutexattr_destroy(&mutexattr);		//销毁互斥锁属性
	pthread_mutex_destroy(&mutex);		//销毁互斥锁

	return 0;
}

image-20240920114607846

通过编译执行发现如果设置为默认互斥锁,当第二次进行加锁的时候会直接阻塞,如果设置为检错互斥锁,当第二次进行加锁的时候会直接出错并显示避免资源死锁,如果设置为递归互斥锁,每次加锁都能成功但是内部有一个计数。在不同的场景中可以通过设置合适的互斥锁的属性来对共享资源进行控制。

线程互斥–读写锁

上边介绍了线程互斥的一种方式——互斥锁,现在介绍互斥的第二种方式——读写锁。下边讲一下为什么还要使用读写锁:

在上边的案例中,在存款、取款、获取账户余额的函数功能里分别加入了互斥锁操作实现线程之间的互斥,来保证共享资源的安全。一旦有一个线程进入这三个功能中的任意一个拿到锁并成功加锁,那么其他的线程只能够阻塞等待当前这个线程释放互斥锁,然后根据系统调度来决定哪个线程再拿到互斥锁。这里如果是对账户进行存款、取款的操作还好,因为这是对共享资源进行写入操作,但是实际情况中可能大部分操作都是去查看账户的余额,而这个操作是对共享资源进行读操作,假如现在有50个线程同时对账户余额进行读取,那么如果按照互斥锁的原则,只要有一个线程拿到互斥锁并上锁,其他的线程都得阻塞等待,这其实有点不符合逻辑,因为查询账户余额并没有对账户进行写入操作,所以如果按照这种方式其响应速度是非常慢的,这就有悖于线程提高并发性的原则。如果这里使用互斥锁来做的话还是有弊端的,所以读写锁就应运而生

读写锁和互斥锁相比有以下几个特点:

  • 提高并发性能:读写锁允许多个线程同时获取读锁,这意味着在没有写操作的情况下,多个线程可以并行地执行读操作,从而显著提高了程序的并发性能和响应速度。
  • 避免数据竞争:读写锁确保了再写操作进行时,其他线程不能进行读或写操作,从而避免了数据竞争和不一致性问题。这有助于维护数据的完整性和准确性。
  • 降低锁粒度:在某些场景下,如果使用普通的互斥锁来保护整个共享资源,那么所有的读写操作都会被串行化,造成性能瓶颈。而读写锁可以将共享资源分成读写两个部分,允许多个线程同时读取共享资源,降低了锁的粒度,提高了并发性能。
  • 适用于多读少写的业务场景:在实际应用中,如果存在大量的读操作和少量的写操作,那么使用读写锁的效果会比互斥锁更好。因为在这种情况下,读写锁的优势能够得到最大的发挥。

读写锁的创建和销毁

#include <pthread.h>

int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
           const pthread_rwlockattr_t *restrict attr);

int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

/*
	功能:pthread_rwlock_init 初始化读写锁
				pthread_rwlock_destroy 销毁读写锁
				
	参数:*rwlock	指向要操作的读写锁的指针
				*attr	指向读写锁属性的指针
				
	返回值:成功执行返回0,否则返回错误编码
*/

读写锁加锁和解锁

#include <pthread.h>

int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

/*
	功能:pthread_rwlock_rdlock	加读锁
				pthread_rwlock_wrlock	加写锁
				pthread_rwlock_unlock	释放锁
				
	参数:pthread_rwlock_t *rwlock		指向读写锁的指针
	
	返回值:成功执行返回0,否则返回错误编码
*/
示例–对读写锁进行使用以观察和互斥锁的区别
#include "header.h"

int main(int argc, char **argv)
{
	if(argc < 3)
	{
		fprintf(stderr, "usage: %s [r|w] [r|w]\n",argv[0]);
		exit(EXIT_FAILURE);
	}

	pthread_rwlock_t rwlock;		//定义读写锁类型
	int err = -1;

	pthread_rwlock_init(&rwlock, NULL);		//初始化读写锁

	if(!strcmp(argv[1], "r"))
	{
		if((err = pthread_rwlock_rdlock(&rwlock)) != 0)	
			fprintf(stderr,"first failed to create rdlock:%s\n",strerror(err));
		else
			fprintf(stdout,"first successfully create rwlock...\n");	
	}
	else if(!strcmp(argv[1], "w"))
	{
		if((err = pthread_rwlock_wrlock(&rwlock)) != 0)
			fprintf(stderr,"first failed to create wrlock:%s\n",strerror(err));
		else
			fprintf(stdout,"first successfully create wrlock...\n");
	}

	if(!strcmp(argv[2], "r"))
	{
		if((err = pthread_rwlock_rdlock(&rwlock)) != 0)	
			fprintf(stderr,"second failed to create rdlock:%s\n",strerror(err));
		else
			fprintf(stdout,"second successfully create rwlock...\n");	
	}
	else if(!strcmp(argv[2], "w"))
	{
		if((err = pthread_rwlock_wrlock(&rwlock)) != 0)
			fprintf(stderr,"second failed to create wrlock:%s\n",strerror(err));
		else
			fprintf(stdout,"second successfully create wrlock...\n");
	}

	pthread_rwlock_unlock(&rwlock);		//释放读写锁
	pthread_rwlock_unlock(&rwlock);		//释放读写锁

	pthread_rwlock_destroy(&rwlock);		//销毁读写锁

	return 0;
}

image-20240920211336206

通过编译执行可以发现,读写锁中的读锁每次都能够成功执行,那么放到线程中也就意味着它能够拿到锁也就能够访问到共享资源,然后对共享资源进行读取操作。而写锁由于要当前进行修改完才能够释放锁给另外一个线程获取锁的机会,所以当第二个线程(不管是读锁还是写锁)拿不到锁的时候就会被阻塞或者直接返回错误信息。

示例–验证读写锁的特性
#include "header.h"

typedef struct
{
	int data;
	char buffer[32];
	pthread_rwlock_t rwlock;
}OperArg;

void* read_data(void *arg)
{
	OperArg *r = (OperArg*)arg;
	int i;
	pthread_rwlock_rdlock(&r->rwlock);
	
	for(i=0;i<3;i++)
	{
		printf("[thread id:%lx data = %d]\n",pthread_self(),r->data);
		printf("thread is sleeping....\n");
		sleep(1);
	}
	pthread_rwlock_unlock(&r->rwlock);

	pthread_exit(NULL);
}

void* write_data(void *arg)
{
	OperArg *r = (OperArg*)arg;
	int i = 3;
	pthread_rwlock_wrlock(&r->rwlock);
	strcpy(r->buffer,"hello world");
	printf("%s\n",r->buffer);
	pthread_rwlock_unlock(&r->rwlock);
	
	pthread_exit(NULL);
}

int main(void)
{
	int err = -1;
	pthread_t w1,w2,w3;
	OperArg arg;

	memset(&arg,'\0',sizeof(arg));
	arg.data = 10;

	pthread_rwlock_init(&arg.rwlock, NULL);

	//创建读锁线程用来读取结构体中的数据
	if((err = pthread_create(&w1, NULL, read_data, (void*)&arg)) != 0)
	{
		perror("pthread_create error");
		exit(EXIT_FAILURE);
	}
	if((err = pthread_create(&w3, NULL, read_data, (void*)&arg)) != 0)
	{
		perror("pthread_create error");
		exit(EXIT_FAILURE);
	}

	sleep(1);
	//创建写锁线程向结构体内部写入数据
	if((err = pthread_create(&w2, NULL, write_data, (void*)&arg)) != 0)
	{
		perror("pthread_create error");
		exit(EXIT_FAILURE);
	}
	else
	{
		printf("main control create thread successfully\n");
	}
	
	pthread_join(w1, NULL);
	pthread_join(w2, NULL);
	pthread_join(w3, NULL);

	pthread_rwlock_destroy(&arg.rwlock);

	return 0;
}

image-20240920212926048

通过编译执行可以看到当设置为读锁的线程会交替执行,并不像互斥锁那样执行完一个再去执行另一个,说明它们能够通过对共享资源进行读取,而主线程在创建了写线程后,写线程由于此时没有获取到读写锁所以被阻塞,直到读线程全部操作完释放锁以后写线程才开始执行。可见读写锁应用在读操作较多,写操作较少的场景中能够大大地提高线程运行的并发性。

示例–使用读写锁对上边的案例ATM进行修改
//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_rwlock_init(&a->rwlock, NULL);	//对读写锁进行初始化
		
    return a;
}

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

	pthread_rwlock_wrlock(&a->rwlock);		//加写锁

    if(amount <= 0 || amount > a->balance)
    {
        //printf("输入错误,请重新输入\n");
        pthread_rwlock_unlock(&a->rwlock);		//释放读写锁
		return 0.0;
    }

    double balance = a->balance;
    sleep(1);
    balance -= amount;
    a->balance = balance;

	pthread_rwlock_unlock(&a->rwlock);		//释放读写锁

    return amount;
}

//存款
double deposit(Account *a, double amount)
{
    assert(a != NULL);

	pthread_rwlock_wrlock(&a->rwlock);		//加写锁
    if(amount <= 0)
    {
		pthread_rwlock_unlock(&a->rwlock);		//释放读写锁
        return 0.0;
    }
    double balance = a->balance;
    sleep(1);
    balance += amount;
    a->balance = balance;
	pthread_rwlock_unlock(&a->rwlock);		//释放读写锁

    return amount;
}

//查询余额
double get_balance(Account *a)
{
    assert(a != NULL);

    pthread_rwlock_rdlock(&a->rwlock);		//加读锁
	double balance = a->balance;
    pthread_rwlock_rdlock(&a->rwlock);
    return balance;
}

//销毁账户(释放堆空间)
void destroy_account(Account* a)
{
    assert(a != NULL);

    pthread_rwlock_destroy(&a->rwlock);       //销毁读写锁
    free(a);
    a = NULL;
}

image-20240920213922749

通过编译执行发现执行结果是正确的,可见读写锁也能够实现线程之间的互斥,但是和互斥锁相比更加的灵活。

代码执行过程:如果读锁先拿到锁,那么设置为读锁的所有线程都能够拿到读锁从而实现对共享资源的读取,若设置为写锁的线程想要对共享资源进行修改就会被阻塞,直到所有设置为读锁的线程全部执行完成释放锁才能够被设置为写锁的线程获取。如果写锁先拿到锁,那么后边的所有设置读写锁的线程都会被阻塞,直到当前的线程执行完释放锁。因此读写锁保证了共享资源的一致性并且能够保证当读操作较多,写操作较少时读操作的并发性大大地提高。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2154736.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

鸿蒙【项目打包】- .hap 和 .app;(测试如何安装发的hap包)(应用上架流程)

#打包成.hap需要用到真机 原因是&#xff1a;只有用上了真机才能在项目中配置 自动签名 #步骤: ##第一步:选择真机->选择项目结构->点Sigining Configs(签名配置) ##第二步:勾选Automatically generate signature(自动签名)->点击ok ##第三步:点击构建->点击 …

伊犁云计算22-1 rhel8 dhcp 配置

1 局域网搭建 2 yum 配置 这个参考前面 不说 3 dnf 安装dhcp 好我们废话不说开始安装。理论看书去 进入 dhcp.conf 配置 重启dhcpd 不能报错&#xff01;&#xff01;&#xff01;&#xff01; 我们在客户机上做测试 全局的dhcp关闭 很明显我们的客户机获取到192.16…

Why Is Prompt Tuning for Vision-Language Models Robust to Noisy Labels?

文章汇总 本文的作者针对了提示学习的结构设计进行了分析&#xff0c;发现了一些规律&#xff1a; 1)固定的类名令牌为模型的优化提供了强正则化&#xff0c;减少了由噪声样本引起的梯度。 2)从多样化和通用的web数据中学习到的强大的预训练图像文本嵌入为图像分类提供了强大…

李宏毅机器学习2023-HW11-Domain Adaptation

文章目录 TaskLinkBaselineSimple BaselineMedium BaselineStrong BaselineBoss Baseline Task Domain Adaptation 通过训练真实图片得到分类模型&#xff0c;并将其应用到涂鸦图片上进行分类&#xff0c;来获得更高的精准度。 Link kaggle colab Baseline Simple Baseli…

JVM(HotSpot):JVM简单介绍

文章目录 一、什么是JVM二、优点三、比较四、学习路线 一、什么是JVM 定义&#xff1a;java程序的运行环境 首先&#xff0c;我们要知道&#xff0c;JVM是一套规范&#xff0c;运行java程序的一套规范。 那么&#xff0c;我们学习过java的人都知道&#xff0c;接口规范的实现类…

软考高级:数据库关系模式推理规则 AI 解读

你提出的是关系模式中的一些经典推理规则&#xff0c;这些规则在数据库理论、函数依赖和范式相关的讨论中经常出现。我们可以通过以下方式深入理解这些规则&#xff0c;并且对其中的推理逻辑进行分解。 生活化例子 想象你在管理一家快递公司&#xff0c;货物需要从仓库&#…

24年研赛-华为杯数模竞赛D题论文首发+代码分享

本届研赛助攻题目 C D F三题论文均已经全部完成。后更新计划 如图所示。 免费给大家分享 三个问题的论文部分代码 2024年华为杯-研赛分享资料&#xff08;论文部分代码&#xff09;&#xff08;已更新部分代码&#xff09;&#xff1a; 链接&#xff1a;https://pan.baidu.com…

数据结构哈希(hash)

个人主页&#xff1a;C忠实粉丝 欢迎 点赞&#x1f44d; 收藏✨ 留言✉ 加关注&#x1f493;本文由 C忠实粉丝 原创 数据结构哈希(hash) 收录于专栏 [C进阶学习] 本专栏旨在分享学习C的一点学习笔记&#xff0c;欢迎大家在评论区交流讨论&#x1f48c; 目录 1. 哈希的概念 2.…

Linux:进程(四)

目录 一、进程优先级 二、Linux调度与切换 1.背景 2.进程切换 一、进程优先级 背景&#xff1a;在计算机中&#xff0c;软硬件资源是有限的&#xff0c;而进程想要访问某一种资源&#xff0c;就得通过排队来保证访问资源的过程是有条不紊的。 Linux下对优先级的定义。执行命…

MAE 模型

masked autoencoders (MAE) 论文地址&#xff1a;https://arxiv.org/abs/2111.06377 代码地址&#xff1a;https://github.com/facebookresearch/mae 模型结构图: 思想&#xff1a;自监督学习&#xff08;Self-Supervised Learning&#xff09;&#xff0c;遮住大部分&…

【BEV 视图变换】Fast-Ray(2): 代码复现+画图解释 基于查找表LUT、多视角到单个三维体素转换

paper&#xff1a;Fast-BEV: A Fast and Strong Bird’s-Eye View Perception Baseline code&#xff1a;https://github.com/Sense-GVT/Fast-BEV 致谢: 感谢我司傅同学提供的复现源码 一、完整复现代码(可一键运行)和效果图 Fast-Ray pipeline&#xff1a; 1.创建uv coord se…

PHP项目中Vendor错误导致项目失败的解决方案

在PHP项目中&#xff0c;vendor目录通常用于存放通过Composer安装的依赖库。虽然这些依赖极大地提高了开发效率&#xff0c;但它们也可能成为项目失败的隐患。本文将探讨常见的Vendor错误及其解决方案。 #### 1. 常见的Vendor错误 ##### 1.1 版本不兼容 不同的依赖可能对PHP…

微软AI核电计划

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…

如何养成好习惯

最近一直在玩黑神话悟空&#xff0c;加上有录制实况视频&#xff0c;还是花了快一个月才通关&#xff0c;现在可以回到正常的文章更新节奏了。 上次我们学习了读书笔记-《习惯的力量》&#xff0c;不过那毕竟是国外的、十多年前的书籍了。如今&#xff0c;我们的生活又发生了不…

如何在银河麒麟操作系统中关闭IPv6

如何在银河麒麟操作系统中关闭IPv6 1、临时关闭IPv62、永久关闭IPv6方法一&#xff1a;通过sysctl.conf方法二&#xff1a;通过GRUB配置 1、3、 验证IPv6是否已关闭 &#x1f496;The Begin&#x1f496;点点关注&#xff0c;收藏不迷路&#x1f496; 在当前的网络环境中&#…

MVC、MVP和MVVM三种设计模式之间的区别是什么

区别&#xff1a; mvc表示“模型-视图-控制器”&#xff0c;mvp表示“模型-视图-演示者”&#xff0c;mvvm表示“模型-视图-视图模型”&#xff1b; mvp、mvvm都是由mvc衍生出的。mvc中&#xff0c;view会直接从model中读取数据&#xff1b;mvp中&#xff0c;view并不直接使用m…

如何使用智能代码编辑器改变编程体验

你是否曾经在深夜加班时&#xff0c;望着屏幕上密密麻麻的代码&#xff0c;感到无比疲惫&#xff1f;或者在处理复杂项目时&#xff0c;被繁琐的代码管理和调试过程折磨得头痛不已&#xff1f;如果是这样&#xff0c;那么你可能还没有发现编程世界中的一个秘密武器——智能代码…

《关键跃升》读书笔记10

发展靠规划 执⾏靠闭环&#xff0c;提⾼靠循环&#xff0c;其实讲的是短期和中期的事。短期内完成 任务靠闭环&#xff0c;经理有⽆数需要执⾏的事在⼿边&#xff0c;要靠闭环&#xff0c;不能有漏 洞&#xff0c;不能出现不了了之的情况&#xff1b;中期的团队成⻓靠循环&…

go 读取excel数据存储到mysql

一、安装依赖 go get github.com/go-sql-driver/mysql go get github.com/jmoiron/sqlx 二、main.go package mainimport ("fmt""github.com/jmoiron/sqlx""log" ) import "github.com/tealeg/xlsx" import _ "github.com/go-s…

并查集(上)

并查集简要介绍&#xff1a; 我们先讲并查集的一般使用场景&#xff0c;之后再讲并查集的具体细节以及原理。 并查集的使用一般是如下的场景&#xff1a; 一开始每个元素都拥有自己的集合&#xff0c;在自己的集合里只有这个元素自己。 f i n d ( i ) find(i) find(i)&#…