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

来源:互联网 发布:unity3d gui 编辑:程序博客网 时间:2024/05/16 14:33

一. 线程同步与互斥概念

    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");elseprintf("first lock success\n");//第二次上锁if(pthread_mutex_lock(&mutex) != 0)printf("second lock failure\n");elseprintf("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在重入的时候可能死锁。另外,在追求低延迟读取的场合也不适合用读写锁。






    

2 0
原创粉丝点击