一. 线程同步与互斥概念
1. 线程同步
- 是一个宏观概念,在微观上包含线程的相互排斥和线程先后执行的约束问题;
- 解决同步方式:条件变量和线程信号量;
- 线程执行的相互排斥;
- 解决互斥方式:互斥锁、读写锁和线程信号;
//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.h2)对account.c的修改,这里我们仅以取款操作withdraw为例修改——就是在取款之前上锁,在取款之后释放锁。其他操作也是同样的修改。
//...
typedef struct
{
int code;
double balance;
// 定义一把互斥锁,用来对多线程操作的
// 银行账户(共享资源)进行枷锁(保护)的
/*
* 建议互斥锁用来锁定一个账户,和账户绑定在一起
* 尽量不设置成全局变量,否则可能出现一把锁
* 去锁几百个账户,导致并发性能降低。
*/
pthread_mutex_t mutex;
}Account;
//...
//account.c3)对于account_test.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;
}
//...
结果程序运行结果如下:
可以看出,当一个线程对Account这个共享资源进行操作时,另一个线程只能等待,直到第一个线程释放锁后,第二个线程才获得锁,对资源进行操作。
三. 互斥锁的属性和类型
1. 互斥锁进程共享属性操作
#include <pthreda.h>返回:成功返回0,出错返回错误编号; 参数:attr:互斥锁属性; pshared:进程共享属性 1)PTHREAD_PROCESS_PRIVATE(默认情况)——锁只能用于一个进程内部的两个线程进行互斥; 2)PTHREAD_PROCESS_SHARED——锁可以用于两个不同进程中的线程进行互斥;
int pthread_mutexattr_getpshared(const pthread_mutexattr_t *restrict attr, int *restrict pshared);
int pthread_mutexattr_setpshared(pthread_mutex_t *attr, int pshared);
2. 互斥锁类型操作
#include <pthread.h>返回:成功返回0,出错返回错误编号; 参数:attr:互斥锁属性; type:互斥锁类型 1)标准互斥锁:PTHREAD_MUTEX_NORMAL 第一次上锁成功,第二次上锁会阻塞; 2)递归互斥锁:PTHREAD_MUTEX_RECURSIVE 第一次上锁成功,第二次上锁还是成功,内部计数; 3)检测互斥锁:PTHREAD_MUTEX_ERRIRCHECK 第一次上锁成功,第二次上锁会出错; 4)默认互斥锁 PTHREAD_MUTEX_DEFAULT(同标准互斥锁)
int pthread_mutexattr_gettype(const pthread_mutexattr_t *restrict attr, int *restrict type);
int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int type);
3. 示例——说明各个函数及参数的使用
//lock_type.c如果线程属性的类型是error,则结果如下:
#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;
}
表示第一次上锁成功,第二次上锁失败(不阻塞)。
如果线程属性的类型是normal,则结果如下:
表示第一次上锁成功,第二次上锁阻塞;
如果线程属性的类型是recursive,则结果如下:
表示两次上锁都成功,内部进行计数。
四. 读写锁 引入读写锁是因为互斥锁有其弊端。 比如:在account案例中,如果仅仅是查看账户余额的操作(读),完全可以多个线程同时操作,但是互斥锁在第一个线程操作后就上锁了,后面的线程再执行此操作就会阻塞。
1. 读写锁
- 线程使用互斥锁缺乏读写并发性;
- 当读操作比较多,写操作比较少时,可使读写锁提高线程读并发性;
- 读写锁数据类型 pthread_rwlock_t
#include <pthread.h>返回:成功返回0,出错返回错误编号;参数:rwlock:读写锁 attr:读写锁属性
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
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在重入的时候可能死锁。另外,在追求低延迟读取的场合也不适合用读写锁。