自学内容网 自学内容网

线程(三) 线程的互斥

线程

线程的同步和互斥

线程同步

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

线程互斥

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

为什么要使用线程互斥

之前线程所执行的函数里边的变量都属于局部变量(例如前边的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

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

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


原文地址:https://blog.csdn.net/m0_52867657/article/details/142421334

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!