linux 线程的同步 三 (信号量的使用)

信号量、同步这些名词在进程间通信时就已经说过,在这里它们的意思是相同的,只不过是同步的对象不同而已。但是下面介绍的信号量的接口是用于线程的信号量,注意不要跟用于进程间通信的信号量混淆,关于用于进程间通信的信号量的详细介绍可以参阅我的另一篇博文:Linux进程间通信——使用信号量。相似地,线程同步是控制线程执行和访问临界区域的方法。

一、什么是信号量

线程的信号量与进程间通信中使用的信号量的概念是一样,它是一种特殊的变量,它可以被增加或减少,但对其的关键访问被保证是原子操作。如果一个程序中有多个线程试图改变一个信号量的值,系统将保证所有的操作都将依次进行。

而只有0和1两种取值的信号量叫做二进制信号量,在这里将重点介绍。而信号量一般常用于保护一段代码,使其每次只被一个执行线程运行。我们可以使用二进制信号量来完成这个工作。

二、信号量的接口和使用

信号量的函数都以sem_开头,线程中使用的基本信号量函数有4个,它们都声明在头文件semaphore.h中。

1、sem_init函数

该函数用于创建信号量,其原型如下:

  1. int sem_init(sem_t *sem, int pshared, unsigned int value);

该函数初始化由sem指向的信号对象,设置它的共享选项,并给它一个初始的整数值。pshared控制信号量的类型,如果其值为0,就表示这个信号量是当前进程的局部信号量,否则信号量就可以在多个进程之间共享,value为sem的初始值。调用成功时返回0,失败返回-1.

2、sem_wait函数

该函数用于以原子操作的方式将信号量的值减1。原子操作就是,如果两个线程企图同时给一个信号量加1或减1,它们之间不会互相干扰。它的原型如下:

  1. int sem_wait(sem_t *sem);

sem指向的对象是由sem_init调用初始化的信号量。调用成功时返回0,失败返回-1.

3、sem_post函数

该函数用于以原子操作的方式将信号量的值加1。它的原型如下:

  1. int sem_post(sem_t *sem);

与sem_wait一样,sem指向的对象是由sem_init调用初始化的信号量。调用成功时返回0,失败返回-1.

4、sem_destroy函数

该函数用于对用完的信号量的清理。它的原型如下:

  1. int sem_destroy(sem_t *sem);

成功时返回0,失败时返回-1.

三、使用信号量同步线程

下面以一个简单的多线程程序来说明如何使用信号量进行线程同步。在主线程中,我们创建子线程,并把数组msg作为参数传递给子线程,然后主线程等待直到有文本输入,然后调用sem_post来增加信号量的值,这样就会立刻使子线程从sem_wait的等待中返回并开始执行。线程函数在把字符串的小写字母变成大写并统计输入的字符数量之后,它再次调用sem_wait并再次被阻塞,直到主线程再次调用sem_post增加信号量的值。

 

  1. #include <unistd.h>
  2. #include <pthread.h>
  3. #include <semaphore.h>
  4. #include <stdlib.h>
  5. #include <stdio.h>
  6. #include <string.h>
  7. //线程函数
  8. void *thread_func(void *msg);
  9. sem_t sem;//信号量
  10. #define MSG_SIZE 512
  11. int main()
  12. {
  13. int res = -1;
  14. pthread_t thread;
  15. void *thread_result = NULL;
  16. char msg[MSG_SIZE];
  17. //初始化信号量,其初值为0
  18. res = sem_init(&sem, 0, 0);
  19. if(res == -1)
  20. {
  21. perror("semaphore intitialization failed\n");
  22. exit(EXIT_FAILURE);
  23. }
  24. //创建线程,并把msg作为线程函数的参数
  25. res = pthread_create(&thread, NULL, thread_func, msg);
  26. if(res != 0)
  27. {
  28. perror("pthread_create failed\n");
  29. exit(EXIT_FAILURE);
  30. }
  31. //输入信息,以输入end结束,由于fgets会把回车(\n)也读入,所以判断时就变成了“end\n”
  32. printf("Input some text. Enter ‘end‘to finish...\n");
  33. while(strcmp("end\n", msg) != 0)
  34. {
  35. fgets(msg, MSG_SIZE, stdin);
  36. //把信号量加1
  37. sem_post(&sem);
  38. }
  39. printf("Waiting for thread to finish...\n");
  40. //等待子线程结束
  41. res = pthread_join(thread, &thread_result);
  42. if(res != 0)
  43. {
  44. perror("pthread_join failed\n");
  45. exit(EXIT_FAILURE);
  46. }
  47. printf("Thread joined\n");
  48. //清理信号量
  49. sem_destroy(&sem);
  50. exit(EXIT_SUCCESS);
  51. }
  52. void* thread_func(void *msg)
  53. {
  54. //把信号量减1
  55. sem_wait(&sem);
  56. char *ptr = msg;
  57. while(strcmp("end\n", msg) != 0)
  58. {
  59. int i = 0;
  60. //把小写字母变成大写
  61. for(; ptr[i] != ‘\0‘; ++i)
  62. {
  63. if(ptr[i] >= ‘a‘ && ptr[i] <= ‘z‘)
  64. {
  65. ptr[i] -= ‘a‘ - ‘A‘;
  66. }
  67. }
  68. printf("You input %d characters\n", i-1);
  69. printf("To Uppercase: %s\n", ptr);
  70. //把信号量减1
  71. sem_wait(&sem);
  72. }
  73. //退出线程
  74. pthread_exit(NULL);
  75. }

