zoukankan      html  css  js  c++  java
  • 【转】Linux下的多线程编程

    1 引言 

      线程(thread)技术早在60年代就被提出,但真正应用多线程到操作系统中去,是在80年代中期,solaris是这方面的佼佼者。传统的 Unix也支持线程的概念,但是在一个进程(process)中只允许有一个线程,这样多线程就意味着多进程。现在,多线程技术已经被许多操作系统所支持,包括Windows也包括Linux。 
      为什么有了进程的概念后,还要再引入线程呢?使用多线程到底有哪些好处?什么的系统应该选用多线程?我们首先必须回答这些问题。 
      使用多线程的理由之一是和进程相比,它是一种非常"节俭"的多任务操作方式。我们知道,在Linux系统下,启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段,这是一种"昂贵"的多任务工作方式。而运行于一个进程中的多个线程,它们彼此之间使用相同的地址空间,共享大部分数据,启动一个线程所花费的空间远远小于启动一个进程所花费的空间,而且,线程间彼此切换所需的时间也远远小于进程间切换所需要的时间。据统计,总的说来,一个进程的开销大约是一个线程开销的30倍左右,当然,在具体的系统上,这个数据可能会有较大的区别。 
      使用多线程的理由之二是线程间方便的通信机制。对不同进程来说,它们具有独立的数据空间,要进行数据的传递只能通过通信的方式进行,这种方式不仅费时,而且很不方便。线程则不然,由于同一进程下的线程之间共享数据空间,所以一个线程的数据可以直接为其它线程所用,这不仅快捷,而且方便。当然,数据的共享也带来其他一些问题,有的变量不能同时被两个线程所修改,有的子程序中声明为static的数据更有可能给多线程程序带来灾难性的打击,这些正是编写多线程程序时最需要注意的地方。 
    除了以上所说的优点外,不和进程比较,多线程程序作为一种多任务、并发的工作方式,当然有以下的优点: 

    1. 提高应用程序响应。这对图形界面的程序尤其有意义,当一个操作耗时很长时,整个系统都会等待这个操作,此时程序不会响应键盘、鼠标、菜单的操作,而使用多线程技术,将耗时长的操作(time consuming)置于一个新的线程,可以避免这种尴尬的情况。 
    2. 使多CPU系统更加有效。操作系统会保证当线程数不大于CPU数目时,不同的线程运行于不同的CPU上。 
    3. 改善程序结构。一个既长又复杂的进程可以考虑分为多个线程,成为几个独立或半独立的运行部分,这样的程序会利于理解和修改。 

      下面我们先来尝试编写一个简单的多线程程序。 

    2 简单的多线程编程 

      Linux系统下的多线程遵循POSIX线程接口,称为pthread。编写Linux下的多线程程序,需要使用头文件pthread.h,连接时需要使用库libpthread.a。顺便说一下,Linux下pthread的实现是通过系统调用clone()来实现的。clone()是Linux所特有的系统调用,它的使用方式类似fork,关于clone()的详细情况,有兴趣的读者可以去查看有关文档说明。下面我们展示一个最简单的多线程程序 example1.c。 

    复制代码
    /*
    * =====================================================================================
    *
    * Filename: pthread1.c
    *
    * Description: A Simple program of showing What pthread is
    *
    * Version: 1.0
    * Created: 03/10/2009 08:53:48 PM
    * Revision: none
    * Compiler: gcc
    *
    * Author: Futuredaemon (BUPT), gnuhpc@gmail.com
    * Company: BUPT_UNITED
    *
    * =====================================================================================
    */
    #include <stdio.h>
    #include <stdlib.h>
    #include <pthread.h>
    void *thread(void *threadid)
    {
    int tid;
    tid = (int)threadid;
    printf("Hello World! It's me, thread #%d!/n", tid);
    pthread_exit(NULL);
    }
    int main(void)
    {
        pthread_t id;
        void *ret;
        int i,retv;
        int t=123;
        retv=pthread_create(&id,NULL,(void *) thread,(void *)t);
        if (retv!=0)
        {
            printf ("Create pthread error!/n");
            return 1;
        }
        for (i=0;i<3;i++)
            printf("This is the main process./n");
        pthread_join(id,&ret);
        printf("The thread return value is%d/n",(int)ret);
        return 0;
    }                        
    复制代码

      上面的示例中,我们使用到了两个函数, pthread_create和pthread_join,并声明了一个pthread_t型的变量。 

    pthread_t在头文件/usr/include/bits/pthreadtypes.h中定义: 
    typedef unsigned long int pthread_t; 

      它是一个线程的标识符。函数pthread_create用来创建一个线程,它的原型为: 

    extern int pthread_create __P ((pthread_t *__thread, __const pthread_attr_t *__attr, 
    void *(*__start_routine) (void *), void *__arg)); 

      第一个参数为指向线程标识符的指针,第二个参数用来设置线程属性,第三个参数是线程运行函数的起始地址,最后一个参数是运行函数的参数。这里,我们的函数thread不需要参数,所以最后一个参数设为空指针。第二个参数我们也设为空指针,这样将生成默认属性的线程。对线程属性的设定和修改我们将在下一节阐述。当创建线程成功时,函数返回0,若不为0则说明创建线程失败,常见的错误返回代码为EAGAIN和EINVAL。前者表示系统限制创建新的线程,例如线程数目过多了;后者表示第二个参数代表的线程属性值非法。创建线程成功后,新创建的线程则运行参数三和参数四确定的函数,原来的线程则继续运行下一行代码。
      函数pthread_join用来等待一个线程的结束。函数原型为: 

    extern int pthread_join __P ((pthread_t __th, void **__thread_return)); 

      第一个参数为被等待的线程标识符,第二个参数为一个用户定义的指针,它可以用来存储被等待线程的返回值。这个函数是一个线程阻塞的函数,调用它的函数将一直等待到被等待的线程结束为止,当函数返回时,被等待线程的资源被收回。一个线程的结束有两种途径,一种是象我们上面的例子一样,函数结束了,调用它的线程也就结束了;另一种方式是通过函数pthread_exit来实现。它的函数原型为: 

    extern void pthread_exit __P ((void *__retval)) __attribute__ ((__noreturn__)); 

      唯一的参数是函数的返回代码,只要pthread_join中的第二个参数thread_return不是NULL,这个值将被传递给 thread_return。最后要说明的是,一个线程不能被多个线程等待,否则第一个接收到信号的线程成功返回,其余调用pthread_join的线程则返回错误代码ESRCH。 
    在这一节里,我们编写了一个最简单的线程,并掌握了最常用的三个函数pthread_create,pthread_join和pthread_exit。下面,我们来了解线程的一些常用属性以及如何设置这些属性。 

    3 修改线程的属性 

      在上一节的例子里,我们用pthread_create函数创建了一个线程,在这个线程中,我们使用了默认参数,即将该函数的第二个参数设为NULL。的确,对大多数程序来说,使用默认属性就够了,但我们还是有必要来了解一下线程的有关属性。 
      属性结构为pthread_attr_t,它同样在头文件/usr/include/pthread.h中定义,喜欢追根问底的人可以自己去查看。属性值不能直接设置,须使用相关函数进行操作,初始化的函数为pthread_attr_init,这个函数必须在pthread_create函数之前调用。属性对象主要包括是否绑定、是否分离、堆栈地址、堆栈大小、优先级。默认的属性为非绑定、非分离、缺省1M的堆栈、与父进程同样级别的优先级。 
      关于线程的绑定,牵涉到另外一个概念:轻进程(LWP:Light Weight Process)。轻进程可以理解为内核线程,它位于用户层和系统层之间。系统对线程资源的分配、对线程的控制是通过轻进程来实现的,一个轻进程可以控制一个或多个线程。默认状况下,启动多少轻进程、哪些轻进程来控制哪些线程是由系统来控制的,这种状况即称为非绑定的。绑定状况下,则顾名思义,即某个线程固定的"绑"在一个轻进程之上。被绑定的线程具有较高的响应速度,这是因为CPU时间片的调度是面向轻进程的,绑定的线程可以保证在需要的时候它总有一个轻进程可用。通过设置被绑定的轻进程的优先级和调度级可以使得绑定的线程满足诸如实时反应之类的要求。 
      设置线程绑定状态的函数为 pthread_attr_setscope,它有两个参数,第一个是指向属性结构的指针,第二个是绑定类型,它有两个取值:PTHREAD_SCOPE_SYSTEM(绑定的)和PTHREAD_SCOPE_PROCESS(非绑定的)。下面的代码即创建了一个绑定的线程。 

    复制代码
    #include <pthread.h> 
    pthread_attr_t attr; 
    pthread_t tid; 
    /*初始化属性值,均设为默认值*/ 
    pthread_attr_init(&attr); 
    pthread_attr_setscope(&attr, PTHREAD_SCOPE_SYSTEM); 
    pthread_create(&tid, &attr, (void *) my_function, NULL); 
    复制代码

      线程的分离状态决定一个线程以什么样的方式来终止自己。在上面的例子中,我们采用了线程的默认属性,即为非分离状态,这种情况下,原有的线程等待创建的线程结束。只有当pthread_join()函数返回时,创建的线程才算终止,才能释放自己占用的系统资源。而分离线程不是这样子的,它没有被其他的线程所等待,自己运行结束了,线程也就终止了,马上释放系统资源。程序员应该根据自己的需要,选择适当的分离状态。设置线程分离状态的函数为pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate)。第二个参数可选为PTHREAD_CREATE_DETACHED(分离线程)和 PTHREAD _CREATE_JOINABLE(非分离线程)。这里要注意的一点是,如果设置一个线程为分离线程,而这个线程运行又非常快,它很可能在 pthread_create函数返回之前就终止了,它终止以后就可能将线程号和系统资源移交给其他的线程使用,这样调用pthread_create的线程就得到了错误的线程号。要避免这种情况可以采取一定的同步措施,最简单的方法之一是可以在被创建的线程里调用 pthread_cond_timewait函数,让这个线程等待一会儿,留出足够的时间让函数pthread_create返回。设置一段等待时间,是在多线程编程里常用的方法。但是注意不要使用诸如wait()之类的函数,它们是使整个进程睡眠,并不能解决线程同步的问题。 

      另外一个可能常用的属性是线程的优先级,它存放在结构sched_param中。用函数pthread_attr_getschedparam和函数 pthread_attr_setschedparam进行存放,一般说来,我们总是先取优先级,对取得的值修改后再存放回去。下面即是一段简单的例子。 

    复制代码
    #include <pthread.h> 
    #include <sched.h> 
    pthread_attr_t attr; 
    pthread_t tid; 
    sched_param param; 
    int newprio=20; 
    pthread_attr_init(&attr); 
    pthread_attr_getschedparam(&attr, &param); 
    param.sched_priority=newprio; 
    pthread_attr_setschedparam(&attr, &param); 
    pthread_create(&tid, &attr, (void *)myfunction, myarg); 
    复制代码

    4 线程的数据处理 

      和进程相比,线程的最大优点之一是数据的共享性,各个进程共享父进程处沿袭的数据段,可以方便的获得、修改数据。但这也给多线程编程带来了许多问题。我们必须当心有多个不同的进程访问相同的变量。许多函数是不可重入的,即同时不能运行一个函数的多个拷贝(除非使用不同的数据段)。在函数中声明的静态变量常常带来问题,函数的返回值也会有问题。因为如果返回的是函数内部静态声明的空间的地址,则在一个线程调用该函数得到地址后使用该地址指向的数据时,别的线程可能调用此函数并修改了这一段数据。在进程中共享的变量必须用关键字volatile来定义,这是为了防止编译器在优化时(如gcc中使用-OX参数)改变它们的使用方式。为了保护变量,我们必须使用信号量、互斥等方法来保证我们对变量的正确使用。下面,我们就逐步介绍处理线程数据时的有关知识。 

    4.1 线程数据

      在单线程的程序里,有两种基本的数据:全局变量和局部变量。但在多线程程序里,还有第三种数据类型:线程数据(TSD: Thread-Specific Data)。它和全局变量很象,在线程内部,各个函数可以象使用全局变量一样调用它,但它对线程外部的其它线程是不可见的。这种数据的必要性是显而易见的。例如我们常见的变量errno,它返回标准的出错信息。它显然不能是一个局部变量,几乎每个函数都应该可以调用它;但它又不能是一个全局变量,否则在 A线程里输出的很可能是B线程的出错信息。要实现诸如此类的变量,我们就必须使用线程数据。我们为每个线程数据创建一个键,它和这个键相关联,在各个线程里,都使用这个键来指代线程数据,但在不同的线程里,这个键代表的数据是不同的,在同一个线程里,它代表同样的数据内容。 
      和线程数据相关的函数主要有4个:创建一个键;为一个键指定线程数据;从一个键读取线程数据;删除键。 

      创建键的函数原型为: 

    extern int pthread_key_create __P ((pthread_key_t *__key,void (*__destr_function) (void *))); 

      第一个参数为指向一个键值的指针,第二个参数指明了一个destructor函数,如果这个参数不为空,那么当每个线程结束时,系统将调用这个函数来释放绑定在这个键上的内存块。这个函数常和函数pthread_once ((pthread_once_t*once_control, void (*initroutine) (void)))一起使用,为了让这个键只被创建一次。函数pthread_once声明一个初始化函数,第一次调用pthread_once时它执行这个函数,以后的调用将被它忽略。 
      在下面的例子中,我们创建一个键,并将它和某个数据相关联。我们要定义一个函数 createWindow,这个函数定义一个图形窗口(数据类型为Fl_Window *,这是图形界面开发工具FLTK中的数据类型)。由于各个线程都会调用这个函数,所以我们使用线程数据。 

    复制代码
    /* 声明一个键*/ 
    pthread_key_t myWinKey; 
    /* 函数 createWindow */ 
    void createWindow ( void ) { 
      Fl_Window * win; 
      static pthread_once_t once=PTHREAD_ONCE_INIT; 
      /* 调用函数createMyKey,创建键*/ 
      pthread_once ( & once, createMyKey) ; 
      /*win指向一个新建立的窗口*/ 
      win=new Fl_Window( 0, 0, 100, 100, "MyWindow"); 
      /* 对此窗口作一些可能的设置工作,如大小、位置、名称等 */ 
      setWindow(win); 
      /* 将窗口指针值绑定在键myWinKey上*/ 
      pthread_setpecific ( myWinKey, win); 
    } 
    /* 函数 createMyKey,创建一个键,并指定了destructor */ 
    void createMyKey ( void ) { 
      pthread_keycreate(&myWinKey, freeWinKey); 
    } 
    /* 函数 freeWinKey,释放空间*/ 
    void freeWinKey ( Fl_Window * win){ 
      delete win; 
    } 
    复制代码

      这样,在不同的线程中调用函数createMyWin,都可以得到在线程内部均可见的窗口变量,这个变量通过函数 pthread_getspecific得到。在上面的例子中,我们已经使用了函数pthread_setspecific来将线程数据和一个键绑定在一起。这两个函数的原型如下: 

    extern int pthread_setspecific __P ((pthread_key_t __key,__const void *__pointer)); 
    extern void *pthread_getspecific __P ((pthread_key_t __key)); 

      这两个函数的参数意义和使用方法是显而易见的。要注意的是,pthread_setspecific为一个键指定新的线程数据时,必须自己释放原有的线程数据以回收空间。这个过程函数pthread_key_delete用来删除一个键,这个键占用的内存将被释放,但同样要注意的是,它只释放键占用的内存,并不释放该键关联的线程数据所占用的内存资源,而且它也不会触发函数pthread_key_create中定义的destructor函数。线程数据的释放必须在释放键之前完成。 

    4.2 互斥锁 

    互斥锁用来保证一段时间内只有一个线程在执行一段代码。必要性显而易见:假设各个线程向同一个文件顺序写入数据,最后得到的结果一定是灾难性的。 
    我们先看下面一段代码。这是一个读/写程序,它们公用一个缓冲区,并且我们假定一个缓冲区只能保存一条信息。即缓冲区只有两个状态:有信息或没有信息。 

    复制代码
    /*
    * =====================================================================================
    *
    * Filename: pthread2.c
    *
    * Description: A Program of mutex
    *
    * Version: 1.0
    * Created: 03/11/2009 08:32:51 PM
    * Revision: none
    * Compiler: gcc
    *
    * Author: Futuredaemon (BUPT), gnuhpc@gmail.com
    * Company: BUPT_UNITED
    *
    * =====================================================================================
    */
    #include <pthread.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <time.h>
    void reader_function ( void );
    void writer_function ( void );
    int buffer_has_item=0;
    pthread_mutex_t mutex;
    int main ( void )
    {
      pthread_t reader;
      pthread_mutex_init (&mutex,NULL);
      pthread_create(&reader, NULL, (void *)&reader_function, NULL);
      writer_function( );
      return 0;
    }
    void writer_function (void)
    {
      while (1)
      {
        pthread_mutex_lock (&mutex);
        if (buffer_has_item==0)
        {
          buffer_has_item=1;
          printf("Write once!/n");
        }
        pthread_mutex_unlock(&mutex);
      }
    }
    void reader_function(void)
    {
      while (1)
      {
        pthread_mutex_lock(&mutex);
        if (buffer_has_item==1)
        {
          buffer_has_item=0;
          printf("Read once!/n");
        }
        pthread_mutex_unlock(&mutex);
      }
    }
    复制代码

      这里声明了互斥锁变量mutex,结构pthread_mutex_t为不公开的数据类型,其中包含一个系统分配的属性对象。函数 pthread_mutex_init用来生成一个互斥锁。NULL参数表明使用默认属性。如果需要声明特定属性的互斥锁,须调用函数 pthread_mutexattr_init。函数pthread_mutexattr_setpshared和函数 pthread_mutexattr_settype用来设置互斥锁属性。前一个函数设置属性pshared,它有两个取值,PTHREAD_PROCESS_PRIVATE和PTHREAD_PROCESS_SHARED。前者用来不同进程中的线程同步,后者用于同步本进程的不同线程。在上面的例子中,我们使用的是默认属性PTHREAD_PROCESS_ PRIVATE。后者用来设置互斥锁类型,可选的类型有PTHREAD_MUTEX_NORMAL、PTHREAD_MUTEX_ERRORCHECK、 PTHREAD_MUTEX_RECURSIVE和PTHREAD _MUTEX_DEFAULT。它们分别定义了不同的上所、解锁机制,一般情况下,选用最后一个默认属性。 
      pthread_mutex_lock声明开始用互斥锁上锁,此后的代码直至调用pthread_mutex_unlock为止,均被上锁,即同一时间只能被一个线程调用执行。当一个线程执行到pthread_mutex_lock处时,如果该锁此时被另一个线程使用,那此线程被阻塞,即程序将等待到另一个线程释放此互斥锁。在上面的例子中,我们使用了pthread_delay_np函数,让线程睡眠一段时间,就是为了防止一个线程始终占据此函数。 
      上面的例子非常简单,就不再介绍了,需要提出的是在使用互斥锁的过程中很有可能会出现死锁:两个线程试图同时占用两个资源,并按不同的次序锁定相应的互斥锁,例如两个线程都需要锁定互斥锁1和互斥锁2,a线程先锁定互斥锁1,b线程先锁定互斥锁2,这时就出现了死锁。此时我们可以使用函数 pthread_mutex_trylock,它是函数pthread_mutex_lock的非阻塞版本,当它发现死锁不可避免时,它会返回相应的信息,程序员可以针对死锁做出相应的处理。另外不同的互斥锁类型对死锁的处理不一样,但最主要的还是要程序员自己在程序设计注意这一点。 
      总结一下: 
      1) 只能用于"锁"住临界代码区域 
      2) 一个线程加的锁必须由该线程解锁. 
      锁几乎是我们学习同步时最开始接触到的一个策略,也是最简单, 最直白的策略.

     4.3 条件变量 

      前一节中我们讲述了如何使用互斥锁来实现线程间数据的共享和通信,互斥锁一个明显的缺点是它只有两种状态:锁定和非锁定。而条件变量通过允许线程阻塞和等待另一个线程发送信号的方法弥补了互斥锁的不足,它常和互斥锁一起使用。使用时,条件变量被用来阻塞一个线程,当条件不满足时,线程往往解开相应的互斥锁并等待条件发生变化。一旦其它的某个线程改变了条件变量,它将通知相应的条件变量唤醒一个或多个正被此条件变量阻塞的线程。这些线程将重新锁定互斥锁并重新测试条件是否满足。一般说来,条件变量被用来进行线程间的同步。条件变量,与锁不同, 条件变量用于等待某个条件被触发 
      1) 大体使用的伪码: 

    复制代码
    // 线程一代码 
    pthread_mutex_lock(&mutex); 
    // 设置条件为true 
    pthread_cond_signal(&cond); 
    pthread_mutex_unlock(&mutex); 
    // 线程二代码 
    pthread_mutex_lock(&mutex); 
    while (条件为false) 
    pthread_cond_wait(&cond, &mutex); 
    修改该条件 
    pthread_mutex_unlock(&mutex); 
    复制代码

    需要注意几点: 

    1) 
      第二段代码之所以在pthread_cond_wait外面包含一个while循环不停测试条件是否成立的原因是, 在 pthread_cond_wait被唤醒的时候可能该条件已经不成立,这个情况举例:在pthread_cond_wait解锁、测试到信号后但是在加锁前这个条件不成立了,那么通过这个While还要再检测这个条件是不是成立,那么即使收到了这样一个不稳定的错误信号,while也是跳不出去的。 UNPV2对这个的描述是:"Notice that when pthread_cond_wait returns, we always test the condition again, because spurious wakeups can occur: a wakeup when the desired condition is still not true.". 
    2) 
      pthread_cond_wait调用必须和某一个mutex一起调用, 这个mutex是在外部进行加锁的mutex, 这个锁的作用是互斥,因为两个线程要对线程间共享的某个数据作操作,互斥就是必不可少的了。所以说pthread_cond_wait既进行了线程间的互斥还进行了线程间的同步。在调用pthread_cond_wait时, 内部的实现将首先将这个mutex解锁, 然后等待条件变量被唤醒, 如果没有被唤醒, 该线程将一直休眠, 也就是说, 该线程将一直阻塞在这个pthread_cond_wait调用中, 而当此线程被唤醒时, 将自动将这个mutex加锁. 
      man文档中对这部分的说明是: 
    pthread_cond_wait atomically unlocks the mutex (as per pthread_unlock_mutex) and waits for the condition variable cond to be signaled. The thread execution is suspended and does not consume any CPU time until the condition variable is signaled. The mutex must be locked by the calling thread on entrance to thread_cond_wait. Before returning to the calling thread, pthread_cond_wait re-acquires mutex (as per pthread_lock_mutex). 
      也就是说pthread_cond_wait实际上可以看作是以下几个动作的合体: 
      a.解锁线程锁 
      b.等待条件为true 
      c.加锁线程锁. 

    复制代码
    /*
    * =====================================================================================
    *
    * Filename: pthread3.c
    *
    * Description: A program of showing semaphore
    *
    * Version: 1.0
    * Created: 03/11/2009 10:03:23 PM
    * Revision: none
    * Compiler: gcc
    *
    * Author: Futuredaemon (BUPT), gnuhpc@gmail.com
    * Company: BUPT_UNITED
    *
    * =====================================================================================
    */
    #include <stdio.h>
    #include <pthread.h>
    #include <unistd.h>
    pthread_mutex_t count_lock;
    pthread_cond_t count_nonzero;
    unsigned count = 0;
    void * decrement_count(void *arg)
    {
      pthread_mutex_lock (&count_lock);
      printf("decrement_count get count_lock/n");
      while (count==0)
      {
        printf("decrement_count count == 0 /n");
        printf("decrement_count before cond_wait /n");
        pthread_cond_wait( &count_nonzero, &count_lock);
        printf("decrement_count after cond_wait /n");
      }
      count = count -1;
      pthread_mutex_unlock (&count_lock);
    }
    void * increment_count(void *arg) {   pthread_mutex_lock(&count_lock);   printf("increment_count get count_lock/n");   if (count==0)   {     printf("increment_count before cond_signal/n");     pthread_cond_signal(&count_nonzero);     printf("increment_count after cond_signal/n");   }
      count=count+1;   pthread_mutex_unlock(&count_lock); }
    int main(void) {   pthread_t tid1,tid2;   pthread_mutex_init(&count_lock,NULL);   pthread_cond_init(&count_nonzero,NULL);   pthread_create(&tid1,NULL,decrement_count,NULL);   sleep(2);   pthread_create(&tid2,NULL,increment_count,NULL);   sleep(10);   pthread_exit(0); }
    复制代码

      我们现在要讨论的是什么时候单一Mutex不够,还需要这么麻烦用条件变量? 
      假设有共享的资源sum,与之相关联的mutex是lock_s.假设每个线程对sum的操作很简单的,与sum的状态无关,比如只是sum++.那么只用mutex足够了.程序员只要确保每个线程操作前,取得lock,然后sum++,再unlock即可. 每个线程的代码将像这样 

    add() 
    { 
      pthread_mutex_lock(lock_s); 
      sum++; 
      pthread_mutex_unlock(lock_s); 
    } 

    如果操作比较复杂,假设线程t0,t1,t2的操作是sum++,而线程t3则是在sum到达100的时候,打印出一条信息,并对sum清零.这种情况下, 如果只用mutex, 则t3需要一个循环,每个循环里先取得lock_s,然后检查sum的状态,如果sum>=100,则打印并清零,然后unlock.如果sum>=100,则unlock,并sleep()本线程合适的一段时间. 这个时候,t0,t1,t2的代码不变,t3的代码如下 

    复制代码
    print() 
    { 
      while (1) 
      { 
        pthread_mutex_lock(lock_s); 
        if(sum>=100) 
        { 
          printf(“sum reach 100!”); 
          pthread_mutex_unlock(lock_s); 
        } 
        else 
        { 
          pthread_mutex_unlock(lock_s); 
          my_thread_sleep(100); 
          return OK; 
        } 
      } 
    } 
    复制代码

      这种办法有两个问题 
      1) sum在大多数情况下不会到达100,那么对t3的代码来说,大多数情况下,走的是else分支,只是lock和unlock,然后sleep().这浪费了CPU处理时间. 
      2) 为了节省CPU处理时间,t3会在探测到sum没到达100的时候sleep()一段时间.这样却又带来另外一个问题,亦即t3响应速度下降.可能在sum到达200的时候,t4才会醒过来. 
      3) 这样,程序员在设置sleep()时间的时候陷入两难境地,设置得太短了节省不了资源,太长了又降低响应速度.真是难办啊! 
      这个时候,condition variable内裤外穿,从天而降,拯救了焦头烂额的你. 你首先定义一个condition variable. 

    复制代码
    pthread_cond_t cond_sum_ready=PTHREAD_COND_INITIALIZER; 
    t0,t1,t2的代码只要后面加两行,像这样 
    add() 
    { 
      pthread_mutex_lock(lock_s); 
      sum++; 
      pthread_mutex_unlock(lock_s); 
      if(sum>=100) 
        pthread_cond_signal(&cond_sum_ready); 
    } 
    复制代码

    而t3的代码则是 

    复制代码
    print 
    { 
      pthread_mutex_lock(lock_s); 
      while(sum<100) 
        pthread_cond_wait(&cond_sum_ready, &lock_s); 
      printf(“sum is over 100!”); 
      sum=0; 
      pthread_mutex_unlock(lock_s); 
      return OK; 
    } 
    复制代码

      注意两点: 

      1) 在thread_cond_wait()之前,必须先lock相关联的mutex,因为假如目标条件未满足,pthread_cond_wait()实际上会unlock该mutex, 然后block,在目标条件满足后再重新lock该mutex, 然后返回. 
      2) 为什么是while(sum<100),而不是if(sum<100) ?这是因为在pthread_cond_signal()和pthread_cond_wait()返回之间,有时间差,假设在这个时间差内,还有另外一 
    个线程t4又把sum减少到100以下了,那么t3在pthread_cond_wait()返回之后,显然应该再检查一遍sum的大小.这就是用 while的用意. 
      这么一说就知道什么时候要用条件变量了~就在涉及判断共同变量状态时,换句话说,也就是本节所说的要进程同步的时候用~ 

    4.3信号量 

      信号量既可以作为二值计数器(即0,1),也可以作为资源计数器. 
      信号量本质上是一个非负的整数计数器,它被用来控制对公共资源的访问。当公共资源增加时,调用函数sem_post()增加信号量。只有当信号量值大于0时,才能使用公共资源,使用后,函数sem_wait()减少信号量。函数sem_trywait()和函数pthread_ mutex_trylock()起同样的作用,它是函数sem_wait()的非阻塞版本。下面我们逐个介绍和信号量有关的一些函数,它们都在头文件 /usr/include/semaphore.h中定义。 
      信号量的数据类型为结构sem_t,它本质上是一个长整型的数。函数sem_init()用来初始化一个信号量。它的原型为: 

    extern int sem_init __P ((sem_t *__sem, int __pshared, unsigned int __value)); 

      sem为指向信号量结构的一个指针;pshared不为0时此信号量在进程间共享,否则只能为当前进程的所有线程共享;value给出了信号量的初始值。 

    而函数int sem_getvalue(sem_t *sem, int *sval);则用于获取信号量当前的计数. 函数sem_destroy(sem_t *sem)用来释放信号量sem。 
    可以用信号量模拟锁和条件变量: 

    1. 锁,在同一个线程内同时对某个信号量先调用sem_wait再调用sem_post, 两个函数调用其中的区域就是所要保护的临界区代码了,这个时候其实信号量是作为二值计数器来使用的.不过在此之前要初始化该信号量计数为1,见下面例子中的代码. 
    2. 条件变量,在某个线程中调用sem_wait, 而在另一个线程中调用sem_post. 

      不过, 信号量除了可以作为二值计数器用于模拟线程锁和条件变量之外, 还有比它们更加强大的功能, 信号量可以用做资源计数器, 也就是说初始化信号量的值为某个资源当前可用的数量, 使用了一个之后递减, 归还了一个之后递增。 
      信号量与线程锁,条件变量相比还有以下几点不同: 

    1. 锁必须是同一个线程获取以及释放, 否则会死锁.而条件变量和信号量则不必. 
    2. 信号的递增与减少会被系统自动记住, 系统内部有一个计数器实现信号量,不必担心会丢失, 而唤醒一个条件变量时,如果没有相应的线程在等待该条件变量, 这次唤醒将被丢失. 
    复制代码
    /*
    * =====================================================================================
    *
    * Filename: pthread4.c
    *
    * Description: A program of Semaphore
    *
    * Version: 1.0
    * Created: 03/13/2009 11:54:35 PM
    * Revision: none
    * Compiler: gcc
    *
    * Author: Futuredaemon (BUPT), gnuhpc@gmail.com
    * Company: BUPT_UNITED
    *
    * =====================================================================================
    */
    #include <stdio.h>
    #include <string.h>
    #include <pthread.h>
    #include <errno.h>
    #include <semaphore.h>
    #define BUFSIZE 4
    #define NUMBER 8
    int sum_of_number=0;
    /* 可读 和 可写资源数*/
    sem_t write_res_number;
    sem_t read_res_number;
    /* 循环队列 */
    struct recycle_buffer{
      int buffer[BUFSIZE];
      int head,tail;
    }re_buf;
    /* 用于实现临界区的互斥锁,我们对其初始化*/
    pthread_mutex_t buffer_mutex=PTHREAD_MUTEX_INITIALIZER;
    static void *producer(void * arg)
    {
      int i;
      for(i=0;i<=NUMBER;i++)
      {
        /* 减少可写的资源数 */
        sem_wait(&write_res_number);
        /* 进入互斥区 */
        pthread_mutex_lock(&buffer_mutex);
        /*将数据复制到缓冲区的尾部*/
        re_buf.buffer[re_buf.tail]=i;
        re_buf.tail=(re_buf.tail+1)%BUFSIZE;
        printf("procuder %d write %d./n",(int)pthread_self(),i);
        /*离开互斥区*/
        pthread_mutex_unlock(&buffer_mutex);
        /*增加可读资源数*/
        sem_post(&read_res_number);
      }
      /* 线程终止,如果有线程等待它们结束,则把NULL作为等待其结果的返回值*/
      return NULL;
    }
    static void * consumer(void * arg)
    {
      int i,num;
      for(i=0;i<=NUMBER;i++)
      {
        /* 减少可读资源数 */
        sem_wait(&read_res_number);
        /* 进入互斥区*/
        pthread_mutex_lock(&buffer_mutex);
        /* 从缓冲区的头部获取数据*/
        num = re_buf.buffer[re_buf.head];
        re_buf.head = (re_buf.head+1)%BUFSIZE;
        printf("consumer %d read %d./n",pthread_self(),num);
        /* 离开互斥区*/
        pthread_mutex_unlock(&buffer_mutex);
        sum_of_number+=num;
        /* 增加客写资源数*/
        sem_post(&write_res_number);
      } 
      /* 线程终止,如果有线程等待它们结束,则把NULL作为等待其结果的返回值*/
      return NULL;
    }
    int main(int argc,char ** argv)
    {
      /* 用于保存线程的线程号 */
      pthread_t p_tid;
      pthread_t c_tid;
      int i;
      re_buf.head=0;
      re_buf.tail=0;
      for(i=0;i<BUFSIZE;i++)
        re_buf.buffer[i] =0;
      /* 初始化可写资源数为循环队列的单元数 */
      sem_init(&write_res_number,0,BUFSIZE); // 这里限定了可写的bufsize,当写线程写满buf时,会阻塞,等待读线程读取
      /* 初始化可读资源数为0 */
      sem_init(&read_res_number,0,0);
      /* 创建两个线程,线程函数分别是 producer 和 consumer */
      /* 这两个线程将使用系统的缺省的线程设置,如线程的堆栈大小、线程调度策略和相应的优先级等等*/
      pthread_create(&p_tid,NULL,producer,NULL);
      pthread_create(&c_tid,NULL,consumer,NULL);
      /*等待两个线程完成退出*/
      pthread_join(p_tid,NULL);
      pthread_join(c_tid,NULL);
      printf("The sum of number is %d/n",sum_of_number);
    }
    复制代码

      一直想整理一下Pthread,这次终于利用了几个晚上,参考了多篇文档,整理了自己的思路,终于完成~

  • 相关阅读:
    docker部署数据库
    JAVA 删除Map中元素(JDK8)
    Docker 学习记录基于Linux
    Liunx 操作命令学习记录
    NACOS 认识和学习
    SpringCloud 学习及其相关组件的认识
    springBoot 配置文件的优先级
    配置redisTemplate的序列化
    springBoot 使用测试类报错
    注解反射的认识
  • 原文地址:https://www.cnblogs.com/tianlangshu/p/5223185.html
Copyright © 2011-2022 走看看