线程互斥——互斥锁与读写锁

一. 线程同步与互斥概念

    1. 线程同步

  • 是一个宏观概念,在微观上包含线程的相互排斥线程先后执行的约束问题;
  • 解决同步方式:条件变量和线程信号量;
    2. 线程互斥
  • 线程执行的相互排斥;
  • 解决互斥方式:互斥锁、读写锁和线程信号;
    3. 说明
        1)线程的同步与互斥主要是用于解决共享资源的安全性问题;
        2)线程同步与线程互斥是不同的,线程同步是建立在线程互斥的基础上,要考虑线程先后执行的约束问题;
    4. 示例—线程互斥
//account.h

#ifndef __ACCOUNT__H__
#define __ACCOUNT__H__
#include <pthread.h>

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

// 创建账户
extern Account* create_account(int code, double balance);
// 销毁账户
extern void destroy_account(Account *a);
// 取款
extern double withdraw(Account *a, double amt);
// 存款
extern double deposit(Account *a, double amt);
// 查看账户余额
extern double get_balance(Account *a);

#endif

//account.c

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


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

	return a;
}

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

	free(a);
}

// 取款
double withdraw(Account *a, double amt)
{
	assert(a != NULL);
	
	double balance = a->balance;
	if(balance <= 0 || balance < amt)
		return 0;

	sleep(1);//取款是个过程
	balance -= amt;
	
	a->balance = balance;

	return amt;
}

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

	if(amt < 0)
		return 0.0;
	
	double balance = a->balance;
	sleep(1);
	balance += amt;
	a->balance = balance;

	return amt;
}

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

	double balance = a->balance;

	return balance;
}

//account_test.c

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

typedef struct
{
	char name[20];
	Account *account;
	double amt;
}OperArg;

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

//查看余额操作的线程运行函数
void *get_balance_fn(void *arg)
{
	OperArg *oa = (OperArg*)arg;
	double balance = get_balance(oa->account);
	printf("%s(0x%lx) check balance %f from account %d\n",
			oa->name, pthread_self(),
						balance, oa->account->code);
	return (void*)0;
}

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

int main(void)
{
	int err;
	pthread_t boy, girl;
	Account *a = create_account(100001, 10000);

	OperArg o1, o2;// 对同一账户操作的两个用户
	strcpy(o1.name, "boy");
	o1.account = a;
	o1.amt = 10000;

	strcpy(o2.name, "girl");
	o2.account = a;
	o2.amt = 10000;

	// 启动两个子线程(boy和girl)同时去操作同一个银行账户
	// 线程运行函数:取款操作
	if((err = pthread_create(&boy, NULL,
					withdraw_fn, (void*)&o1)) != 0)
	{
		perror("pthread create error");
	}
	if((err = pthread_create(&girl, NULL,
					withdraw_fn, (void*)&o2)) != 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;
}

程序运行结果如下:

说明:上述两个线程执行的操作是以取款操作为例,大家也可以试试其他的操作,每种操作的具体代码都给出了。
由上述结果可以看出,两个线程对同一账户(我们暂且认为是同一个账号的父子账户)进行操作,第一个线程执行取款操作,取款金额为10000,执行完毕后账户余额应该为0,第二个线程将不能取款才对,但是结果是第二个线程同样从账户中取出了10000,显然是不对的,原因就是:这里的账户是两个线程的共享资源,因此二者都 有权对其进行操作,但是对于这样的资源我们没有考虑互斥问题。也就是在一个线程对资源进行操作的时候,我们应该对该资源进行上锁,使其他线程不能操作,等第一个线程结束之后其他线程才能操作该资源。
因此,解决上述问题,我们需要用到互斥锁。

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

    1. 互斥锁的创建和销毁
    有两种方法创建互斥锁,静态方式和动态方式。
    静态方式为:pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER,其中PTHREAD_MUTEX_INITIALIZER是POSIX标准定义的一个用来静态初始化互斥锁的宏,是一个结构常量。
    动态方式是采用pthread_mutex_init()函数来初始化互斥锁。
    
    #include <pthread.h>
    int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutex_attr_t *mutexattr);
    int pthread_mutex_destroy(pthread_mutex_t *mutex);

    返回:成功返回0,否则返回错误编号;
    参数:mutex:互斥锁
                mutexattr:互斥锁创建方式(互斥锁属性)
                        a. PTHREAD_MUTEX_TIMED_NP:这是缺省值,也就是普通锁。当一个线程加锁以后,其余请求锁的线程将形成一                               个等待队列,并在解锁后按优先级获得锁。
                        b. PTHREAD_MUTEX_RECURSIVE_NP:嵌套锁。允许同一个线程对同一个锁成功获得多次,并通过多次unlock解                                 锁。如果是不同线程请求,则在加锁线程解锁时重新竞争。
                        c. PTHREAD_ERRORCHECK_INITIALIZER_NP:检错锁。如果同一个线程请求同一个锁,则返回EDEADLK,否则                               与PTHREAD_MUTEX_TIMED_NP类型动作相同。这样就保证当不允许多次加锁时不会出现最简单情况下的死锁。
                        d. PTHREAD_MUTEX_ADAPTIVE_NP:适应锁,动作最简单的锁类型,仅等待解锁后重新竞争。

    2. 互斥锁的上锁和解锁
    #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,出错返回出错码;

    3. 了解了互斥锁以后,下面我们对account这个案例进行修改,使得同一时刻只能有一个账户对账户资源进行操作。
    分别对上面的三个文件进行修改。
    1)对account.h的修改——仅在Account结构体中增加一个互斥锁变量