运行结果如下:

从运行的结果来看,这个程序的确是同时在运行两个线程,一个控制输入,另一个控制处理统计和输出。

四、分析此信号量同步程序的缺陷

但是这个程序有一点点的小问题,就是这个程序依赖接收文本输入的时间足够长,这样子线程才有足够的时间在主线程还未准备好给它更多的单词去处理和统计之前处理和统计出工作区中字符的个数。所以当我们连续快速地给它两组不同的单词去统计时,子线程就没有足够的时间支执行,但是信号量已被增加不止一次,所以字符统计线程(子线程)就会反复处理和统计字符数目,并减少信号量的值,直到它再次变成0为止。

为了更加清楚地说明上面所说的情况,修改主线程的while循环中的代码,如下:

  1. printf("Input some text. Enter ‘end‘to finish...\n");
  2. while(strcmp("end\n", msg) != 0)
  3. {
  4. if(strncmp("TEST", msg, 4) == 0)
  5. {
  6. strcpy(msg, "copy_data\n");
  7. sem_post(&sem);
  8. }
  9. fgets(msg, MSG_SIZE, stdin);
  10. //把信号量加1
  11. sem_post(&sem);
  12. }

重新编译程序,此时运行结果如下:

当我们输入TEST时,主线程向子线程提供了两个输入,一个是来自键盘的输入,一个来自主线程复数据到msg中,然后从运行结果可以看出,运行出现了异常,没有处理和统计从键盘输入TEST的字符串而却对复制的数据作了两次处理。原因如上面所述。

五、解决此缺陷的方法

解决方法有两个,一个就是再增加一个信号量,让主线程等到子线程处理统计完成之后再继续执行;另一个方法就是使用互斥量。

