Linux学习_线程的互斥

Posted Leslie X徐

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux学习_线程的互斥相关的知识,希望对你有一定的参考价值。

线程的同步和互斥

同步和互斥的概念

  1. 线程同步
  • 线程同步是一个宏观的概念,在微观上包含线程的相互排斥(注意和线程互斥的不一样)和线程先后执行的约束问题
  • 解决同步方式
    • 条件变量
    • 线程信号量
  1. 线程互斥
  • 线程执行是相互排斥的,一个线程在操作共享资源时,其他线程是处于阻塞状态或等待状态,不可以操作共享资源。
  • 和线程同步相互排斥的区别是:线程互斥不考虑线程先后执行的约束问题。
  • 解决互斥方式
    • 互斥锁
    • 读写锁
    • 线程信号量

保证同一时间操作共享资源的线程只有一个。

  1. ATM线程冲突示例
    创建boy和girl线程同时对银行账户进行操作
    主函数:
typedef struct
{
	int code;
	double balance;
}Account,*pAccount;

//取款金额amt
extern double withdraw(pAccount a, double amt)
{
	if(amt<0 || amt> a->balance) return 0.0;
	double balance = a->balance;
	sleep(1);//模拟银行连接互联网取钱的延时
	balance -= amt;
	a->balance = balance;
	return amt; //成功则返回取款金额
}

//------------------------------------------------------------------------
/*
 * account_test.c
 * 创建boy和girl线程同时对银行账户进行操作
 */
//头文件
//--------------------------------
#include "account.h"
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <string.h>
//--------------------------------

//结构体
//--------------------------------
typedef struct
{
	char name[20];
	pAccount account;
	double amt; //存取款金额
}OperArg;
//--------------------------------

//定义取款操作的线程运行函数
//--------------------------------
void* withdraw_fn(void* arg)
{
	OperArg *oa = (OperArg*)arg;
	double amt = withdraw(oa->account, oa->amt);
	
	printf("%8s(0x%lx) withdraw %f from account %d\\n",\\
	oa->name, pthread_self(), amt, oa->account->code);
	
	return (void*)0;
}
//--------------------------------

//定义存款操作的线程运行函数
//--------------------------------
void* deposit_fn(void* arg)
{
	OperArg *oa = (OperArg*)arg;
	double amt = deposit(oa->account, oa->amt);
	
	printf("%8s(0x%lx) deposit %f from account %d\\n",\\
	oa->name, pthread_self(), amt, oa->account->code);
	
	return (void*)0;
}
//--------------------------------

//定义检查银行账户的线程运行函数
//--------------------------------
void* check_fn(void* arg)
{
	return (void*)0;
}
//--------------------------------

//--------------------------------

//主函数
//--------------------------------
int main(void)
{
	int err;
	pthread_t boy, girl;
	pAccount a = create_account(100001, 10000);
	OperArg o1, o2; //o1,o2分别代表操作的两个用户
	
	strcpy(o1.name, "boy");
	o1.account = a;
	o1.amt = 10000;
	
	strcpy(o2.name, "girl");
	o2.account = a;
	o2.amt = 10000;
	
	//启动两个子线程(boy和girl)
	//同时操作同一个银行账户
	err = pthread_create(&boy, NULL,withdraw_fn, (void*)&o1);
	if(err != 0) perror("pthread create error");
	err = pthread_create(&girl, NULL,withdraw_fn, (void*)&o2);
	if(err != 0) perror("pthread create error");
	
	pthread_join(boy, NULL);
	pthread_join(girl, NULL);
	
	printf("account balance: %f\\n", get_balance(a));
	destroy_account(a);
	
	return 0;
}


输出:

girl(0xb657e460) withdraw 10000.000000 from account 100001
boy(0xb6d7f460) withdraw 10000.000000 from account 100001
account balance: 0.000000

男生女生同时取款,都取到了10000元,相当于两人一共取了20000,这是不对的。
此时线程不安全。
分析:boy girl 两者共同调用了withdraw函数,在栈中都有一个空间,girl在boy延时睡眠时进来,此时账户里余额并未改变,于是两人得到的balance都为10000。

线程互斥——互斥锁(互斥量)

  1. 概念:
  • 互斥锁(mutex)是一种简单的加锁的方法来控制对共享资源的访问。在同一时刻只能有一个线程掌握某个互斥锁,拥有上锁状态的线程能够对共享资源进行访问。若其他线程希望上锁一个已经被上了互斥锁的资源,则线程挂起,直到上锁的线程释放互斥锁为止。
  1. 互斥锁数据类型
  • pthread_mutex_t
  1. 互斥锁的创建和销毁
#include <pthread.h>
int pthread_mutex_init(
								pthread_mutex_t* restrict mutex,
								const pthread_mutexattr_t* mutexattr);

