• Linux系统编程系列之互斥锁和读写锁


     Linux系统编程系列(16篇管饱,吃货都投降了!)

            1、Linux系统编程系列之进程基础

            2、Linux系统编程系列之进程间通信(IPC)-信号

            3、Linux系统编程系列之进程间通信(IPC)-管道

            4、Linux系统编程系列之进程间通信-IPC对象

            5、Linux系统编程系列之进程间通信-消息队列

            6、Linux系统编程系列之进程间通信-共享内存

            7、Linux系统编程系列之进程间通信-信号量组

            8、Linux系统编程系列之守护进程

            9、Linux系统编程系列之线程

            10、Linux系统编程系列之线程属性 

            11、Linux系统编程系列之互斥锁和读写锁

            12、Linux系统编程系列之线程的信号处理

            13、Linux系统编程系列之POSIX信号量

            14、Linux系统编程系列之条件变量

            15、Linux系统编程系列之死锁

            16、 Linux系统编程系列之线程池

    一、什么是互斥锁和读写锁

            互斥锁是一种并发机制,用于控制多个线程对共享资源的访问。

            读写锁是一种并发机制,用于控制多个线程对共享资源的访问。

    二、特性

            1、互斥锁

            当一个线程获得了互斥锁并进入临界区(对共享资源进行访问)时,其他线程将被阻塞,直到该线程释放互斥锁。这可以确保同时只有一个线程能够访问共享资源,避免多个线程同时修改共享资源导致数据不一致或其他问题。

            2、读写锁

            读写锁允许多个线程同时读取共享资源,但是只允许一个线程进行写操作。在读取共享资源时,多个线程可以同时获得读锁,不会相互阻塞,从而提高了并发性能。而在写操作时,只有一个线程可以获得写锁,其他线程将被阻塞,以避免同时修改导致数据不一致或其他问题。        

            使用读写锁可以有效地提高系统的并发性能和吞吐量,访问效率比互斥锁高。

    三、使用场景

            1、互斥锁

                    (1)、 线程共享同一个全局变量或者静态变量时,需要使用互斥锁来保证数据的一致性和正确性。
                    (2)、 多个线程访问共享资源时,需要使用互斥锁来保证同一时间只有一个线程能够访问共享资源。
                    (3)、 线程需要保证一段代码的原子性操作时,需要使用互斥锁来对这段代码进行加锁保护。
                    (4)、 多线程并发执行时,需要使用互斥锁来保证线程间执行的顺序和正确性。

                    总之,互斥锁主要用于控制多个线程对共享资源的访问,确保同一时刻只有一个线程能够访问共享资源。

            2、读写锁

                    (1)、读多写少的情况,读写锁可以提高并发读的性能。读写锁允许多个线程同时读取共享资源,但只允许一个线程进行写操作。
                    (2)、对于频繁的读取共享资源和不频繁的写入共享资源的场景,使用读写锁可以避免由于写操作的串行化导致的性能瓶颈。
                    (3)、适用于需要有一定实时性的场景,读写锁中的读操作是共享的,可以在不阻塞其他线程执行的情况下快速地读取数据,提高程序的响应速度。

                    总之,读写锁适用于读多写少的场景,可以提高并发读的性能,避免由于写操作的串行化导致的性能瓶颈,并且具有一定的实时性。

    四、同步与互斥

            互斥可以简单理解为控制两个进度使之互相排斥,不同同时运行。

            同步可以简单理解为控制两个进度使之有先有后,次序可控。

    五、相关的函数API接口

            1、互斥锁

                    (1)、定义

    1. // 互斥锁是一个特殊的变量
    2. // 声明一个互斥锁变量m
    3. pthread_mutext_t m;

                    (2)、初始化和销毁

                    未经初始化的互斥锁是无法使用的,初始化互斥锁有两种办法:

    1. // 静态初始化
    2. pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER;
    3. // 动态初始化
    4. int pthread_mutex_init(pthread_mutex_t *restrict mutex,
    5. const pthread_mutexattr_t *restrict attr);
    6. // 接口说明
    7. 返回值:一直都是0
    8. 参数mutex:互斥锁
    9. 参数attr:互斥锁属性(一般置为NULL
    10. // 销毁互斥锁
    11. int pthread_mutex_destroy(pthread_mutex_t *mutex);
    12. // 接口说明
    13. 返回值:成功返回0,失败返回错误码
    14. 参数mutex:互斥锁
    15. 由于静态初始化互斥锁不涉及动态内存,因此无需显式释放互斥锁资源,互斥锁会伴随程序一直存在,直到程序退出为止。而动态初始化指使用 pthread_mutex_init()给互斥锁分配动态内存并赋予初始值,因此这种情况下的互斥锁需要在用完之后显式地进行释放资源。

                    (3)、加锁

    1. // 阻塞上锁
    2. int pthread_mutex_lock(pthread_mutex_t *mutex);
    3. // 接口说明
    4. 返回值:成功返回0,失败返回错误码
    5. 参数mutex:互斥锁
    6. // 非阻塞上锁
    7. int pthread_mutex_trylock(pthread_mutex_t *mutex);
    8. // 接口说明
    9. 返回值:成功返回0,失败返回错误码
    10. 参数mutex:互斥锁

                    (4)、解锁

    1. // 解锁
    2. int pthread_mutex_unlock(pthread_mutex_t *mutex);
    3. // 接口说明
    4. 返回值:成功返回0,失败返回错误码
    5. 参数mutex:互斥锁

                  (5)、锁属性

                    当某一个线程所执行的功能有可能发生递归时,需要注意互斥锁的属性问题,需要对有可能被同一个线程重复加锁的锁资源设置为允许递归(重复)上锁。但是需要注意的是,同一个线程对同一锁资源上了多次锁,就需要解锁多少次,否则其他线程将永远无法获得该锁资源(死锁)。

    1. // 初始化线程互斥锁属性
    2. int pthread_mutexattr_init(pthread_mutexattr_t *attr);
    3. // 销毁线程互斥锁属性
    4. int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);
    5. // 设置线程互斥锁属性
    6. int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int kind);
    7. // 设置锁类型为递归锁
    8. pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);

              2、读写锁

                    (1)、定义

    1. // 读写锁是一种特殊的变量
    2. // 声明一个读写锁变量rw
    3. pthread_rwlock_t rw;

                    (2)、初始化和销毁

                    跟互斥锁的初始化和销毁差不多

    1. // 静态初始化
    2. pthread_rwlock_t rw = PTHREAD_RWLOCK_INITIALIZER;
    3. // 动态初始化
    4. int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
    5. const pthread_rwlockattr_t *restrict attr);
    6. // 接口说明
    7. 返回值:成功返回0,失败返回错误码
    8. 参数rwlock:读写锁
    9. 参数attr:读写锁属性
    10. // 销毁
    11. int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
    12. // 接口说明
    13. 返回值:成功返回0,失败返回错误码
    14. 参数rwlock:读写锁

                    (3)、加锁

    1. // 阻塞加读锁
    2. int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
    3. // 非阻塞加读锁
    4. int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
    5. // 阻塞加写锁
    6. int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
    7. // 非阻塞加写锁
    8. int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);

                    (4)、解锁

    1. // 解锁
    2. int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

                    (5)、锁属性

    1. // 初始化读写锁属性
    2. int pthread_rwlockattr_init(pthread_rwlockattr_t *attr);
    3. // 销毁读写锁属性
    4. int pthread_rwlockattr_destroy(pthread_rwlockattr_t *attr);
    5. // 设置读写锁属性
    6. int pthread_rwlockattr_setkind_np(pthread_rwlockattr_t *attr,
    7. int pref);
    8. // 获取读写锁属性
    9. int pthread_rwlockattr_getkind_np(const pthread_rwlockattr_t *attr,
    10. int *pref);
    11. // 接口说明
    12. 参数pref有以下几种:
    13. 1)、PTHREAD_RWLOCK_PREFER_READER_NP,偏向读取(读锁优先)
    14. 2)、PTHREAD_RWLOCK_PREFER_WRITER_NP,偏向写入(写锁优先)
    15. 3)、PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP,偏向写入非递归
    16. 4)、PTHREAD_RWLOCK_PREFER_DEFAULT_NP,默认
    17. 操作步骤:
    18. 1)、初始化锁属性
    19. 2)、设置锁属性
    20. 3)、使用动态初始化的方式初始化锁资源
    21. 4)、销毁锁属性

                    偏向写入:是指读写锁的属性设置为优先考虑写入操作,即在有写锁请求时,读锁请求将被阻塞,直到写锁释放。

                    偏向读取:是指读锁操作优先处理,如果有读锁请求时,则写锁请求将被阻塞。

                    写入非递归:写入操作不支持递归调用,即同一个线程不能在持有写入锁的情况下再次请求写入锁。如果在持有写入锁的情况下再次请求写入锁,则会导致死锁。

                    写入递归:写入操作支持递归调用,即同一个线程可以在持有写入锁的情况下再次请求写入锁。如果使用写入递归模式,需要注意避免出现死锁的情况。

    六、案例

            用互斥锁来实现两个线程的数据同步,一个负责发送,一个负责接收

    1. // 互斥锁的案例
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. char data[100];
    8. pthread_mutex_t data_mutex; // 定义互斥锁变量
    9. pthread_once_t data_mutex_once_init; // 函数单例初始化变量
    10. pthread_once_t data_mutex_once_destroy; // 函数单例销毁变量
    11. // 初始化互斥锁data_mutex
    12. void data_mutex_init(void)
    13. {
    14. pthread_mutex_init(&data_mutex, NULL);
    15. }
    16. // 销毁互斥锁data_mutex
    17. void data_mutex_destroy(void)
    18. {
    19. pthread_mutex_destroy(&data_mutex);
    20. }
    21. // 线程1的例程函数,用来接收数据
    22. void *recv_routine(void *arg)
    23. {
    24. printf("I am recv_routine, my tid = %ld\n", pthread_self());
    25. // 设置线程分离
    26. pthread_detach(pthread_self());
    27. // 函数单例,本程序只会执行data_mutex_init()一次
    28. pthread_once(&data_mutex_once_init, data_mutex_init);
    29. sleep(1); // 先睡眠1s,保证让线程2先执行
    30. while(1)
    31. {
    32. pthread_mutex_lock(&data_mutex); // 阻塞等待有数据才可以申请成功,用来同步
    33. printf("pthread1 read data: %s\n", data);
    34. memset(data, 0, sizeof(data));
    35. }
    36. // 函数单例,本程序只会执行data_mutex_init()一次
    37. pthread_once(&data_mutex_once_destroy, data_mutex_destroy);
    38. }
    39. // 线程2的例程函数,用来发送数据
    40. void *send_routine(void *arg)
    41. {
    42. printf("I am send_routine, my tid = %ld\n", pthread_self());
    43. // 函数单例,本程序只会执行data_mutex_init()一次
    44. pthread_once(&data_mutex_once_init, data_mutex_init);
    45. // 先申请锁,防止线程1读取空数据,为同步做准备
    46. pthread_mutex_lock(&data_mutex);
    47. while(1)
    48. {
    49. printf("please input data:\n");
    50. fgets(data, 100, stdin);
    51. printf("pthread2 send data\n");
    52. pthread_mutex_unlock(&data_mutex); // 解锁,相当于给线程1发送信号
    53. }
    54. // 函数单例,本程序只会执行data_mutex_init()一次
    55. pthread_once(&data_mutex_once_destroy, data_mutex_destroy);
    56. }
    57. int main(int argc, char *argv[])
    58. {
    59. pthread_t tid1, tid2;
    60. // 创建线程1,用来接收数据
    61. errno = pthread_create(&tid1, NULL, recv_routine, NULL);
    62. if(errno == 0)
    63. {
    64. printf("pthread create recv_routine success, tid = %ld\n", tid1);
    65. }
    66. else
    67. {
    68. perror("pthread create recv_routine fail\n");
    69. }
    70. // 1、定义线程属性变量
    71. pthread_attr_t attr2;
    72. // 2、初始化线程属性变量
    73. pthread_attr_init(&attr2);
    74. // 3、设置分离属性
    75. pthread_attr_setdetachstate(&attr2, PTHREAD_CREATE_DETACHED);
    76. // 4、创建线程2,用来发送数据,线程拥有分离属性
    77. errno = pthread_create(&tid2, &attr2, send_routine, NULL);
    78. if(errno == 0)
    79. {
    80. printf("pthread create send_routine success, tid = %ld\n", tid2);
    81. }
    82. else
    83. {
    84. perror("pthread create send_routine fail\n");
    85. }
    86. // 5、销毁属性变量
    87. pthread_attr_destroy(&attr2);
    88. // 一定要加这个,否则主函数直接退出,相当于进程退出,所有线程也退出
    89. // 或者加上while(1)等让主函数不退出
    90. pthread_exit(0);
    91. return 0;
    92. }

            用读写锁来实现对一个整型数据的操作和访问,一条线程使数据自增,另外一条线程判断该数据的奇偶性,并设置写锁优先。

    1. // 读写锁的案例
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. int data = 100; // 共享变量
    8. pthread_rwlock_t data_rwlock; // 定义互斥锁变量
    9. pthread_once_t data_rwlock_once_init; // 函数单例初始化变量
    10. pthread_once_t data_rwlock_once_destroy; // 函数单例销毁变量
    11. // 初始化互斥锁data_rwlock
    12. void data_rwlock_init(void)
    13. {
    14. pthread_rwlockattr_t data_rwlock_attr;
    15. // 设置锁属性为写锁优先
    16. // 1、初始化读写锁属性
    17. pthread_rwlockattr_init(&data_rwlock_attr);
    18. // 2、设置读写锁属性为写锁优先
    19. pthread_rwlockattr_setkind_np(&data_rwlock_attr, PTHREAD_RWLOCK_PREFER_WRITER_NP);
    20. // 3、动态初始化锁资源,此时读写锁是写锁优先的
    21. pthread_rwlock_init(&data_rwlock, NULL);
    22. // 4、销毁读写锁属性
    23. pthread_rwlockattr_destroy(&data_rwlock_attr);
    24. }
    25. // 销毁互斥锁data_rwlock
    26. void data_rwlock_destroy(void)
    27. {
    28. pthread_rwlock_destroy(&data_rwlock);
    29. }
    30. // 线程1的例程函数,用来接收数据
    31. void *recv_routine(void *arg)
    32. {
    33. printf("I am recv_routine, my tid = %ld\n", pthread_self());
    34. // 设置线程分离
    35. pthread_detach(pthread_self());
    36. // 函数单例,本程序只会执行data_rwlock_init()一次
    37. pthread_once(&data_rwlock_once_init, data_rwlock_init);
    38. while(1)
    39. {
    40. // 加上读锁
    41. pthread_rwlock_rdlock(&data_rwlock);
    42. if(data % 2)
    43. {
    44. printf("%d 是奇数\n", data);
    45. }
    46. else
    47. {
    48. printf("%d 是偶数\n", data);
    49. }
    50. // 解锁
    51. pthread_rwlock_unlock(&data_rwlock);
    52. }
    53. // 函数单例,本程序只会执行data_rwlock_init()一次
    54. pthread_once(&data_rwlock_once_destroy, data_rwlock_destroy);
    55. }
    56. // 线程2的例程函数,用来发送数据
    57. void *send_routine(void *arg)
    58. {
    59. printf("I am send_routine, my tid = %ld\n", pthread_self());
    60. // 函数单例,本程序只会执行data_rwlock_init()一次
    61. pthread_once(&data_rwlock_once_init, data_rwlock_init);
    62. while(1)
    63. {
    64. // 加上写锁
    65. pthread_rwlock_wrlock(&data_rwlock);
    66. data++;
    67. // 解锁
    68. pthread_rwlock_unlock(&data_rwlock);
    69. }
    70. // 函数单例,本程序只会执行data_rwlock_init()一次
    71. pthread_once(&data_rwlock_once_destroy, data_rwlock_destroy);
    72. }
    73. int main(int argc, char *argv[])
    74. {
    75. pthread_t tid1, tid2;
    76. // 创建线程1,用来接收数据
    77. errno = pthread_create(&tid1, NULL, recv_routine, NULL);
    78. if(errno == 0)
    79. {
    80. printf("pthread create recv_routine success, tid = %ld\n", tid1);
    81. }
    82. else
    83. {
    84. perror("pthread create recv_routine fail\n");
    85. }
    86. // 1、定义线程属性变量
    87. pthread_attr_t attr2;
    88. // 2、初始化线程属性变量
    89. pthread_attr_init(&attr2);
    90. // 3、设置分离属性
    91. pthread_attr_setdetachstate(&attr2, PTHREAD_CREATE_DETACHED);
    92. // 4、创建线程2,用来发送数据,线程拥有分离属性
    93. errno = pthread_create(&tid2, &attr2, send_routine, NULL);
    94. if(errno == 0)
    95. {
    96. printf("pthread create send_routine success, tid = %ld\n", tid2);
    97. }
    98. else
    99. {
    100. perror("pthread create send_routine fail\n");
    101. }
    102. // 5、销毁属性变量
    103. pthread_attr_destroy(&attr2);
    104. // 一定要加这个,否则主函数直接退出,相当于进程退出,所有线程也退出
    105. // 或者加上while(1)等让主函数不退出
    106. pthread_exit(0);
    107. return 0;
    108. }

    七、总结

            互斥锁和读写锁都是一种并发机制,用于控制多个线程对共享资源的访问。互斥锁主要用于控制多个线程对共享资源的访问,确保同一时刻只有一个线程能够访问共享资源,而读写锁适用于读多写少的场景,可以提高并发读的性能。 读写锁的属性设置需要遵循一定的步骤。

  • 相关阅读:
    网络安全(黑客)自学
    CSS 用 flex 布局绘制骰子
    Linux 学习(CentOS 7)
    基因组组装---基因组大小评估(genome survey)
    高通Android 12 aapt报错问题踩坑
    Tomcat之startup.bat启动闪退解决
    java集合框架综述
    第十一章 枚举和注解
    ubuntu18.04安装pangolin库,图文详解
    学习C++第二课
  • 原文地址:https://blog.csdn.net/AABond/article/details/133418922