下面给出用增加一个信号量的方法来解决该问题的代码,源文件名为semthread2.c,源代码如下:

  1. #include <unistd.h>
  2. #include <pthread.h>
  3. #include <semaphore.h>
  4. #include <stdlib.h>
  5. #include <stdio.h>
  6. #include <string.h>
  7. //线程函数
  8. void *thread_func(void *msg);
  9. sem_t sem;//信号量
  10. sem_t sem_add;//增加的信号量
  11. #define MSG_SIZE 512
  12. int main()
  13. {
  14. int res = -1;
  15. pthread_t thread;
  16. void *thread_result = NULL;
  17. char msg[MSG_SIZE];
  18. //初始化信号量,初始值为0
  19. res = sem_init(&sem, 0, 0);
  20. if(res == -1)
  21. {
  22. perror("semaphore intitialization failed\n");
  23. exit(EXIT_FAILURE);
  24. }
  25. //初始化信号量,初始值为1
  26. res = sem_init(&sem_add, 0, 1);
  27. if(res == -1)
  28. {
  29. perror("semaphore intitialization failed\n");
  30. exit(EXIT_FAILURE);
  31. }
  32. //创建线程,并把msg作为线程函数的参数
  33. res = pthread_create(&thread, NULL, thread_func, msg);
  34. if(res != 0)
  35. {
  36. perror("pthread_create failed\n");
  37. exit(EXIT_FAILURE);
  38. }
  39. //输入信息,以输入end结束,由于fgets会把回车(\n)也读入,所以判断时就变成了“end\n”
  40. printf("Input some text. Enter ‘end‘to finish...\n");
  41. sem_wait(&sem_add);
  42. while(strcmp("end\n", msg) != 0)
  43. {
  44. if(strncmp("TEST", msg, 4) == 0)
  45. {
  46. strcpy(msg, "copy_data\n");
  47. sem_post(&sem);
  48. //把sem_add的值减1,即等待子线程处理完成
  49. sem_wait(&sem_add);
  50. }
  51. fgets(msg, MSG_SIZE, stdin);
  52. //把信号量加1
  53. sem_post(&sem);
  54. //把sem_add的值减1,即等待子线程处理完成
  55. sem_wait(&sem_add);
  56. }
  57. printf("Waiting for thread to finish...\n");
  58. //等待子线程结束
  59. res = pthread_join(thread, &thread_result);
  60. if(res != 0)
  61. {
  62. perror("pthread_join failed\n");
  63. exit(EXIT_FAILURE);
  64. }
  65. printf("Thread joined\n");
  66. //清理信号量
  67. sem_destroy(&sem);
  68. sem_destroy(&sem_add);
  69. exit(EXIT_SUCCESS);
  70. }
  71. void* thread_func(void *msg)
  72. {
  73. char *ptr = msg;
  74. //把信号量减1
  75. sem_wait(&sem);
  76. while(strcmp("end\n", msg) != 0)
  77. {
  78. int i = 0;
  79. //把小写字母变成大写
  80. for(; ptr[i] != ‘\0‘; ++i)
  81. {
  82. if(ptr[i] >= ‘a‘ && ptr[i] <= ‘z‘)
  83. {
  84. ptr[i] -= ‘a‘ - ‘A‘;
  85. }
  86. }
  87. printf("You input %d characters\n", i-1);
  88. printf("To Uppercase: %s\n", ptr);
  89. //把信号量加1,表明子线程处理完成
  90. sem_post(&sem_add);
  91. //把信号量减1
  92. sem_wait(&sem);
  93. }
  94. sem_post(&sem_add);
  95. //退出线程
  96. pthread_exit(NULL);
  97. }

其运行结果如下:

分析:这里我们多使用了一个信号量sem_add,并把它的初值赋为1,在主线程在使用sem_wait来等待子线程处理完全,由于它的初值为1,所以主线程第一次调用sem_wait总是立即返回,而第二次调用则需要等待子线程处理完成之后。而在子线程中,若处理完成就会马上使用sem_post来增加信号量的值,使主线程中的sem_wait马上返回并执行紧接下面的代码。从运行结果来看,运行终于正常了。注意,在线程函数中,信号量sem和sem_add使用sem_wait和sem_post函数的次序,它们的次序不能错乱,否则在输入end时,可能运行不正常,子线程不能正常退出,从而导致程序不能退出。

至于使用互斥量的方法,将会在下篇文章:Linux多线程——使用互斥量同步线程中详细介绍。

时间: 2024-10-12 12:30:52

linux 线程的同步 三 (信号量的使用)的相关文章

线程间同步之信号量实现环形buf

一.概述: 信号量是一个非负整数的计数器,它通过计数器来实现多线程对临界资源的顺序访问,从而实现线程间的同步.它与进程间通信的信号量不同,进程间通信的信号量是一个信号量集,而线程间同步的信号量是一个信号.还有一点,就是对信号量的操作是原子的. 信号量与互斥锁的区别: (1).互斥锁的值只能是0或1,而信号量的值为非负整数. (2).互斥锁用与实现线程间的互斥,而信号量用于实现线程间的同步. (3).互斥锁的加锁和解锁必须由同一个线程分别对应使用,而信号量可以由一个线程得到,另一个线程释放. 下面

linux线程间同步方式汇总

