• Linux操作系统下的多线程编程解析


    线程(thread)技术早在60年 代就被提出,但真正应用多线程到操作系统中去,是在80年代中期,solaris是这方面的佼佼者。传统的Unix也支持线程的概念,但是在一个进程 (process)中只允许有一个线程,这样多线程就意味着多进程。现在,多线程技术已被许多操作系统所支持,包括Windows/NT,当然,也包括 Linux。

      为什么有了进程的概念后,还要再引入线程呢?使用多线程到底有哪些好处?什么的系统应该选用多线程?我们首先必须回答这些问题。

       使用多线程的理由之一是和进程相比,他是一种很"节俭"的多任务操作方式。我们知道,在Linux系统下,启动一个新的进程必须分配给他单独的地址空 间,建立众多的数据表来维护他的代码段、堆栈段和数据段,这是一种"昂贵"的多任务工作方式。而运行于一个进程中的多个线程,他们彼此之间使用相同的地址 空间,共享大部分数据,启动一个线程所花费的空间远远小于启动一个进程所花费的空间,而且,线程间彼此转换所需的时间也远远小于进程间转换所需要的时间。 据统计,总的说来,一个进程的开销大约是个线程开销的30倍左右,当然,在具体的系统上,这个数据可能会有较大的区别。

      使用多线程的 理由之二是线程间方便的通信机制。对不同进程来说,他们具备单独的数据空间,要进行数据的传递只能通过通信的方式进行,这种方式不但费时,而且很不方便。 线程则不然,由于同一进程下的线程之间共享数据空间,所以一个线程的数据能够直接为其他线程所用,这不但快捷,而且方便。当然,数据的共享也带来其他一些 问题,有的变量不能同时被两个线程所修改,有的子程式中声明为static的数据更有可能给多线程程式带来灾难性的打击,这些正是编写多线程程式时最需要 注意的地方。

      除了以上所说的长处外,不和进程比较,多线程程式作为一种多任务、并发的工作方式,当然有以下的长处:

      1) 提高应用程式响应。这对图像界面的程式尤其有意义,当一个操作耗时很长时,整个系统都会等待这个操作,此时程式不会响应键盘、鼠标、菜单的操作,而使用多线程技术,将耗时长的操作(time consuming)置于一个新的线程,能够避免这种尴尬的情况。

      2) 使多CPU系统更加有效。操作系统会确保当线程数不大于CPU数目时,不同的线程运行于不同的CPU上。

      3) 改善程式结构。一个既长又复杂的进程能够考虑分为多个线程,成为几个单独或半单独的运行部分,这样的程式会利于理解和修改。

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

      简单的多线程编程

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

    /* example.c*/
    #include
    #include
    void thread(void)
    {
     int i;
     for(i=0;i<3;i )
      printf("This is a pthread.n");
    }

    int main(void)
    {
     pthread_t id;
     int i,ret;
     ret=pthread_create(&id,NULL,(void *) thread,NULL);
     if(ret!=0){
      printf ("Create pthread error!n");
      exit (1);
     }
     for(i=0;i<3;i )
      printf("This is the main process.n");
     pthread_join(id,NULL);
     return (0);
    }

      我们编译此程式:

    gcc example1.c -lpthread -o example1

      运行example1,我们得到如下结果:

    This is the main process.
    This is a pthread.
    This is the main process.
    This is the main process.
    This is a pthread.
    This is a pthread.

      再次运行,我们可能得到如下结果:

    This is a pthread.
    This is the main process.
    This is a pthread.
    This is the main process.
    This is a pthread.
    This is the main process.
    前后两次结果不相同,这是两个线程争夺CPU资源的结果。上面的示例中,我们使用到了两个函数,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。下面,我们来了解线程的一些常用属性连同如何配置这些属性。
    [NextPage]

      修改线程的属性

      在上一节的例子里,我们用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_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
    #include
    pthread_attr_t attr;
    pthread_t tid;
    sched_param param;
    int newprio=20;

    pthread_attr_init(&attr);
    pthread_attr_getschedparam(&attr, ¶m);
    param.sched_priority=newprio;
    pthread_attr_setschedparam(&attr, ¶m);
    pthread_create(&tid, &attr, (void *)myfunction, myarg);

      线程的数据处理

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

      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函数。线程数据的释放必须在释放键之前完成。

      2、互斥锁

      互斥锁用来确保一段时间内只有一个线程在执行一段代码。必要性显而易见:假设各个线程向同一个文档顺序写入数据,最后得到的结果一定是灾难性的。

      我们先看下面一段代码。这是个读/写程式,他们公用一个缓冲区,并且我们假定一个缓冲区只能保存一条信息。即缓冲区只有两个状态:有信息或没有信息。

    void reader_function ( void );
    void writer_function ( void );

    char buffer;
    int buffer_has_item=0;
    pthread_mutex_t mutex;
    struct timespec delay;
    void main ( void ){
     pthread_t reader;
     /* 定义延迟时间*/
     delay.tv_sec = 2;
     delay.tv_nec = 0;
     /* 用默认属性初始化一个互斥锁对象*/
     pthread_mutex_init (&mutex,NULL);
     pthread_create(&reader, pthread_attr_default, (void *)&reader_function), NULL);
     writer_function( );
    }

    void writer_function (void){
     while(1){
      /* 锁定互斥锁*/
      pthread_mutex_lock (&mutex);
      if (buffer_has_item==0){
       buffer=make_new_item( );
       buffer_has_item=1;
      }
      /* 打开互斥锁*/
      pthread_mutex_unlock(&mutex);
      pthread_delay_np(&delay);
     }
    }

    void reader_function(void){
     while(1){
      pthread_mutex_lock(&mutex);
      if(buffer_has_item==1){
       consume_item(buffer);
       buffer_has_item=0;
      }
      pthread_mutex_unlock(&mutex);
      pthread_delay_np(&delay);
     }
    }

    [NextPage]

      这里声明了互斥锁变量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的非阻塞版本,当他发现死锁不可避免时,他会返回相应的信 息,程式员能够针对死锁做出相应的处理。另外不同的互斥锁类型对死锁的处理不相同,但最主要的还是要程式员自己在程式设计注意这一点。

    3、条件变量

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

      条件变量的结构为pthread_cond_t,函数pthread_cond_init()被用来初始化一个条件变量。他的原型为:

      extern int pthread_cond_init __P ((pthread_cond_t *__cond,__const pthread_condattr_t *__cond_attr));

      其中cond是个指向结构pthread_cond_t的指针,cond_attr是个指向结构 pthread_condattr_t的指针。结构pthread_condattr_t是条件变量的属性结构,和互斥锁相同我们能够用他来配置条件变量 是进程内可用还是进程间可用,默认值是PTHREAD_ PROCESS_PRIVATE,即此条件变量被同一进程内的各个线程使用。注意初始化条件变量只有未被使用时才能重新初始化或被释放。释放一个条件变量 的函数为pthread_cond_ destroy(pthread_cond_t cond)。 

      函数pthread_cond_wait()使线程阻塞在一个条件变量上。他的函数原型为:

      extern int pthread_cond_wait __P ((pthread_cond_t *__cond,pthread_mutex_t *__mutex));

      线程解开mutex指向的锁并被条件变量cond阻塞。线程能够被函数 pthread_cond_signal和函数pthread_cond_broadcast唤醒,但是要注意的是,条件变量只是起阻塞和唤醒线程的作 用,具体的判断条件还需用户给出,例如一个变量是否为0等等,这一点我们从后面的例子中能够看到。线程被唤醒后,他将重新检查判断条件是否满足,假如还不 满足,一般说来线程应该仍阻塞在这里,被等待被下一次唤醒。这个过程一般用while语句实现。

      另一个用来阻塞线程的函数是pthread_cond_timedwait(),他的原型为:
      extern int pthread_cond_timedwait __P ((pthread_cond_t *__cond,pthread_mutex_t *__mutex, __const struct timespec *__abstime));

      他比函数pthread_cond_wait()多了一个时间参数,经历abstime段时间后,即使条件变量不满足,阻塞也被解除。

      函数pthread_cond_signal()的原型为:

      extern int pthread_cond_signal __P ((pthread_cond_t *__cond));

      他用来释放被阻塞在条件变量cond上的一个线程。多个线程阻塞在此条件变量上时,哪一个线程被唤醒是由线程的 调度策略所决定的。要注意的是,必须用保护条件变量的互斥锁来保护这个函数,否则条件满足信号又可能在测试条件和调用pthread_cond_wait 函数之间被发出,从而造成无限制的等待。下面是使用函数pthread_cond_wait()和函数pthread_cond_signal()的一个 简单的例子。

    pthread_mutex_t count_lock;
    pthread_cond_t count_nonzero;
    unsigned count;
    decrement_count () {
     pthread_mutex_lock (&count_lock);
     while(count==0)
      pthread_cond_wait( &count_nonzero, &count_lock);
      count=count -1;
     pthread_mutex_unlock (&count_lock);
    }

    increment_count(){
     pthread_mutex_lock(&count_lock);
     if(count==0)
      pthread_cond_signal(&count_nonzero);
      count=count 1;
     pthread_mutex_unlock(&count_lock);
    }


      count值为0时,decrement函数在pthread_cond_wait处被 阻塞,并打开互斥锁count_lock。此时,当调用到函数increment_count时,pthread_cond_signal()函数改变条 件变量,告知decrement_count()停止阻塞。读者能够试着让两个线程分别运行这两个函数,看看会出现什么样的结果。

      函数pthread_cond_broadcast(pthread_cond_t *cond)用来唤醒任何被阻塞在条件变量cond上的线程。这些线程被唤醒后将再次竞争相应的互斥锁,所以必须小心使用这个函数。

      4、信号量

       信号量本质上是个非负的整数计数器,他被用来控制对公共资源的访问。当公共资源增加时,调用函数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给出了信号量的初始值。

      函数sem_post( sem_t *sem )用来增加信号量的值。当有线程阻塞在这个信号量上时,调用这个函数会使其中的一个线程不在阻塞,选择机制同样是由线程的调度策略决定的。

    函数sem_wait( sem_t *sem )被用来阻塞当前线程直到信号量sem的值大于0,解除阻塞后将sem的值减一,表明公共资源经使用后减少。函数sem_trywait ( sem_t *sem )是函数sem_wait()的非阻塞版本,他直接将信号量sem的值减一。

      函数sem_destroy(sem_t *sem)用来释放信号量sem。

      下面我们来看一个使用信号量的例子。在这个例子中,一共有4个线程,其中两个线程负责从文档读取数据到公共的缓冲区,另两个线程从缓冲区读取数据作不同的处理(加和乘运算)。

    /* File sem.c */
    #include
    #include
    #include
    #define MAXSTACK 100
    int stack[MAXSTACK][2];
    int size=0;
    sem_t sem;
    /* 从文档1.dat读取数据,每读一次,信号量加一*/
    void ReadData1(void){
     FILE *fp=fopen("1.dat","r");
     while(!feof(fp)){
      fscanf(fp,"%d %d",&stack[size][0],&stack[size][1]);
      sem_post(&sem);
       size;
     }
     fclose(fp);
    }
    /*从文档2.dat读取数据*/
    void ReadData2(void){
     FILE *fp=fopen("2.dat","r");
     while(!feof(fp)){
      fscanf(fp,"%d %d",&stack[size][0],&stack[size][1]);
      sem_post(&sem);
       size;
     }
     fclose(fp);
    }
    /*阻塞等待缓冲区有数据,读取数据后,释放空间,继续等待*/
    void HandleData1(void){
     while(1){
      sem_wait(&sem);
      printf("Plus:%d %d=%dn",stack[size][0],stack[size][1],
      stack[size][0] stack[size][1]);
      --size;
     }
    }

    void HandleData2(void){
     while(1){
      sem_wait(&sem);
      printf("Multiply:%d*%d=%dn",stack[size][0],stack[size][1],
      stack[size][0]*stack[size][1]);
      --size;
     }
    }
    int main(void){
     pthread_t t1,t2,t3,t4;
     sem_init(&sem,0,0);
     pthread_create(&t1,NULL,(void *)HandleData1,NULL);
     pthread_create(&t2,NULL,(void *)HandleData2,NULL);
     pthread_create(&t3,NULL,(void *)ReadData1,NULL);
     pthread_create(&t4,NULL,(void *)ReadData2,NULL);
     /* 防止程式过早退出,让他在此无限期等待*/
     pthread_join(t1,NULL);
    }


      在Linux下,我们用命令gcc -lpthread sem.c -o sem生成可执行文档sem。 我们事先编辑好数据文档1.dat和2.dat,假设他们的内容分别为1 2 3 4 5 6 7 8 9 10和 -1 -2 -3 -4 -5 -6 -7 -8 -9 -10 ,我们运行sem,得到如下的结果:

    Multiply:-1*-2=2
    Plus:-1 -2=-3
    Multiply:9*10=90
    Plus:-9 -10=-19
    Multiply:-7*-8=56
    Plus:-5 -6=-11
    Multiply:-3*-4=12
    Plus:9 10=19
    Plus:7 8=15
    Plus:5 6=11

      从中我们能够看出各个线程间的竞争关系。而数值并未按我们原先的顺序显示出来这是由于size这个数值被各个线程任意修改的缘故。这也往往是多线程编程要注意的问题。

      小结

      多线程编程是个很有意思也很有用的技术,使用多线程技术的网络蚂蚁是现在最常用的下载工具之一,使用多线程技术的grep比单线程的grep要快上几倍,类似的例子更有很多。希望大家能用多线程技术写出高效实用的好程式来。 

  • 相关阅读:
    Hive 函数 + Shell编程的具体实践与运用
    Java的MybatisPlus
    这道 SQL 题,你有不一样的解法吗?
    300行代码,教你用Python写个飞机大战
    仅20行代码,实现文件自动化上传。
    位运算算法
    摆脱五彩斑斓的黑,成为七彩程序员!
    项目管理学定律之墨菲定律——怕什么来什么?
    windows10/liunx创建空大文件
    vim处理冲突文件
  • 原文地址:https://www.cnblogs.com/hjslovewcl/p/2314347.html
Copyright © 2020-2023  润新知