Linux C++的多线程编程

1. 引言

  线程(thread)技术早在60年代就被提出,但真正应用多线程到操做系统中去,是在80年代中期,solaris是这方面的佼佼者。传统的Unix也支持线程的概念,可是在一个进程(process)中只容许有一个线程,这样多线程就意味着多进程。如今,多线程技术已经被许多操做系统所支持,包括Windows/NT,固然,也包括Linux。
  为何有了进程的概念后,还要再引入线程呢?使用多线程到底有哪些好处?什么的系统应该选用多线程?咱们首先必须回答这些问题。
  使用多线程的理由之一是和进程相比,它是一种很是"节俭"的多任务操做方式。咱们知道,在Linux系统下,启动一个新的进程必须分配给它独立的地址空间,创建众多的数据表来维护它的代码段、堆栈段和数据段,这是一种"昂贵"的多任务工做方式。而运行于一个进程中的多个线程,它们彼此之间使用相同的地址空间,共享大部分数据,启动一个线程所花费的空间远远小于启动一个进程所花费的空间,并且,线程间彼此切换所需的时间也远远小于进程间切换所须要的时间。据统计,总的说来,一个进程的开销大约是一个线程开销的30倍左右,固然,在具体的系统上,这个数据可能会有较大的区别。
  使用多线程的理由之二是线程间方便的通讯机制。对不一样进程来讲,它们具备独立的数据空间,要进行数据的传递只能经过通讯的方式进行,这种方式不只费时,并且很不方便。线程则否则,因为同一进程下的线程之间共享数据空间,因此一个线程的数据能够直接为其它线程所用,这不只快捷,并且方便。固然,数据的共享也带来其余一些问题,有的变量不能同时被两个线程所修改,有的子程序中声明为static的数据更有可能给多线程程序带来灾难性的打击,这些正是编写多线程程序时最须要注意的地方。
  除了以上所说的优势外,不和进程比较,多线程程序做为一种多任务、并发的工做方式,固然有如下的优势:
  1) 提升应用程序响应。这对图形界面的程序尤为有意义,当一个操做耗时很长时,整个系统都会等待这个操做,此时程序不会响应键盘、鼠标、菜单的操做,而使用多线程技术,将耗时长的操做(time consuming)置于一个新的线程,能够避免这种尴尬的状况。
  2) 使多CPU系统更加有效。操做系统会保证当线程数不大于CPU数目时,不一样的线程运行于不一样的CPU上。
  3) 改善程序结构。一个既长又复杂的进程能够考虑分为多个线程,成为几个独立或半独立的运行部分,这样的程序会利于理解和修改。
  下面咱们先来尝试编写一个简单的多线程程序。ios

 

2. 简单的多线程编程

  Linux系统下的多线程遵循POSIX线程接口,称为pthread。编写Linux下的多线程程序,须要使用头文件pthread.h,链接时须要使用库libpthread.a。顺便说一下,Linux下pthread的实现是经过系统调用clone()来实现的。clone()是Linux所特有的系统调用,它的使用方式相似fork,关于clone()的详细状况,有兴趣的读者能够去查看有关文档说明。下面咱们展现一个最简单的多线程程序threads.cpp。 程序员

复制代码
//Threads.cpp
#include <iostream>
#include <unistd.h>
#include <pthread.h>
using namespace std;

void *thread(void *ptr)
{
    for(int i = 0;i < 3;i++) {
        sleep(1);
        cout << "This is a pthread." << endl;
    }
    return 0;
}

int main() {
    pthread_t id;
    int ret = pthread_create(&id, NULL, thread, NULL);
    if(ret) {
        cout << "Create pthread error!" << endl;
        return 1;
    }
    for(int i = 0;i < 3;i++) {
        cout <<  "This is the main process." << endl;
        sleep(1);
    }
    pthread_join(id, NULL);
    return 0;
}
复制代码

  咱们编译并运行此程序,能够获得以下结果:
  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。下面,咱们来了解线程的一些经常使用属性以及如何设置这些属性。工具

 

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(非绑定的)。下面的代码即建立了一个绑定的线程。post

 

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

复制代码
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);
    }
}
复制代码

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

  4.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.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 <stdio.h>
#include <pthread.h>
#include <semaphore.h>
#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=%d\n",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=%d\n",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这个数值被各个线程任意修改的缘故。这也每每是多线程编程要注意的问题。


5. 小结

  多线程编程是一个颇有意思也颇有用的技术,使用多线程技术的网络蚂蚁是目前最经常使用的下载工具之一,使用多线程技术的grep比单线程的grep要快上几倍,相似的例子还有不少。但愿你们能用多线程技术写出高效实用的好程序来!

相关文章
相关标签/搜索