抽空做了下linux所有线程间同步方式的汇总(原生的),包含以下几个: 1, mutex 2, condition variable 3, reader-writer lock 4, spin lock 5, barrier mutex是最常用的线程间同步方式,主要目的是保护共享的资源可以被原子地访问. 个人感觉condition variable是除了mutex之外的第二常用的线程间同步方式,可以用来以同步的方式使用一个线程来通知另一个线程某个事件已经发生.可以理解为线程间的信号. reade

linux 线程的同步 二 (互斥锁和条件变量)

互斥锁和条件变量 为了允许在线程或进程之间共享数据,同步时必须的,互斥锁和条件变量是同步的基本组成部分. 1.互斥锁 互斥锁是用来保护临界区资源,实际上保护的是临界区中被操纵的数据,互斥锁通常用于保护由多个线程或多进程分享的共享数据.一般是一些可供线程间使用的全局变量,来达到线程同步的目的,即保证任何时刻只有一个线程或进程在执行其中的代码.一般加锁的轮廓如下: pthread_mutex_lock() 临界区 pthread_mutex_unlock() 互斥锁API pthread_mutex

linux线程间同步(1)读写锁

读写锁比mutex有更高的适用性,可以多个线程同时占用读模式的读写锁,但是只能一个线程占用写模式的读写锁. 1. 当读写锁是写加锁状态时,在这个锁被解锁之前,所有试图对这个锁加锁的线程都会被阻塞: 2. 当读写锁在读加锁状态时,所有试图以读模式对它进行加锁的线程都可以得到访问权,但是以写模式对它进行枷锁的线程将阻塞: 3. 当读写锁在读模式锁状态时,如果有另外线程试图以写模式加锁,读写锁通常会阻塞随后的读模式锁请求,这样可以避免读模式锁长期占用,而等待的写模式锁请求长期阻塞: 这种锁适用对数据结

【转】 Linux 线程同步的三种方法

线程的最大特点是资源的共享性,但资源共享中的同步问题是多线程编程的难点.linux下提供了多种方式来处理线程同步,最常用的是互斥锁.条件变量和信号量. 一.互斥锁(mutex) 通过锁机制实现线程间的同步. 初始化锁.在Linux下,线程的互斥量数据类型是pthread_mutex_t.在使用前,要对它进行初始化.静态分配:pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;动态分配:int pthread_mutex_init(pthread_m

使用信号量控制Linux线程同步

线程同步 在现实生活中,有些东西就必须是按顺序执行的,只有我完成了以后,你才能在我的劳动成果上接着干:不能我还没有完成,你就开始干活了.这就是线程同步最直白的解释了. 在进行程序设计时,亦是如此.线程同步,同步的是什么?它同步的是对共享资源(内存区域,公共变量等)或者临界区域的访问.有的时候,这些共享 资源和临界区域,就只能容忍一个线程对它进行操作(读或者写,读操作一般不控制,主要是写操作),这个时候,我们必须要对这些共享资源或者临界区域进行同 步,那么如何对它们进行线程同步呢? 在Linux中

Linux环境下线程消息同步的陷阱

我们程序中常常会使用到线程间的消息同步处理,比如以下一段伪码 var message = "": void func()  {   1. 启动线程Thread(该线程中填充message的内容):   2. 阻塞,直到等待到完成message填充的事件:   3. 处理message:   .... } void Thread()  {   1. 通过某种处理填充message:   2. 触发func中的阻塞事件: } 我们通常会使用条件变量来完成类似情况的线程同步处理 比如wind

浅析线程间通信三:Barriers、信号量(semaphores)以及各种同步方法比较

之前的文章讨论了互斥量.条件变量.读写锁和自旋锁用于线程的同步,本文将首先讨论Barriers和信号量的使用,并给出了相应的代码和注意事项,相关代码也可在我的github上下载,然后对线程各种同步方法进行了比较. Barriers Barriers是一种不同于前面线程同步机制,它主要用于协调多个线程并行(parallel)共同完成某项任务.一个barrier对象可以使得每个线程阻塞,直到所有协同(合作完成某项任务)的线程执行到某个指定的点,才让这些线程继续执行.前面使用的pthread_join

Linux线程同步

线程同步-互斥锁 1.初始化互斥锁pthread_mutex_init() int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr); 例: pthread_mutex_t mutex; pthread_mutex_init(&mutex, NULL); 2.锁住互斥锁pthread_mutex_lock() int pthread_mutex_lock(pt