int pthread_mutex_destroy(pthread_mutex_t* mutex);
  • 返回:成功返回0,否则返回错误编号。
  • 参数:
    • mutex:互斥锁
    • mutexattr:互斥锁创建方式(一般写NULL,默认互斥锁类型)
      • PTHREAD_MUTEX_INITIALIZER:创建快速互斥锁
      • PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP:创建递归互斥锁
      • PTHREAD_ERRORCHECK_MUTEX_INITIALIZER_NP:创建检错互斥锁
  1. 互斥锁上锁和解锁
#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,否则返回错误编号。

对ATM示例进行加锁

/*account.c
*/
typedef struct
{
	int code;
	double balance;
	//定义一把互斥锁,用来对多个线程操作的银行账户(共享资源)进行加锁(保护)
	//建议一把互斥锁绑定一个账户,尽量不设置成全局变量,否则可能出现一把锁去锁几百个银行账户导致并发性降低
	pthread_mutex_t mutex;
}Account,*pAccount;

#include "account.h"
#include <malloc.h>
#include <assert.h>
#include <string.h>
#include <unistd.h>


//创建账户
extern pAccount create_account(int code, double balance)
{
	pAccount a = (pAccount)malloc(sizeof(Account));
	assert(a != NULL);
	a->code = code;
	a->balance = balance;
	//对互斥锁进行初始化
	pthread_mutex_init(&a->mutex, NULL);
	return a;
}

//销毁账户
extern void destroy_account(pAccount a)
{
	assert(a != NULL);
	//销毁互斥锁
	pthread_mutex_destroy(&a->mutex);
	free(a);
}

//取款金额amt
extern double withdraw(pAccount a, double amt)
{
	assert(a != NULL);
	//以下对共享资源从上锁到释放的代码区称为
	//对共享资源操作的临界区,犹如一个禁区
	
	//对共享资源(账户)加锁
	pthread_mutex_lock(&a->mutex);
	
	if(amt<0 || amt> a->balance) 
	{
		//释放互斥锁
		pthread_mutex_unlock(&a->mutex);
		return 0.0;
	}
	double balance = a->balance;
	sleep(1);//模拟银行连接互联网取钱的延时
	balance -= amt;
	a->balance = balance;
	
	//释放互斥锁
	pthread_mutex_unlock(&a->mutex);
	
	return amt; //成功则返回取款金额
}

//存款
extern double deposit(pAccount a, double amt)
{
	assert(a != NULL);
	
	//对共享资源(账户)加锁
	pthread_mutex_lock(&a->mutex);
	
	if(amt<0){
		//释放互斥锁
		pthread_mutex_unlock(&a->mutex);
		return 0.0;
	}
	double balance = a->balance;
	sleep(1);
	balance += amt;
	a->balance = balance;
	
	//释放互斥锁
	pthread_mutex_unlock(&a->mutex);
	return amt;
}

//查看账户余额
extern double get_balance(pAccount a)
{
	assert(a != NULL);
	//对共享资源(账户)加锁
	pthread_mutex_lock(&a->mutex);
	double balance = a->balance;
	//释放互斥锁
	pthread_mutex_unlock(&a->mutex);
	return balance;
}

给每个线程加上锁,再运行主函数
输出:

 boy(0xb6dc5460) withdraw 10000.000000 from account 100001
 girl(0xb65c4460) withdraw 0.000000 from account 100001
account balance: 0.000000

  1. 互斥锁属性创建和销毁
#include <pthread.h>
int pthread_mutexattr_init(pthread_mutexattr_t* attr);
int pthread_mutexattr_destroy(pthread_mutexattr_t* attr);
  • 返回:成功返回0,否则返回错误编号
  • 参数:attr:互斥锁属性
  1. 互斥锁进程共享属性操作
#include <pthread.h>

int pthread_mutexattr_getpshared(
										const pthread_mutexattr_t* restrict attr,
										int* restrict pshared);
										
int pthread_mutexattr_setpshared(
										pthread_mutexattr_t* attr,
										int pshared);
  • 返回:成功返回0,否则返回错误编号
  • 参数:
    • attr:互斥锁属性
    • pshared:进程共享属性
      • PTHREAD_PROCESS_PRIVATE(默认情况)
        • 锁只能用于一个进程内部的多个线程进行互斥
      • PTHREAD_PROCESS_SHARED
        • 锁可以用于两个不同进程中的线程进行互斥
  1. 互斥锁类型操作
#include <pthread.h>

int pthread_mutexattr_gettype(
										const pthread_mutexattr_t* restrict attr,
										int* restrict type);
										
int pthread_mutexattr_settype(
										pthread_mutexattr_t* attr,
										int type);
  • 返回:成功返回0,否则返回错误编号
  • 参数:
    • attr:互斥锁属性
    • type:互斥锁类型
      • 标准互斥锁:PTHREAD_MUTEX_NORMAL
        • 第一次上锁成功,第二次上锁会阻塞。
      • 递归互斥锁:PTHREAD_MUTEX_RECURSIVE
        • 第一次上锁成功,第二次以后上锁还是成功,内部计数。
      • 检错互斥锁:PTHREAD_MUTEX_ERRORCHECK
        • 第一次上锁成功,第二次上锁会出错。
      • 默认互斥锁:PTHREAD_MUTEX_DEFAULT(同标准互斥锁)