//account.h
//...
typedef struct
{
	int code;
	double balance;

	// 定义一把互斥锁,用来对多线程操作的
	// 银行账户(共享资源)进行枷锁(保护)的
	
	/*
	 * 建议互斥锁用来锁定一个账户,和账户绑定在一起
	 * 尽量不设置成全局变量,否则可能出现一把锁
	 * 去锁几百个账户,导致并发性能降低。
	 */
	pthread_mutex_t mutex;
}Account;
//...
    2)对account.c的修改,这里我们仅以取款操作withdraw为例修改——就是在取款之前上锁,在取款之后释放锁。其他操作也是同样的修改。
//account.c
//...
// 创建账户
Account* create_account(int code, double balance)
{
	Account *a = (Account*)malloc(sizeof(Account));
	assert(a != NULL);
	a->code = code;
	a->balance = balance;
	// 对互斥锁进行初始化
	pthread_mutex_init(&a->mutex, NULL);
	
	return a;
}

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

	free(a);
}

// 取款
double withdraw(Account *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;
}
//...
    3)对于account_test.c不作修改。

    结果程序运行结果如下:


    可以看出,当一个线程对Account这个共享资源进行操作时,另一个线程只能等待,直到第一个线程释放锁后,第二个线程才获得锁,对资源进行操作。

三. 互斥锁的属性和类型

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

2. 互斥锁类型操作
#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:互斥锁类型
               1)标准互斥锁:PTHREAD_MUTEX_NORMAL
                     第一次上锁成功,第二次上锁会阻塞;
               2)递归互斥锁:PTHREAD_MUTEX_RECURSIVE
                     第一次上锁成功,第二次上锁还是成功,内部计数;
               3)检测互斥锁:PTHREAD_MUTEX_ERRIRCHECK
                     第一次上锁成功,第二次上锁会出错;
               4)默认互斥锁
                     PTHREAD_MUTEX_DEFAULT(同标准互斥锁)

3. 示例——说明各个函数及参数的使用
//lock_type.c

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

int main(int argc, char *argv[])
{
	if(argc < 2)
	{
		printf("-usage:%s [error|normal|recuesive]\n", argv[0]);
		exit(1);
	}

	pthread_mutex_t mutex;

	// 定义互斥锁属性
	pthread_mutexattr_t mutexattr;
	//初始化互斥锁属性
	pthread_mutexattr_init(&mutexattr);

	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("first lock failure\n");
	else
		printf("first lock success\n");
	
	//第二次上锁
	if(pthread_mutex_lock(&mutex) != 0)
		printf("second lock failure\n");
	else
		printf("second lock success\n");

	pthread_mutex_unlock(&mutex);
	pthread_mutex_unlock(&mutex);

	pthread_mutexattr_destroy(&mutexattr);
	pthread_mutex_destroy(&mutex);

	return 0;
}
如果线程属性的类型是error,则结果如下:

表示第一次上锁成功,第二次上锁失败(不阻塞)。

如果线程属性的类型是normal,则结果如下:

表示第一次上锁成功,第二次上锁阻塞;

如果线程属性的类型是recursive,则结果如下:

表示两次上锁都成功,内部进行计数。

四. 读写锁
引入读写锁是因为互斥锁有其弊端。
比如:在account案例中,如果仅仅是查看账户余额的操作(读),完全可以多个线程同时操作,但是互斥锁在第一个线程操作后就上锁了,后面的线程再执行此操作就会阻塞。

1. 读写锁
  • 线程使用互斥锁缺乏读写并发性;
  • 当读操作比较多,写操作比较少时,可使读写锁提高线程读并发性;
  • 读写锁数据类型  pthread_rwlock_t
#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:读写锁属性

2. 读写锁的加锁和解锁
#include <pthread.h>
int pthread_rwlock_rdlock(pthread_rwlock_t *rdlock);
// 功能:加读锁
int pthread_rwlock_wrlock(pthread_rwlock_t *wrlock);
//功能:加写锁
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
//功能:释放锁
//返回:成功返回0,出错返回错误编号

:在陈硕的《Linux多线程服务器端编程》一书中,他提到:不要用读写锁。下面我来转述一下。
读写锁看上去是个很美的抽象,它明确区分了read和write两种行为。初学者常干的一件事情是,一见到某个共享数据结构频繁读而很少写,就把mutex替换为rwlock。甚至首选rwlock来保护共享状态,这不见得是正确的。
  • 从正确性方面来说,一种典型的容易犯的错误就是在持有read lock的时候修改了共享数据。
  • 从性能方面来说,读写锁不见得比普通mutex更高效。无论如何reader lock加锁的开销不会比mutex lock小,因为它要更新当前reader的数目。如果临界区很小,锁竞争不激烈,那么mutex往往更快。
  • 通常reader lock是可重入的,writer lock是不可重入的。但是为了防止writer饥饿,writer lock通常会阻塞后来的reader lock,因此reader lock在重入的时候可能死锁。另外,在追求低延迟读取的场合也不适合用读写锁。






    

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值