多线程同步linux_linux多线程 线程同步 - CSDN
  • linux下提供了多种方式来处理线程同步,最常用的是互斥锁、条件变量和信号量。 1)互斥锁(mutex)  通过锁机制实现线程间的同步。同一时刻只允许一个线程执行一个关键部分的代码。 int pthread_mutex_init...
        线程的最大特点是资源的共享性,但资源共享中的同步问题是多线程编程的难点。linux下提供了多种方式来处理线程同步,最常用的是互斥锁、条件变量和信号量。

    1)互斥锁(mutex

        通过锁机制实现线程间的同步。同一时刻只允许一个线程执行一个关键部分的代码。

    int pthread_mutex_init(pthread_mutex_t *mutex,const pthread_mutex_attr_t *mutexattr);

    int pthread_mutex_lock(pthread_mutex *mutex);

    int pthread_mutex_destroy(pthread_mutex *mutex);

    int pthread_mutex_unlock(pthread_mutex *

    (1)先初始化锁init()或静态赋值pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIER

    attr_t有:

    PTHREAD_MUTEX_TIMED_NP:其余线程等待队列

    PTHREAD_MUTEX_RECURSIVE_NP:嵌套锁,允许线程多次加锁,不同线程,解锁后重新竞争

    PTHREAD_MUTEX_ERRORCHECK_NP:检错,与一同,线程请求已用锁,返回EDEADLK;

    PTHREAD_MUTEX_ADAPTIVE_NP:适应锁,解锁后重新竞争

    (2)加锁,lock,trylock,lock阻塞等待锁,trylock立即返回EBUSY

    (3)解锁,unlock需满足是加锁状态,且由加锁线程解锁

    (4)清除锁,destroy(此时锁必需unlock,否则返回EBUSY,//Linux下互斥锁不占用内存资源

    示例代码

     

    1. #include <cstdio> 

    2. #include <cstdlib> 

    3. #include <unistd.h> 

    4. #include <pthread.h> 

    5. #include "iostream" 

    6.    

    7. using namespace std; 

    8.    

    9. pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; 

    10. int tmp; 

    11.    

    12. void* thread(void *arg) 

    13. { 

    14.     cout << "thread id is " << pthread_self() << endl; 

    15.     pthread_mutex_lock(&mutex); 

    16.     tmp = 12; 

    17.     cout << "Now a is " << tmp << endl; 

    18.     pthread_mutex_unlock(&mutex); 

    19.     return NULL; 

    20. } 

    21.    

    22. int main() 

    23. { 

    24.     pthread_t id; 

    25.     cout << "main thread id is " << pthread_self() << endl; 

    26.     tmp = 3; 

    27.     cout << "In main func tmp = " << tmp << endl; 

    28.     if (!pthread_create(&id, NULL, thread, NULL)) 

    29.     { 

    30.         cout << "Create thread success!" << endl; 

    31.     } 

    32.     else 

    33.     { 

    34.         cout << "Create thread failed!" << endl; 

    35.     } 

    36.     pthread_join(id, NULL); 

    37.     pthread_mutex_destroy(&mutex); 

    38.     return 0; 

    39. }

    编译: g++ -o thread testthread.cpp -lpthread

    说明:pthread库不是Linux系统默认的库,连接时需要使用静态库libpthread.a,所以在使用pthread_create()创建线程,以及调用pthread_atfork()函数建立fork处理程序时,需要链接该库。在编译中要加 -lpthread参数。

     

    2)条件变量(cond

        利用线程间共享的全局变量进行同步的一种机制。条件变量上的基本操作有:触发条件(当条件变为 true );等待条件,挂起线程直到其他线程触发条件。

    int pthread_cond_init(pthread_cond_t *cond,pthread_condattr_t *cond_attr);   

    int pthread_cond_wait(pthread_cond_t *cond,pthread_mutex_t *mutex);

    int pthread_cond_timewait(pthread_cond_t *cond,pthread_mutex *mutex,const timespec *abstime);

    int pthread_cond_destroy(pthread_cond_t *cond);

    int pthread_cond_signal(pthread_cond_t *cond);

    int pthread_cond_broadcast(pthread_cond_t *cond);  //解除所有线程的阻塞

    (1)初始化.init()或者pthread_cond_t cond=PTHREAD_COND_INITIALIER(前者为动态初始化,后者为静态初始化);属性置为NULL

    (2)等待条件成立.pthread_wait,pthread_timewait.wait()释放锁,并阻塞等待条件变量为真,timewait()设置等待时间,仍未signal,返回ETIMEOUT(加锁保证只有一个线程wait)

    (3)激活条件变量:pthread_cond_signal,pthread_cond_broadcast(激活所有等待线程)

    (4)清除条件变量:destroy;无线程等待,否则返回EBUSY

    对于

    int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);

    int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime);

    一定要在mutex的锁定区域内使用

        如果要正确的使用pthread_mutex_lockpthread_mutex_unlock,请参考

    pthread_cleanup_push和pthread_cleanup_pop宏,它能够在线程被cancel的时候正确的释放mutex

        另外,posix1标准说,pthread_cond_signalpthread_cond_broadcast无需考虑调用线程是否是mutex的拥有者,也就是说,可以在lockunlock以外的区域调用。如果我们对调用行为不关心,那么请在lock区域之外调用吧。

    说明:

        (1)pthread_cond_wait 自动解锁互斥量(如同执行了pthread_unlock_mutex),并等待条件变量触发。这时线程挂起,不占用CPU时间,直到条件变量被触发(变量为ture)。在调用 pthread_cond_wait之前,应用程序必须加锁互斥量。pthread_cond_wait函数返回前,自动重新对互斥量加锁(如同执行了pthread_lock_mutex)

        (2)互斥量的解锁和在条件变量上挂起都是自动进行的。因此,在条件变量被触发前,如果所有的线程都要对互斥量加锁,这种机制可保证在线程加锁互斥量和进入等待条件变量期间,条件变量不被触发。条件变量要和互斥量相联结,以避免出现条件竞争——个线程预备等待一个条件变量,当它在真正进入等待之前,另一个线程恰好触发了该条件(条件满足信号有可能在测试条件和调用pthread_cond_wait函数(block)之间被发出,从而造成无限制的等待)。

    (3)pthread_cond_timedwait 和 pthread_cond_wait 一样,自动解锁互斥量及等待条件变量,但它还限定了等待时间。如果在abstime指定的时间内cond未触发,互斥量mutex被重新加锁,且pthread_cond_timedwait返回错误 ETIMEDOUTabstime 参数指定一个绝对时间,时间原点与 time 和 gettimeofday相同:abstime = 0 表示 19701100:00:00 GMT

    (4)pthread_cond_destroy 销毁一个条件变量,释放它拥有的资源。进入 pthread_cond_destroy 之前,必须没有在该条件变量上等待的线程。

        (5)条件变量函数不是异步信号安全的,不应当在信号处理程序中进行调用。特别要注意,如果在信号处理程序中调用 pthread_cond_signal pthread_cond_boardcast 函数,可能导致调用线程死锁。

    示例程序1

     

    1. #include <stdio.h>
    2. #include <pthread.h>
    3. #include "stdlib.h"
    4. #include "unistd.h"

    5. pthread_mutex_t mutex;
    6. pthread_cond_t cond;

    7. void hander(void *arg)
    8. {
    9.     free(arg); 
    10.     (void)pthread_mutex_unlock(&mutex);
    11. }

    12. void *thread1(void *arg)
    13. {
    14.      pthread_cleanup_push(hander, &mutex); 
    15.      while(1) 
    16.      { 
    17.          printf("thread1 is running\n"); 
    18.          pthread_mutex_lock(&mutex); 
    19.          pthread_cond_wait(&cond,&mutex); 
    20.          printf("thread1 applied the condition\n"); 
    21.          pthread_mutex_unlock(&mutex); 
    22.          sleep(4); 
    23.      } 
    24.      pthread_cleanup_pop(0); 
    25. } 

    26. void *thread2(void *arg)
    27. { 
    28.     while(1) 
    29.     { 
    30.         printf("thread2 is running\n"); 
    31.         pthread_mutex_lock(&mutex); 
    32.         pthread_cond_wait(&cond,&mutex); 
    33.         printf("thread2 applied the condition\n"); 
    34.         pthread_mutex_unlock(&mutex); 
    35.         sleep(1); 
    36.     }
    37. }

    38. int main()
    39. {
    40.      pthread_t thid1,thid2; 
    41.      printf("condition variable study!\n"); 
    42.      pthread_mutex_init(&mutex,NULL); 
    43.      pthread_cond_init(&cond,NULL); 
    44.      pthread_create(&thid1,NULL,thread1,NULL); 
    45.      pthread_create(&thid2,NULL,thread2,NULL); 
    46.      sleep(1); 
    47.      do 
    48.      { 
    49.          pthread_cond_signal(&cond); 
    50.      }while(1); 
    51.      sleep(20); 
    52.      pthread_exit(0); 
    53.      return 0;
    54. }

    示例程序2:

    1. #include <pthread.h> 
    2. #include <unistd.h> 
    3. #include "stdio.h"
    4. #include "stdlib.h"

    5. static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER; 
    6. static pthread_cond_t cond = PTHREAD_COND_INITIALIZER; 

    7. struct node 
    8. {
    9.      int n_number; 
    10.      struct node *n_next; 
    11. } *head = NULL; 

    12. /*[thread_func]*/ 
    13. static void cleanup_handler(void *arg) 
    14. {
    15.      printf("Cleanup handler of second thread./n"); 
    16.      free(arg); 
    17.      (void)pthread_mutex_unlock(&mtx); 
    18. } 

    19. static void *thread_func(void *arg) 
    20. {
    21.      struct node *= NULL; 
    22.      pthread_cleanup_push(cleanup_handler, p); 
    23.      while (1) 
    24.      { 
    25.          //这个mutex主要是用来保证pthread_cond_wait的并发性
    26.          pthread_mutex_lock(&mtx); 
    27.          while (head == NULL) 
    28.          { 
    29.          //这个while要特别说明一下,单个pthread_cond_wait功能很完善,为何
    30.          //这里要有一个while (head == NULL)呢?因为pthread_cond_wait里的线 
    31.          //程可能会被意外唤醒,如果这个时候head != NULL,则不是我们想要的情况。 
    32.          //这个时候,应该让线程继续进入pthread_cond_wait 
    33.          // pthread_cond_wait会先解除之前的pthread_mutex_lock锁定的mtx, 
    34.          //然后阻塞在等待对列里休眠,直到再次被唤醒(大多数情况下是等待的条件成立 
    35.          //而被唤醒,唤醒后,该进程会先锁定先pthread_mutex_lock(&mtx);,再读取资源 
    36.          //用这个流程是比较清楚的/*block-->unlock-->wait() return-->lock*/ 
    37.          pthread_cond_wait(&cond, &mtx); 
    38.          p = head; 
    39.          head = head->n_next; 
    40.          printf("Got %d from front of queue/n", p->n_number);
    41.          free(p); 
    42.           } 
    43.           pthread_mutex_unlock(&mtx); //临界区数据操作完毕,释放互斥锁 
    44.      } 
    45.      pthread_cleanup_pop(0); 
    46.      return 0; 
    47. } 

    48. int main(void) 
    49. {
    50.      pthread_t tid; 
    51.      int i; 
    52.      struct node *p; 
    53.      //子线程会一直等待资源,类似生产者和消费者,但是这里的消费者可以是多个消费者,而 
    54.      //不仅仅支持普通的单个消费者,这个模型虽然简单,但是很强大
    55.      pthread_create(&tid, NULL, thread_func, NULL); 
    56.      sleep(1); 
    57.      for (= 0; i < 10; i++) 
    58.      { 
    59.          p = (struct node*)malloc(sizeof(struct node)); 
    60.          p->n_number = i; 
    61.          pthread_mutex_lock(&mtx); //需要操作head这个临界资源,先加锁, 
    62.          p->n_next = head; 
    63.          head = p; 
    64.          pthread_cond_signal(&cond); 
    65.          pthread_mutex_unlock(&mtx); //解锁 
    66.          sleep(1); 
    67.      } 
    68.      printf("thread 1 wanna end the line.So cancel thread 2./n"); 
    69.      
    70.      //关于pthread_cancel,有一点额外的说明,它是从外部终止子线程,子线程会在最近的取消点,退出 
    71.      //线程,而在我们的代码里,最近的取消点肯定就是pthread_cond_wait()了。 
    72.      pthread_cancel(tid); 
    73.      pthread_join(tid, NULL); 
    74.      printf("All done -- exiting/n"); 
    75.      return 0; 
    76. }

    3)信号量

        如同进程一样,线程也可以通过信号量来实现通信,虽然是轻量级的。

        信号量函数的名字都以"sem_"打头。线程使用的基本信号量函数有四个。

    #include

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

        这是对由sem指定的信号量进行初始化,设置好它的共享选项(linux 只支持为0,即表示它是当前进程的局部信号量),然后给它一个初始值VALUE

    两个原子操作函数:

    int sem_wait(sem_t *sem);

    int sem_post(sem_t *sem);

        这两个函数都要用一个由sem_init调用初始化的信号量对象的指针做参数。

    sem_post:给信号量的值加1

    sem_wait:给信号量减1;对一个值为0的信号量调用sem_wait,这个函数将会等待直到有其它线程使它不再是0为止。

    int sem_destroy(sem_t *sem);

        这个函数的作用是再我们用完信号量后都它进行清理。归还自己占有的一切资源。

     

    示例代码:

     

    1. #include <stdlib.h> 
    2. #include <stdio.h> 
    3. #include <unistd.h> 
    4. #include <pthread.h> 
    5. #include <semaphore.h> 
    6. #include <errno.h> 
    7.     
    8. #define return_if_fail(p) if((p) == 0){printf ("[%s]:func error!/n", __func__);return;} 
    9.     
    10. typedef struct _PrivInfo 
    11. { 
    12.   sem_t s1; 
    13.   sem_t s2; 
    14.   time_t end_time; 
    15. }PrivInfo; 
    16.     
    17. static void info_init (PrivInfo* thiz); 
    18. static void info_destroy (PrivInfo* thiz); 
    19. static void* pthread_func_1 (PrivInfo* thiz); 
    20. static void* pthread_func_2 (PrivInfo* thiz); 
    21.     
    22. int main (int argc, char** argv) 
    23. { 
    24.   pthread_t pt_1 = 0; 
    25.   pthread_t pt_2 = 0; 
    26.   int ret = 0; 
    27.   PrivInfo* thiz = NULL; 
    28.       
    29.   thiz = (PrivInfo* )malloc (sizeof (PrivInfo)); 
    30.   if (thiz == NULL) 
    31.   { 
    32.     printf ("[%s]: Failed to malloc priv./n"); 
    33.     return -1; 
    34.   } 
    35.     
    36.   info_init (thiz); 
    37.     
    38.   ret = pthread_create (&pt_1, NULL, (void*)pthread_func_1, thiz); 
    39.   if (ret != 0) 
    40.   { 
    41.     perror ("pthread_1_create:"); 
    42.   } 
    43.     
    44.   ret = pthread_create (&pt_2, NULL, (void*)pthread_func_2, thiz); 
    45.   if (ret != 0) 
    46.   { 
    47.      perror ("pthread_2_create:"); 
    48.   } 
    49.     
    50.   pthread_join (pt_1, NULL); 
    51.   pthread_join (pt_2, NULL); 
    52.     
    53.   info_destroy (thiz); 
    54.       
    55.   return 0; 
    56. } 
    57.     
    58. static void info_init (PrivInfo* thiz) 
    59. { 
    60.   return_if_fail (thiz != NULL); 
    61.     
    62.   thiz->end_time = time(NULL) + 10; 
    63.       
    64.   sem_init (&thiz->s1, 0, 1); 
    65.   sem_init (&thiz->s2, 0, 0); 
    66.     
    67.   return; 
    68. } 
    69.     
    70. static void info_destroy (PrivInfo* thiz) 
    71. { 
    72.   return_if_fail (thiz != NULL); 
    73.     
    74.   sem_destroy (&thiz->s1); 
    75.   sem_destroy (&thiz->s2); 
    76.     
    77.   free (thiz); 
    78.   thiz = NULL; 
    79.     
    80.   return; 
    81. } 
    82.     
    83. static void* pthread_func_1 (PrivInfo* thiz) 
    84. { 
    85.   return_if_fail (thiz != NULL); 
    86.     
    87.   while (time(NULL) < thiz->end_time) 
    88.   { 
    89.     sem_wait (&thiz->s2); 
    90.     printf ("pthread1: pthread1 get the lock./n"); 
    91.     
    92.     sem_post (&thiz->s1); 
    93.     printf ("pthread1: pthread1 unlock/n"); 
    94.     
    95.     sleep (1); 
    96.   } 
    97.     
    98.   return; 
    99. } 
    100.     
    101. static void* pthread_func_2 (PrivInfo* thiz) 
    102. { 
    103.   return_if_fail (thiz != NULL); 
    104.     
    105.   while (time (NULL) < thiz->end_time) 
    106.   { 
    107.     sem_wait (&thiz->s1); 
    108.     printf ("pthread2: pthread2 get the unlock./n"); 
    109.     
    110.     sem_post (&thiz->s2); 
    111.     printf ("pthread2: pthread2 unlock./n"); 
    112.     
    113.     sleep (1); 
    114.   } 
    115.     
    116.   return; 
    117. }

    通过执行结果后,可以看出,会先执行线程二的函数,然后再执行线程一的函数。它们两就实现了同步。在上大学的时候,虽然对这些概念知道,可都没有实践过,所以有时候时间一久就会模糊甚至忘记,到了工作如果还保持这么一种状态,那就太可怕了。虽然现在外面的技术在不断的变化更新,可是不管怎么变,其核心技术还是依旧的,所以我们必须要打好自己的基础,再学习其他新的知识,那时候再学新的知识也会觉得比较简单的。信号量代码摘自http://blog.csdn.net/wtz1985/article/details/3835781

    参考:

    1】 http://www.cnblogs.com/feisky/archive/2009/11/12/1601824.html

    2】 http://www.cnblogs.com/mydomain/archive/2011/07/10/2102147.html

    3】 线程函数介绍

    http://www.unix.org/version2/whatsnew/threadsref.html

    4】 http://www.yolinux.com/TUTORIALS/LinuxTutorialPosixThreads.html

    5】 线程常用函数简介

    http://www.rosoo.net/a/201004/8954.html

    6】 条件变量

    http://blog.csdn.net/hiflower/article/details/2195350

    7】条件变量函数说明

    http://blog.csdn.net/hairetz/article/details/4535920

     

    本文来自博文:

    http://www.cnblogs.com/mydomain/archive/2011/08/14/2138455.html

    展开全文
  • 相似地,线程同步是控制线程执行和访问临界区域的方法。 一、什么是信号量 线程的信号量与进程间通信中使用的信号量的概念是一样,它是一种特殊的变量,它可以被增加或减少,但对其的关键访问被保证是原子操作。...
    信号量、同步这些名词在进程间通信时就已经说过,在这里它们的意思是相同的,只不过是同步的对象不同而已。但是下面介绍的信号量的接口是用于线程的信号量,注意不要跟用于进程间通信的信号量混淆,关于用于进程间通信的信号量的详细介绍可以参阅我的另一篇博文:Linux进程间通信——使用信号量。相似地,线程同步是控制线程执行和访问临界区域的方法

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

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

    二、信号量的接口和使用

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

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

    2、sem_wait函数
    该函数用于以原子操作的方式将信号量的值减1。原子操作就是,如果两个线程企图同时给一个信号量加1或减1,它们之间不会互相干扰。它的原型如下:
    int sem_wait(sem_t *sem);
    sem指向的对象是由sem_init调用初始化的信号量。调用成功时返回0,失败返回-1.

    3、sem_post函数
    该函数用于以原子操作的方式将信号量的值加1。它的原型如下:
    int sem_post(sem_t *sem);
    与sem_wait一样,sem指向的对象是由sem_init调用初始化的信号量。调用成功时返回0,失败返回-1.

    4、sem_destroy函数
    该函数用于对用完的信号量的清理。它的原型如下:
    int sem_destroy(sem_t *sem);
    成功时返回0,失败时返回-1.

    三、使用信号量同步线程

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

    #include <unistd.h>
    #include <pthread.h>
    #include <semaphore.h>
    #include <stdlib.h>
    #include <stdio.h>
    #include <string.h>
    
    //线程函数
    void *thread_func(void *msg);
    sem_t sem;//信号量
    
    #define MSG_SIZE 512
    
    int main()
    {
    	int res = -1;
    	pthread_t thread;
    	void *thread_result = NULL;
    	char msg[MSG_SIZE];
    	//初始化信号量,其初值为0
    	res = sem_init(&sem, 0, 0);
    	if(res == -1)
    	{
    		perror("semaphore intitialization failed\n");
    		exit(EXIT_FAILURE);
    	}
    	//创建线程,并把msg作为线程函数的参数
    	res = pthread_create(&thread, NULL, thread_func, msg);
    	if(res != 0)
    	{
    		perror("pthread_create failed\n");
    		exit(EXIT_FAILURE);
    	}
    	//输入信息,以输入end结束,由于fgets会把回车(\n)也读入,所以判断时就变成了“end\n”
    	printf("Input some text. Enter 'end'to finish...\n");
    	while(strcmp("end\n", msg) != 0)
    	{
    		fgets(msg, MSG_SIZE, stdin);
    		//把信号量加1
    		sem_post(&sem);
    	}
    
    	printf("Waiting for thread to finish...\n");
    	//等待子线程结束
    	res = pthread_join(thread, &thread_result);
    	if(res != 0)
    	{
    		perror("pthread_join failed\n");
    		exit(EXIT_FAILURE);
    	}
    	printf("Thread joined\n");
    	//清理信号量
    	sem_destroy(&sem);
    	exit(EXIT_SUCCESS);
    }
    
    void* thread_func(void *msg)
    {
    	//把信号量减1
    	sem_wait(&sem);
    	char *ptr = msg;
    	while(strcmp("end\n", msg) != 0)
    	{
    		int i = 0;
    		//把小写字母变成大写
    		for(; ptr[i] != '\0'; ++i)
    		{
    			if(ptr[i] >= 'a' && ptr[i] <= 'z')
    			{
    				ptr[i] -= 'a' - 'A';
    			}
    		}
    		printf("You input %d characters\n", i-1);
    		printf("To Uppercase: %s\n", ptr);
    		//把信号量减1
    		sem_wait(&sem);
    	}
    	//退出线程
    	pthread_exit(NULL);
    }
    运行结果如下:



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

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

    为了更加清楚地说明上面所说的情况,修改主线程的while循环中的代码,如下:
    	printf("Input some text. Enter 'end'to finish...\n");
    	while(strcmp("end\n", msg) != 0)
    	{
    		if(strncmp("TEST", msg, 4) == 0)
    		{
    			strcpy(msg, "copy_data\n");
    			sem_post(&sem);
    		}
    		fgets(msg, MSG_SIZE, stdin);
    		//把信号量加1
    		sem_post(&sem);
    	}
    重新编译程序,此时运行结果如下:



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

    五、解决此缺陷的方法

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

    下面给出用增加一个信号量的方法来解决该问题的代码,源文件名为semthread2.c,源代码如下:
    #include <unistd.h>
    #include <pthread.h>
    #include <semaphore.h>
    #include <stdlib.h>
    #include <stdio.h>
    #include <string.h>
    
    
    //线程函数
    void *thread_func(void *msg);
    sem_t sem;//信号量
    sem_t sem_add;//增加的信号量
    
    
    #define MSG_SIZE 512
    
    
    int main()
    {
    	int res = -1;
    	pthread_t thread;
    	void *thread_result = NULL;
    	char msg[MSG_SIZE];
    	//初始化信号量,初始值为0
    	res = sem_init(&sem, 0, 0);
    	if(res == -1)
    	{
    		perror("semaphore intitialization failed\n");
    		exit(EXIT_FAILURE);
    	}
    	//初始化信号量,初始值为1
    	res = sem_init(&sem_add, 0, 1);
    	if(res == -1)
    	{
    		perror("semaphore intitialization failed\n");
    		exit(EXIT_FAILURE);
    	}
    	//创建线程,并把msg作为线程函数的参数
    	res = pthread_create(&thread, NULL, thread_func, msg);
    	if(res != 0)
    	{
    		perror("pthread_create failed\n");
    		exit(EXIT_FAILURE);
    	}
    	//输入信息,以输入end结束,由于fgets会把回车(\n)也读入,所以判断时就变成了“end\n”
    	printf("Input some text. Enter 'end'to finish...\n");
    	
    	sem_wait(&sem_add);
    	while(strcmp("end\n", msg) != 0)
    	{
    		if(strncmp("TEST", msg, 4) == 0)
    		{
    			strcpy(msg, "copy_data\n");
    			sem_post(&sem);
    			//把sem_add的值减1,即等待子线程处理完成
    			sem_wait(&sem_add);
    		}
    		fgets(msg, MSG_SIZE, stdin);
    		//把信号量加1
    		sem_post(&sem);
    		//把sem_add的值减1,即等待子线程处理完成
    		sem_wait(&sem_add);
    	}
    
    
    	printf("Waiting for thread to finish...\n");
    	//等待子线程结束
    	res = pthread_join(thread, &thread_result);
    	if(res != 0)
    	{
    		perror("pthread_join failed\n");
    		exit(EXIT_FAILURE);
    	}
    	printf("Thread joined\n");
    	//清理信号量
    	sem_destroy(&sem);
    	sem_destroy(&sem_add);
    	exit(EXIT_SUCCESS);
    }
    
    
    void* thread_func(void *msg)
    {
    	char *ptr = msg;
    	//把信号量减1
    	sem_wait(&sem);
    	while(strcmp("end\n", msg) != 0)
    	{
    		int i = 0;
    		//把小写字母变成大写
    		for(; ptr[i] != '\0'; ++i)
    		{
    			if(ptr[i] >= 'a' && ptr[i] <= 'z')
    			{
    				ptr[i] -= 'a' - 'A';
    			}
    		}
    		printf("You input %d characters\n", i-1);
    		printf("To Uppercase: %s\n", ptr);
    		//把信号量加1,表明子线程处理完成
    		sem_post(&sem_add);
    		//把信号量减1
    		sem_wait(&sem);
    	}
    	sem_post(&sem_add);
    	//退出线程
    	pthread_exit(NULL);
    }
    其运行结果如下:


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

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

    展开全文
  • Linux下提供了多种方式来处理线程同步,最常用的是互斥锁、条件变量和信号量。 一、互斥锁(mutex)  锁机制是同一时刻只允许一个线程执行一个关键部分的代码。  1. 初始化锁  int pthread_mutex_init...

    Linux下提供了多种方式来处理线程同步,最常用的是互斥锁、条件变量和信号量。

    一、互斥锁(mutex)
      锁机制是同一时刻只允许一个线程执行一个关键部分的代码。

     1. 初始化锁
      int pthread_mutex_init(pthread_mutex_t *mutex,const pthread_mutex_attr_t *mutexattr);
       其中参数 mutexattr 用于指定锁的属性(见下),如果为NULL则使用缺省属性。
       互斥锁的属性在创建锁的时候指定,在LinuxThreads实现中仅有一个锁类型属性,不同的锁类型在试图对一个已经被锁定的互斥锁加锁时表现不同。当前有四个值可供选择:
       (1)PTHREAD_MUTEX_TIMED_NP,这是缺省值,也就是普通锁。当一个线程加锁以后,其余请求锁的线程将形成一个等待队列,并在解锁后按优先级获得锁。这种锁策略保证了资源分配的公平性。
       (2)PTHREAD_MUTEX_RECURSIVE_NP,嵌套锁,允许同一个线程对同一个锁成功获得多次,并通过多次unlock解锁。如果是不同线程请求,则在加锁线程解锁时重新竞争。
       (3)PTHREAD_MUTEX_ERRORCHECK_NP,检错锁,如果同一个线程请求同一个锁,则返回EDEADLK,否则与PTHREAD_MUTEX_TIMED_NP类型动作相同。这样就保证当不允许多次加锁时不会出现最简单情况下的死锁。
       (4)PTHREAD_MUTEX_ADAPTIVE_NP,适应锁,动作最简单的锁类型,仅等待解锁后重新竞争。

     2. 阻塞加锁
      int pthread_mutex_lock(pthread_mutex *mutex);
     3. 非阻塞加锁
       int pthread_mutex_trylock( pthread_mutex_t *mutex);
       该函数语义与 pthread_mutex_lock() 类似,不同的是在锁已经被占据时返回 EBUSY 而不是挂起等待。
     4. 解锁(要求锁是lock状态,并且由加锁线程解锁)
      int pthread_mutex_unlock(pthread_mutex *mutex);
     5. 销毁锁(此时锁必需unlock状态,否则返回EBUSY)
      int pthread_mutex_destroy(pthread_mutex *mutex);

      示例代码:

    [oracle@localhost]$ cat mutextest.c


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

    pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

    int gn;

    void* thread(void *arg)
    {
        printf("thread's ID is  %d\n",pthread_self());
        pthread_mutex_lock(&mutex);
        gn = 12;
        printf("Now gn = %d\n",gn);
        pthread_mutex_unlock(&mutex);
        return NULL;
    }

    int main()
    {
        pthread_t id;
        printf("main thread's ID is %d\n",pthread_self());
        gn = 3;
        printf("In main func, gn = %d\n",gn);
        if (!pthread_create(&id, NULL, thread, NULL))
        {
            printf("Create thread success!\n");
        }else
        {
            printf("Create thread failed!\n");
        }
        pthread_join(id, NULL);
        pthread_mutex_destroy(&mutex);

        return 0;

    }


    [oracle@localhost]$


    二、条件变量(cond)

      条件变量是利用线程间共享全局变量进行同步的一种机制。条件变量上的基本操作有:触发条件(当条件变为 true 时);等待条件,挂起线程直到其他线程触发条件。
       
       1. 初始化条件变量
         int pthread_cond_init(pthread_cond_t *cond,pthread_condattr_t *cond_attr);
          尽管POSIX标准中为条件变量定义了属性,但在Linux中没有实现,因此cond_attr值通常为NULL,且被忽略。
       2. 有两个等待函数
          (1)无条件等待
             int pthread_cond_wait(pthread_cond_t *cond,pthread_mutex_t *mutex);
          (2)计时等待
             int pthread_cond_timewait(pthread_cond_t *cond,pthread_mutex *mutex,const timespec *abstime);
              如果在给定时刻前条件没有满足,则返回ETIMEOUT,结束等待,其中abstime以与time()系统调用相同意义的绝对时间形式出现,0表示格林尼治时间1970年1月1日0时0分0秒。
     
          无论哪种等待方式,都必须和一个互斥锁配合,以防止多个线程同时请求(用 pthread_cond_wait() 或 pthread_cond_timedwait() 请求)竞争条件(Race Condition)。mutex互斥锁必须是普通锁(PTHREAD_MUTEX_TIMED_NP)或者适应锁(PTHREAD_MUTEX_ADAPTIVE_NP),且在调用pthread_cond_wait()前必须由本线程加锁(pthread_mutex_lock()),而在更新条件等待队列以前,mutex保持锁定状态,并在线程挂起进入等待前解锁。在条件满足从而离开pthread_cond_wait()之前,mutex将被重新加锁,以与进入pthread_cond_wait()前的加锁动作对应。

       3. 激发条件
         (1)激活一个等待该条件的线程(存在多个等待线程时按入队顺序激活其中一个)  
             int pthread_cond_signal(pthread_cond_t *cond);
         (2)激活所有等待线程
          int pthread_cond_broadcast(pthread_cond_t *cond);

       4. 销毁条件变量
         int pthread_cond_destroy(pthread_cond_t *cond);
          只有在没有线程在该条件变量上等待的时候才能销毁这个条件变量,否则返回EBUSY


    说明:

      1. pthread_cond_wait 自动解锁互斥量(如同执行了pthread_unlock_mutex),并等待条件变量触发。这时线程挂起,不占用CPU时间,直到条件变量被触发(变量为ture)。在调用 pthread_cond_wait之前,应用程序必须加锁互斥量。pthread_cond_wait函数返回前,自动重新对互斥量加锁(如同执行了pthread_lock_mutex)。

      2. 互斥量的解锁和在条件变量上挂起都是自动进行的。因此,在条件变量被触发前,如果所有的线程都要对互斥量加锁,这种机制可保证在线程加锁互斥量和进入等待条件变量期间,条件变量不被触发。条件变量要和互斥量相联结,以避免出现条件竞争——个线程预备等待一个条件变量,当它在真正进入等待之前,另一个线程恰好触发了该条件(条件满足信号有可能在测试条件和调用pthread_cond_wait函数(block)之间被发出,从而造成无限制的等待)。

      3. 条件变量函数不是异步信号安全的,不应当在信号处理程序中进行调用。特别要注意,如果在信号处理程序中调用 pthread_cond_signal 或 pthread_cond_boardcast 函数,可能导致调用线程死锁

    示例代码1:

    [oracle@localhost]$ cat condtest1.c


    #include <stdio.h>
    #include <pthread.h>
    #include "stdlib.h"
    #include "unistd.h"

    pthread_mutex_t mutex;
    pthread_cond_t cond;

    void hander(void *arg)
    {
        free(arg);
        (void)pthread_mutex_unlock(&mutex);
    }

    void *thread1(void *arg)
    {
        pthread_cleanup_push(hander, &mutex);
        while(1)
        {
            printf("thread1 is running\n");
            pthread_mutex_lock(&mutex);
            pthread_cond_wait(&cond,&mutex);
            printf("thread1 applied the condition\n");
            pthread_mutex_unlock(&mutex);
            sleep(4);
        }
        pthread_cleanup_pop(0);
    }

    void *thread2(void *arg)
    {
        while(1)
        {
            printf("thread2 is running\n");
            pthread_mutex_lock(&mutex);
            pthread_cond_wait(&cond,&mutex);
            printf("thread2 applied the condition\n");
            pthread_mutex_unlock(&mutex);
            sleep(1);
        }
    }

    int main()
    {
        pthread_t thid1,thid2;
        printf("condition variable study!\n");
        pthread_mutex_init(&mutex,NULL);
        pthread_cond_init(&cond,NULL);
        pthread_create(&thid1,NULL,thread1,NULL);
        pthread_create(&thid2,NULL,thread2,NULL);

        sleep(1);

        do{
            pthread_cond_signal(&cond);
        }while(1);

        sleep(20);
        pthread_exit(0);

        return 0;
    }

    [oracle@localhost]$


    示例代码2:

    [oracle@localhost]$ cat condtest2.c


    #include <pthread.h>
    #include <unistd.h>
    #include "stdio.h"
    #include "stdlib.h"

    static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
    static pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

    struct node
    {
        int n_number;
        struct node *n_next;
    }*head = NULL;

    static void cleanup_handler(void *arg)
    {
        printf("Cleanup handler of second thread.\n");
        free(arg);
        (void)pthread_mutex_unlock(&mtx);
    }

    static void *thread_func(void *arg)
    {
        struct node *p = NULL;
        pthread_cleanup_push(cleanup_handler, p);

        while (1)
        {
            // 这个mutex主要是用来保证pthread_cond_wait的并发性。
            pthread_mutex_lock(&mtx);
            while (head == NULL)
            {
                /* 这个while要特别说明一下,单个pthread_cond_wait功能很完善,为何
                * 这里要有一个while (head == NULL)呢?因为pthread_cond_wait里的线
                * 程可能会被意外唤醒,如果这个时候head != NULL,则不是我们想要的情况。
                * 这个时候,应该让线程继续进入pthread_cond_wait
                * pthread_cond_wait会先解除之前的pthread_mutex_lock锁定的mtx,
                * 然后阻塞在等待对列里休眠,直到再次被唤醒(大多数情况下是等待的条件成立
                * 而被唤醒,唤醒后,该进程会先锁定先pthread_mutex_lock(&mtx);,再读取资源
                * 用这个流程是比较清楚的。*/

                pthread_cond_wait(&cond, &mtx);
                p = head;
                head = head->n_next;
                printf("Got %d from front of queue\n", p->n_number);
                free(p);
            }

            pthread_mutex_unlock(&mtx); // 临界区数据操作完毕,释放互斥锁。
        }

        pthread_cleanup_pop(0);

        return 0;

    }

    int main(void)
    {
        pthread_t tid;
        int i;
        struct node *p;

        /* 子线程会一直等待资源,类似生产者和消费者,但是这里的消费者可以是多个消费者,
        * 而不仅仅支持普通的单个消费者,这个模型虽然简单,但是很强大。*/

        pthread_create(&tid, NULL, thread_func, NULL);

        sleep(1);

        for (i = 0; i < 10; i++)
        {
            p = (struct node*)malloc(sizeof(struct node));
            p->n_number = i;
            pthread_mutex_lock(&mtx); // 需要操作head这个临界资源,先加锁。

            p->n_next = head;
            head = p;

            pthread_cond_signal(&cond);

            pthread_mutex_unlock(&mtx); //解锁

            sleep(1);
        }

        printf("thread 1 wanna end the line.So cancel thread 2.\n");

        /* 关于pthread_cancel,有一点额外的说明,它是从外部终止子线程,子线程会在最近的取消点,
        * 退出线程,而在我们的代码里,最近的取消点肯定就是pthread_cond_wait()了。*/

        pthread_cancel(tid);

        pthread_join(tid, NULL);

        printf("All done -- exiting\n");

        return 0;
    }

    [oracle@localhost]$


    可以看出,等待条件变量信号的用法约定一般是这样的:
    ...
    pthread_mutex_lock(&mutex);
    ...
    pthread_cond_wait (&cond, &mutex);
    ...
    pthread_mutex_unlock (&mutex);
    ...

    相信很多人都会有这个疑问:为什么pthread_cond_wait需要的互斥锁不在函数内部定义,而要使用户定义的呢?现在没有时间研究 pthread_cond_wait 的源代码,带着这个问题对条件变量的用法做如下猜测,希望明白真相看过源代码的朋友不吝指正。

    1. pthread_cond_wait 和 pthread_cond_timewait 函数为什么需要互斥锁?因为:条件变量是线程同步的一种方法,这两个函数又是等待信号的函数,函数内部一定有须要同步保护的数据。
    2. 使用用户定义的互斥锁而不在函数内部定义的原因是:无法确定会有多少用户使用条件变量,所以每个互斥锁都须要动态定义,而且管理大量互斥锁的开销太大,使用用户定义的即灵活又方便,符合UNIX哲学的编程风格(随便推荐阅读《UNIX编程哲学》这本好书!)。
    3. 好了,说完了1和2,我们来自由猜测一下 pthread_cond_wait 函数的内部结构吧:
      int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex)
       {
          if(没有条件信号)
          {
             (1)pthread_mutex_unlock (mutex); // 因为用户在函数外面已经加锁了(这是使用约定),但是在没有信号的情况下为了让其他线程也能等待cond,必须解锁。
             (2) 阻塞当前线程,等待条件信号(当然应该是类似于中断触发的方式等待,而不是软件轮询的方式等待)... 有信号就继续执行后面。
             (3) pthread_mutex_lock (mutex); // 因为用户在函数外面要解锁(这也是使用约定),所以要与1呼应加锁,保证用户感觉依然是自己加锁、自己解锁。
          }      
          ...
      }


    三、 信号量


     如同进程一样,线程也可以通过信号量来实现通信,虽然是轻量级的。
       线程使用的基本信号量函数有四个:

      #include <semaphore.h>

         1. 初始化信号量
          int sem_init (sem_t *sem , int pshared, unsigned int value);

          参数:
          sem - 指定要初始化的信号量;
          pshared - 信号量 sem 的共享选项,linux只支持0,表示它是当前进程的局部信号量;
          value - 信号量 sem 的初始值。

          2. 信号量值加1
          给参数sem指定的信号量值加1。
         int sem_post(sem_t *sem);

         3. 信号量值减1
          给参数sem指定的信号量值减1。
         int sem_wait(sem_t *sem);
          如果sem所指的信号量的数值为0,函数将会等待直到有其它线程使它不再是0为止。

         4. 销毁信号量
        销毁指定的信号量。
      int sem_destroy(sem_t *sem);

      示例代码:

    [oracle@localhost]$ cat semtest.c


    #include <stdlib.h>

    #include <stdio.h>
    #include <unistd.h>
    #include <pthread.h>
    #include <semaphore.h>
    #include <errno.h>

    #define return_if_fail(p) if((p) == 0){printf ("[%s]:func error!\n", __func__);return;}

    typedef struct _PrivInfo
    {
        sem_t s1;
        sem_t s2;
        time_t end_time;
    }PrivInfo;

    static void info_init (PrivInfo* prifo);
    static void info_destroy (PrivInfo* prifo);
    static void* pthread_func_1 (PrivInfo* prifo);
    static void* pthread_func_2 (PrivInfo* prifo);

    int main (int argc, char** argv)
    {
        pthread_t pt_1 = 0;
        pthread_t pt_2 = 0;
        int ret = 0;
        PrivInfo* prifo = NULL;
        prifo = (PrivInfo* )malloc (sizeof (PrivInfo));

        if (prifo == NULL)
        {
            printf ("[%s]: Failed to malloc priv.\n");
            return -1;
        }

        info_init (prifo);
        ret = pthread_create (&pt_1, NULL, (void*)pthread_func_1, prifo);
        if (ret != 0)
        {
            perror ("pthread_1_create:");
        }

        ret = pthread_create (&pt_2, NULL, (void*)pthread_func_2, prifo);
        if (ret != 0)
        {
            perror ("pthread_2_create:");
        }

        pthread_join (pt_1, NULL);
        pthread_join (pt_2, NULL);
        info_destroy (prifo);
        return 0;
    }

    static void info_init (PrivInfo* prifo)
    {
        return_if_fail (prifo != NULL);
        prifo->end_time = time(NULL) + 10;
        sem_init (&prifo->s1, 0, 1);
        sem_init (&prifo->s2, 0, 0);
        return;
    }

    static void info_destroy (PrivInfo* prifo)
    {
        return_if_fail (prifo != NULL);
        sem_destroy (&prifo->s1);
        sem_destroy (&prifo->s2);
        free (prifo);
        prifo = NULL;
        return;
    }

    static void* pthread_func_1 (PrivInfo* prifo)
    {
        return_if_fail (prifo != NULL);
        while (time(NULL) < prifo->end_time)
        {
            sem_wait (&prifo->s2);
            printf ("pthread1: pthread1 get the lock.\n");
            sem_post (&prifo->s1);
            printf ("pthread1: pthread1 unlock\n");
            sleep (1);
        }
        return;
    }

    static void* pthread_func_2 (PrivInfo* prifo)
    {
        return_if_fail (prifo != NULL);
        while (time (NULL) < prifo->end_time)
        {
            sem_wait (&prifo->s1);
            printf ("pthread2: pthread2 get the unlock.\n");
            sem_post (&prifo->s2);
            printf ("pthread2: pthread2 unlock.\n");
            sleep (1);
        }
        return;

    }


    [oracle@localhost]$


    也可参考:

    Posix线程编程指南(3)

    Linux线程同步之条件变量

    展开全文
  • Linux多线程同步

    2017-12-09 21:52:26
    典型的UNIX系统都支持一个进程创建多个线程(thread)。在Linux进程基础中提到,Linux以...尽管实现方式有异于其它的UNIX系统,但Linux多线程在逻辑和使用上与真正的多线程并没有差别。   多线程 我们

    https://www.cnblogs.com/freedomabcd/p/7774743.html

    典型的UNIX系统都支持一个进程创建多个线程(thread)。在Linux进程基础中提到,Linux以进程为单位组织操作,Linux中的线程也都基于进程。尽管实现方式有异于其它的UNIX系统,但Linux的多线程在逻辑和使用上与真正的多线程并没有差别。

     

    多线程

    我们先来看一下什么是多线程。在Linux从程序到进程中,我们看到了一个程序在内存中的表示。这个程序的整个运行过程中,只有一个控制权的存在。当函数被调用的时候,该函数获得控制权,成为激活(active)函数,然后运行该函数中的指令。与此同时,其它的函数处于离场状态,并不运行。如下图所示:

    Linux从程序到进程

     

    我们看到,各个方块之间由箭头连接。各个函数就像是连在一根线上一样,计算机像一条流水线一样执行各个函数中定义的操作。这样的一个程序叫做单线程程序。


    多线程就是允许一个进程内存在多个控制权,以便让多个函数同时处于激活状态,从而让多个函数的操作同时运行。即使是单CPU的计算机,也可以通过不停地在不同线程的指令间切换,从而造成多线程同时运行的效果。如下图所示,就是一个多线程的流程:

    main()到func3()再到main()构成一个线程,此外func1()和func2()构成另外两个线程。操作系统一般都有一些系统调用来让你将一个函数运行成为一个新的线程。

     

    回忆我们在Linux从程序到进程中提到的栈的功能和用途。一个栈,只有最下方的帧可被读写。相应的,也只有该帧对应的那个函数被激活,处于工作状态。为了实现多线程,我们必须绕开栈的限制。为此,创建一个新的线程时,我们为这个线程建一个新的栈。每个栈对应一个线程。当某个栈执行到全部弹出时,对应线程完成任务,并收工。所以,多线程的进程在内存中有多个栈。多个栈之间以一定的空白区域隔开,以备栈的增长。每个线程可调用自己栈最下方的帧中的参数和变量,并与其它线程共享内存中的Text,heap和global data区域。对应上面的例子,我们的进程空间中需要有3个栈。

    (要注意的是,对于多线程来说,由于同一个进程空间中存在多个栈,任何一个空白区域被填满都会导致stack overflow的问题。)

     

    并发

    多线程相当于一个并发(concunrrency)系统。并发系统一般同时执行多个任务。如果多个任务可以共享资源,特别是同时写入某个变量的时候,就需要解决同步的问题。比如说,我们有一个多线程火车售票系统,用全局变量i存储剩余的票数。多个线程不断地卖票(i = i - 1),直到剩余票数为0。所以每个都需要执行如下操作:

    复制代码
    复制代码
    /*mu is a global mutex*/

    while (1) {    /*infinite loop*/ if (i != 0) i = i -1 else { printf("no more tickets"); exit(); } }
    复制代码
    复制代码

    如果只有一个线程执行上面的程序的时候(相当于一个窗口售票),则没有问题。但如果多个线程都执行上面的程序(相当于多个窗口售票), 我们就会出现问题。我们会看到,其根本原因在于同时发生的各个线程都可以对i读取和写入。

    我们这里的if结构会给CPU两个指令, 一个是判断是否有剩余的票(i != 0), 一个是卖票 (i = i -1)。某个线程会先判断是否有票(比如说此时i为1),但两个指令之间存在一个时间窗口,其它线程可能在此时间窗口内执行卖票操作(i = i -1),导致该线程卖票的条件不再成立。但该线程由于已经执行过了判断指令,所以无从知道i发生了变化,所以继续执行卖票指令,以至于卖出不存在的票 (i成为负数)。对于一个真实的售票系统来说,这将成为一个严重的错误 (售出了过多的票,火车爆满)。

    在并发情况下,指令执行的先后顺序由内核决定。同一个线程内部,指令按照先后顺序执行,但不同线程之间的指令很难说清除哪一个会先执行。如果运行的结果依赖于不同线程执行的先后的话,那么就会造成竞争条件(race condition),在这样的状况下,计算机的结果很难预知。我们应该尽量避免竞争条件的形成。最常见的解决竞争条件的方法是将原先分离的两个指令构成不可分隔的一个原子操作(atomic operation),而其它任务不能插入到原子操作中。

     

    多线程同步

    对于多线程程序来说,同步(synchronization)是指在一定的时间内只允许某一个线程访问某个资源 。而在此时间内,不允许其它的线程访问该资源。我们可以通过互斥锁(mutex),条件变量(condition variable)和读写锁(reader-writer lock)来同步资源。

     

    1) 互斥锁

    互斥锁是一个特殊的变量,它有锁上(lock)和打开(unlock)两个状态。互斥锁一般被设置成全局变量。打开的互斥锁可以由某个线程获得。一旦获得,这个互斥锁会锁上,此后只有该线程有权打开。其它想要获得互斥锁的线程,会等待直到互斥锁再次打开的时候。我们可以将互斥锁想像成为一个只能容纳一个人的洗手间,当某个人进入洗手间的时候,可以从里面将洗手间锁上。其它人只能在互斥锁外面等待那个人出来,才能进去。在外面等候的人并没有排队,谁先看到洗手间空了,就可以首先冲进去。

    上面的问题很容易使用互斥锁的问题解决,每个线程的程序可以改为:

    复制代码
    复制代码
    /*mu is a global mutex*/

    while (1) { /*infinite loop*/ mutex_lock(mu);       /*aquire mutex and lock it, if cannot, wait until mutex is unblocked*/ if (i != 0) i = i - 1; else { printf("no more tickets"); exit(); } mutex_unlock(mu);     /*release mutex, make it unblocked*/ }
    复制代码
    复制代码

    第一个执行mutex_lock()的线程会先获得mu。其它想要获得mu的线程必须等待,直到第一个线程执行到mutex_unlock()释放mu,才可以获得mu,并继续执行线程。所以线程在mutex_lock()和mutex_unlock()之间的操作时,不会被其它线程影响,就构成了一个原子操作。

    需要注意的时候,如果存在某个线程依然使用原先的程序 (即不尝试获得mu,而直接修改i),互斥锁不能阻止该程序修改i,互斥锁就失去了保护资源的意义。所以,互斥锁机制需要程序员自己来写出完善的程序来实现互斥锁的功能。我们下面讲的其它机制也是如此。

     

    2) 条件变量

    条件变量是另一种常用的变量。它也常常被保存为全局变量,并和互斥锁合作。

     

    假设这样一个状况: 有100个工人,每人负责装修一个房间。当有10个房间装修完成的时候,老板就通知相应的十个工人一起去喝啤酒。

    我们如何实现呢?老板让工人在装修好房间之后,去检查已经装修好的房间数。但多线程条件下,会有竞争条件的危险。也就是说,其他工人有可能会在该工人装修好房子和检查之间完成工作。采用下面方式解决:

    复制代码
    复制代码
    /*mu: global mutex, cond: global codition variable, num: global int*/
    mutex_lock(mu) num = num + 1; /*worker build the room*/ if (num <= 10) { /*worker is within the first 10 to finish*/ cond_wait(mu, cond);     /*wait*/ printf("drink beer"); } else if (num = 11) { /*workder is the 11th to finish*/ cond_broadcast(mu, cond);        /*inform the other 9 to wake up*/ } mutex_unlock(mu);
    复制代码
    复制代码

    上面使用了条件变量。条件变量除了要和互斥锁配合之外,还需要和另一个全局变量配合(这里的num, 也就是装修好的房间数)。这个全局变量用来构成各个条件。

     

    具体思路如下。我们让工人在装修好房间(num = num + 1)之后,去检查已经装修好的房间数( num < 10 )。由于mu被锁上,所以不会有其他工人在此期间装修房间(改变num的值)。如果该工人是前十个完成的人,那么我们就调用cond_wait()函数。
    cond_wait()做两件事情,一个是释放mu,从而让别的工人可以建房。另一个是等待,直到cond的通知。这样的话,符合条件的线程就开始等待。

    当有通知(第十个房间已经修建好)到达的时候,condwait()会再次锁上mu。线程的恢复运行,执行下一句prinft("drink beer") (喝啤酒!)。从这里开始,直到mutex_unlock(),就构成了另一个互斥锁结构。

    那么,前面十个调用cond_wait()的线程如何得到的通知呢?我们注意到elif if,即修建好第11个房间的人,负责调用cond_broadcast()。这个函数会给所有调用cond_wait()的线程放送通知,以便让那些线程恢复运行。

     

    条件变量特别适用于多个线程等待某个条件的发生。如果不使用条件变量,那么每个线程就需要不断尝试获得互斥锁并检查条件是否发生,这样大大浪费了系统的资源。

     

    3) 读写锁

    读写锁与互斥锁非常相似。r、RW lock有三种状态: 共享读取锁(shared-read), 互斥写入锁(exclusive-write lock), 打开(unlock)。后两种状态与之前的互斥锁两种状态完全相同。

    一个unlock的RW lock可以被某个线程获取R锁或者W锁。

    如果被一个线程获得R锁,RW lock可以被其它线程继续获得R锁,而不必等待该线程释放R锁。但是,如果此时有其它线程想要获得W锁,它必须等到所有持有共享读取锁的线程释放掉各自的R锁。

    如果一个锁被一个线程获得W锁,那么其它线程,无论是想要获取R锁还是W锁,都必须等待该线程释放W锁。

    这样,多个线程就可以同时读取共享资源。而具有危险性的写入操作则得到了互斥锁的保护。

     

    我们需要同步并发系统,这为程序员编程带来了难度。但是多线程系统可以很好的解决许多IO瓶颈的问题。比如我们监听网络端口。如果我们只有一个线程,那么我们必须监听,接收请求,处理,回复,再监听。如果我们使用多线程系统,则可以让多个线程监听。当我们的某个线程进行处理的时候,我们还可以有其他的线程继续监听,这样,就大大提高了系统的利用率。在数据越来越大,服务器读写操作越来越多的今天,这具有相当的意义。多线程还可以更有效地利用多CPU的环境。

    (就像做饭一样,不断切换去处理不同的菜。)

     

    本文中的程序采用伪C的写法。不同的语言有不同的函数名(比如mutex_lock)。这里关注的是逻辑上的概念,而不是具体的实现和语言规范。

     

    总结

    multiple threads, multiple stacks

    race condition

    mutex, condition variable, RW lock


    展开全文
  • 背景问题:在特定的应用场景下,多线程不进行同步会造成什么问题?通过多线程模拟多窗口售票为例:#include <iostream> #include<pthread.h> #include<stdio.h> #include<stdlib.h> #...
  • linux下提供了多种方式来处理线程同步,最常用的是互斥锁、条件变量和信号量。 一、互斥锁(mutex) 通过锁机制实现线程间的同步。 初始化锁。在Linux下,线程的互斥量数据类型是pthread_mutex_t。在使用前,要对它...
  • linux下提供了多种方式来处理线程同步,最常用的是互斥锁、条件变量和信号量。 一、互斥锁(mutex) 通过锁机制实现线程间的同步。 初始化锁。在Linux下,线程的互斥量数据类型是pthread_mutex_t。在使用前,要对...
  • Linux线程同步

    2019-06-14 13:45:21
    线程同步 同步: 在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步。 竞态条件: 因为时序问题,而导致程序异常,我们称之为竞态条件。 没有同步导致的饥饿问题...
  • linux线程同步的方法

    2019-06-02 23:28:12
    Linux 线程同步的三种方法 线程的最大特点是资源的共享性,但资源共享中的同步问题是多线程编程的难点。linux下提供了多种方式来处理线程同步,最常用的是互斥锁、条件变量和信号量。 一、互斥锁(mutex) ...
  • linux下提供了多种方式来处理线程同步,最常用的是互斥锁、条件变量和信号量。一、互斥锁(mutex)通过锁机制实现线程间的同步。初始化锁。在Linux下,线程的互斥量数据类型是pthread_mutex_t。在使用前,要对它进行...
  • 我们在前面文章中已经分析了多线程VS多进程,也分析了线程的使用,现在我们来讲解一下linux多线程编程之同步与互斥。 现在,我们不管究竟是多线程好还是多进程好,先讲解一下,为什么要使用多线程? 一、 为什么要用...
  • Linux的网络编程面试--多线程同步 主要有以下几种同步方式: 1.线数据分离 为每个线程准备一套数据,线程之间彼此独立,没有读写竞争。   2.环形缓冲区 如果是一个数据队列,两个线程分别读写,则可以用环形缓冲区...
  • 一直在使用多线程,也学习过很多linux线程进程方面的知识(APUE UNP),有mfc里包装好的多线程,有python程序里的多线程,但是没有好好归纳过,现在好好整理归纳下关于多线程的知识。 关于多线程、多进程,参考:多...
  • 线程同步多线程编程中,解决共享资源冲突的问题 进程同步:多进程编程中,解决共享资源冲突的问题 但是部分同学对线程同步和进程同步研究得不够深入,比如互斥锁和条件变量能不能同时用于线程同步和进程同步,...
  • Linux——线程同步和线程安全 1、 线程同步 同步:多进程或者多线程访问临界资源时,必须进行同步控制。多进程或者多线程的 执行并不完全是绝对的并行运行,有可能主线程需要等待函数线程的某些条件的发生。 ...
  • windows系统多线程同步机制原理总结 同步问题是开发过程中遇到的重要问题之一。同步是要保证在并发执行的环境中各个控制流可以有序地执行,包括对于资源的共享或互斥访问,以及代码功能的逻辑顺序。 为了保证多线程...
  • 多线程操作还有一个重要问题,不用说你也猜的到了:就是线程同步问题 线程同步的概念就是:多个线程之间相互协作完成某项任务 就是说线程A需要先执行某项操作,执行完后,线程B才能执行。 在window中,有个叫做...
  • 前言 ...提多线程编程,就不可不提互斥以及同步,这两个概念摆在这,或许有很多人不屑,然而事实上可能很多人真的还不清楚。 概念 互斥:针对共享资源,如果不是共享资源,何需谈互斥。同一时...
  • 在了解了《同步与互斥的区别 》之后,我们来看看几个经典的线程同步的例子。相信通过具体场景可以让我们学会分析和解决这类线程同步的问题,以便以后应用在实际的项目中。一、生产者-消费者问题问题描述:一组生产者...
  • linux原本没有线程,后来在windows多线程编程影响下linux内核开发者在进程基础上在功能上做出了类似windows线程的linux版本的线程,linux线程归根到底还是进程,只不过是轻量级的进程,开销比真正进程要小得多,...
1 2 3 4 5 ... 20
收藏数 131,130
精华内容 52,452
关键字:

多线程同步linux