示例

/*
 * lock_type.c
 * 互斥锁类型
 */

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

int main(int argc, char **argv)
{
	pthread_mutex_t mutex;
	
	if(argc<2){
		printf("-usage:%s [error|normal|recursive]\\n",argv[0]);exit(1);
	}
	
	//定义互斥锁属性
	pthread_mutexattr_t mutexattr;
	//初始化互斥锁属性
	pthread_mutexattr_init(&mutexattr);
	//check argv传入的互斥锁类型
	if(!strcmp(argv[1],"error")){
		//设置互斥锁类型
		pthread_mutexattr_settype(&mutexattr,\\
		PTHREAD_MUTEX_ERRORCHECK);
		}
	else if(!strcmp(argv[1],"normal")){
		pthread_mutexattr_settype(&mutexattr,\\
		PTHREAD_MUTEX_NORMAL);
		}
	else if(!strcmp(argv[1],"recursive")){
		pthread_mutexattr_settype(&mutexattr,\\
		PTHREAD_MUTEX_RECURSIVE);
		}
	
	//按指定类型初始化互斥锁
	pthread_mutex_init(&mutex,&mutexattr);
	//上锁
	if(pthread_mutex_lock(&mutex)!=0)
	printf("lock failure\\n");
	else
	printf("lock success\\n");
	//第二次上锁
	if(pthread_mutex_lock(&mutex)!=0)
	printf("lock failure\\n");
	else
	printf("lock success\\n");
	//上两次锁,也需要释放两次
	pthread_mutex_unlock(&mutex);
	pthread_mutex_unlock(&mutex);
	
	//销毁互斥锁和属性
	pthread_mutexattr_destroy(&mutexattr);
	pthread_mutex_destroy(&mutex);
	
	return 0;
}


输出:

$ ./lock_type error
lock success
lock failure
$ ./lock_type recursive
lock success
lock success
$ ./lock_type normal
lock success

^C
$

线程互斥——读写锁

  1. 概念
  • 线程使用互斥锁缺乏读并发性。
  • 当读操作较多,写操作较少时,可使用读写锁提高线程读并发性。
  • 读写锁数据类型
    • pthread_rwlock_t
  1. 读写锁创建和销毁
#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);

  • 返回:成功返回0,否则返回错误编号
  • 参数:
    • rwlock:读写锁
    • attr:读写锁属性
  1. 读写锁加锁和解锁
#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);

  • 返回:成功返回0,否则返回错误编号
  • 参数:
    • rwlock:读写锁
  1. 示例:
/*
 * rwlock_feature.c
 * 
 */

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

//定义读写锁
pthread_rwlock_t rwlock;

int main(int argc, char **argv)
{
	if(argc<3){
		printf("-usage:%s [r|w] [r|w]\\n",argv[0]);
		exit(1);
	}
	//读写初始化
	pthread_rwlock_init(&rwlock,NULL);
	
	if(!strcmp("r",argv[1])){
		//加读锁
		if(pthread_rwlock_rdlock(&rwlock) != 0)
		{printf("first read lock failure\\n");}
		else
		{printf("first read lock success\\n");}
	}
	
	if(!strcmp("w",argv[1])){
		//加写锁
		if(pthread_rwlock_wrlock(&rwlock) != 0)
		{printf("first write lock failure\\n");}
		else
		{printf("first write lock success\\n");}
	}
	
	if(!strcmp("r",argv[2])){
		//加读锁
		if(pthread_rwlock_rdlock(&rwlock) != 0)
		{printf("second read lock failure\\n");}
		else
		{printf("second read lock success\\n");}
	}
	
	if(!strcmp("w",argv[2])){
		//加写锁
		if(pthread_rwlock_wrlock(&rwlock) != 0)
		{printf("second write lock failure\\n");}
		else
		{printf("second write lock success\\n");}
	}
	
	//释放读写锁
	pthread_rwlock_unlock(&rwlock);
	pthread_rwlock_unlock(&rwlock);
	
	return 0;
}


输出:

$ ./rwlock_feature r r
first read lock success
second read lock success

$ ./rwlock_feature r w
first read lock success
^C

$ ./rwlock_feature w w
first write lock success
second write lock failure

$ ./rwlock_feature w r
first write lock success
second read lock failure

分析:

  • 读和读:不排斥
  • 读和写\\写和读:排斥(后一个线程阻塞或失败)
  • 写和写:排斥

以上是关于Linux学习_线程的互斥的主要内容,如果未能解决你的问题,请参考以下文章

Linux学习_线程信号量

Linux___线程互斥与同步

Linux多线程_(进程与线程,线程的生命周期认识线程,线程互斥)

02_线程,互斥锁的基础用法

Linux:详解多线程(线程安全互斥和死锁)

互斥锁 & 共享锁