From:《多线程编程指南》

Sun Microsystems, Inc.

4150 Network Circle

Santa Clara, CA95054

U.S.A.

生成方和使用者问题

    并发编程中收集了许多标准的众所周知的问题,生成方和使用者问题只是其中的一个问题。此问题涉及到一个大小限定的缓冲区和两类线程(生成方和使用者),生成方将项放入缓冲区中,然后使用者从缓冲区中取走项。

    生成方必须在缓冲区中有可用空间之后才能向其中放置内容。使用者必须在生成方向缓冲区中写入之后才能从中提取内容。

    条件变量表示一个等待某个条件获取信号的线程队列。

    示例4-11中包含两个此类队列。一个队列(less)针对生成方,用于等待缓冲区中出现空位置。另一个队列(more)针对使用者,用于等待从缓冲槽位的空位置中提取其中包含的信息。该示例中还包含一个互斥锁,因为描述该缓冲区的数据结构一次只能由一个线程访问。

    示例4-11生成方和使用者的条件变量问题

    typedef struct

    {

        char buf[BSIZE];

        int occupied;

        int nextin;

        int nextout;

        pthread_mutex_t mutex;

        pthread_cond_t more;

        pthread_cond_t less;

    } buffer_t;

    

    buffer_t buffer;

    如示例4-12中所示,生成方线程获取互斥锁以保护buffer数据结构,然后,缓冲区确定是否有空间可用于存放所生成的项。如果没有可用空间,生成方线程会调用pthread_cond_wait() 。pthread_cond_wait()会导致生成方线程连接正在等待less条件获得信号的线程队列。less表示缓冲区中的可用空间。

    与此同时,在调用pthread_cond_wait()的过程中,该线程会释放互斥锁的锁定。正在等待的生成方线程依赖于使用者线程在条件为真时发出信号,如示例4-12中所示。该条件获得信号时,将会唤醒等待less的第一个线程。但是,该线程必须再次锁定互斥锁,然后才能从pthread_cond_wait()返回。

    获取互斥锁可确保该线程再次以独占方式访问缓冲区的数据结构。该线程随后必须检查缓冲区中是否确实存在可用空间。如果空间可用,该线程会向下一个可用的空位置中进行写入。

    于此同时,使用者线程可能正在等待项出现在缓冲区中。这些线程正在等待条件变量more。刚在缓冲区中存储内容和生成方线程会调用pthread_cond_signal()以唤醒下一个正在等待的使用者。如果没有正在等待的使用者,此调用将不起所用。

    最后,生成方线程会解除锁定互斥锁,从而允许其他线程处理缓冲区的数据结构。

    示例4-12生成方和使用者问题:生成方

    void producer(buffer_t * b, char item)

    {

        pthread_mutex_lock(&b->mutex);

        

        while(b->occupied >= BSIZE)

            pthread_cond_wait(&b->less, &b->mutex);

        assert(b->occupied < BSIZE);

        

        b->buf[b->nextin++] = item;

        b->nextin %= BSIZE;        

        b->occupied++;

        

        /*

        now: either b->occupied < BSIZE and b->nextin is the index 

        of the next empty slot in the buffer, or 

        b->occupied == BSIZE and b->nextin is the index of the 

        next (occupied) slot that will be emptied by a consumer 

        (such as b->nextin == b->nextout)

        */

        

        pthread_cond_signal(&b->more);

        pthread_mutex_unlock(&b->mutex);

    }

    

    请注意assert()语句的用法。除非在编译代码时定义了NDEBUG,否则assert()在其参数的计算结果为真(非零)时将不执行任何操作。如果参数的计算结果为假(零),则该程序会中止。在多线程程序中,此类断言特别有用。如果断言失败,assert()会立即指出运行时问题。assert()还有另一个作用,即提供有用的注释。

    以/* now:either b->occupied ... 开头的注释最好以断言形式表示,但是由于语句过于复杂无法用布尔值表达式来表示,因此将用英语表示。

    断言和注释都是不变量的示例。这些不变量是逻辑语句,在程序正常执行时不应将其声明为假,除非是线程正在修改不变量中提到的一些程序变量时的短暂修改过程中。当然,只要有线程执行语句,断言就应当为真。

    使用不变量是一种极为有用的方法。即使没有在程序文本中声明不变量,在分析程序时也应将其视为不变量。

    每次线程执行包含注释的代码时,生成方代码中表示为注释的不变量始终为真。如果将此注释移到紧挨mutex_unlock()的后面,则注释不一定仍然为真。如果将此注释移到紧跟assert()之后的位置,则注释仍然为真。

    因此,不变量可用于表示一个始终为真的属性,除非一个生成方或一个使用者正在更改缓冲区的状态。线程在互斥锁的保护下处理缓冲区时,该线程可能会暂时声明不变量为假。

但是,一旦线程结束对缓冲区的操作,不变量即会恢复为真。

    示例4-13给出了使用者的代码。该逻辑流程与生成方的逻辑流程相对称。

    示例4-13生成方和使用者问题:使用者

    char consumer(buffer_t * b)

    {

        char item;

        pthread_mutex_lock(&b->mutex);

        while(b->occupied <= 0)

            pthread_cond_wait(&b->more, &b->mutex);

        assert(b->occupied > 0);

        item = b->buf[b->nextout++];

        b->nextout %= BSIZE;

        b->occupied--;

        /*

        now: either b->occupied > 0 and b->nextout is the index 

        of the next occupied slot in the buffer, or 

        b->occupied == 0 and b->nextout is the index of the next 

        (empty) slot that will be filled by a producer (such as 

        b->nextout == b->nextin) 

        */

        pthread_cond_signal(&b->less);

        pthread_mutex_unlock(&b->mutex);

        return (item);

    }

 

使用信号时的生成方和使用者问题

    示例4-14中的数据结构与示例4-11中所示的用于条件变量示例的结构类似。两个信号分别表示空缓冲区和满缓冲区的数目,通过这些信号可确保生成等待缓冲区变空,使用者等待缓冲区变满为止。

    示例4-14使用信号时的生成方和使用者问题

    typedef struct

    {

        char buf[BSIZE];

        sem_t occupied;

        sem_t empty;

        int nextin;

        int nextout;

        sem_t pmut;

        sem_t cmut;

    } buffer_t;

    

    buffer_t buffer;

    

    sem_init(&buffer.occupied, 0, 0);

    sem_init(&buffer.empty, 0, BSIZE);

    sem_init(&buffer.pmut, 0, 1);

    sem_init(&buffer.cmut, 0, 1);

    

    buffer.nextin = buffer.nextout = 0;

    

    另一对二进制信号与互斥锁作用相同。在多个生成方使用多个空缓冲槽位,以及多个使用者使用多个满缓冲槽位的情况下,信号可用来控制对缓冲区的访问。在这种情况下,使用互斥锁可能会更好,但这里主要是为了演示信号的用法。

    示例4-15生成方和使用者问题:生成方

    void producer(buffer_t * b, char item)

    {

        sem_wait(&b->empty);

        sem_wait(&b->pmut);

        

        b->buf[b->nextin] = item;

        b->nextin++;

        b->nextint %= BSIZE;

        

        sem_post(&b->pmut);

        sem_post(&b->occupied);

    }

    示例4-16生成方和使用者问题:使用者

    char consumer(buffer_t * b)

    {

        char item;

        sem_wait(&b->occupied);

        sem_wait(&b->cmut);

        

        item = b->buf[b->nextout];

        b->nextout++;

        b->nextout %= BSIZE;

        

        sem_post(&b->cmut);

        sem_post(&b->empty);

        return (item);

    }

 

跨进程边界同步

    每个同步元语都可以跨进程边界使用。通过确保同步变量位于共享内存段中,并调用适当的init()例程,可设置元语。元语必须已经初始化,并且其共享属性设置为在进程间使用。

 

    生成方和使用者问题示例

    示例4-17说明了位于不同进程中的生成方和使用者的问题。主例程将与其子进程共享的全零内存段映射到其地址空间。

    创建子进程是为了运行使用者,父进程则运行生成方。

    本示例还说明了生成方和使用者的驱动程序。producer_driver()可从stdin读取字符并调用producer()。consumer_driver()通过调用consumer()来获取字符并将这些字符写入stdout中。

    示例4-17中的数据结构与示例4-4中所示用于条件变量示例的结构类似。两个信号分别空缓冲区和满缓冲区的数量,通过这些信号可确保生成等待缓冲区变空,使用者等待缓冲区变满为止。

 

    示例4-17跨进程边界同步

    main()

    {

        int zfd;

        buffer_t * buffer;

        pthread_mutexattr_t mattr;

        pthread_condattr_t cvattr_less, cvattr_more;

        

        zfd = open("/dev/zero", O_RDWR);

        buffer = (buffer_t *)mmap(NULL, sizeof(buffer_t), 

            PROT_READ |  PROT_WRITE, MAP_SHARED, zfd, 0);

        buffer->occupied = buffer->nextin = bufer->nextout = 0;

        

        pthread_mutex_attr_init(&mattr);

        pthread_mutexattr_setpshared(&mattr, 

            PTHREAD_PROCESS_SHARED);

        

        pthread_mutex_init(&buffer->lock, &mattr);

        pthread_condattr_init(&cvattr_less);

        pthread_condattr_setpshared(&cattr_less, PTHREAD_PROCESS_SHARED);

        pthread_cond_init(&buffer->less, &cvattr_less);

        pthread_condattr_init(&cvattr_more);

        pthread_condattr_setpshared(&cvattr_more, 

            PTHREAD_PROCESS_SHARED);

        pthread_cond_init(&buffer->more, &cvattr_more);

        

        if(fork() == 0)

            consumer_driver(buffer);

        else

            producer_driver(buffer);

    }

    

    void producer_driver(buffer_t * b)

    {

        int item;

        

        while(1)

        {

            item = getchar();

            if(item == EOF)

            {

                producer(b, '\0');

                break;

            }

            else

                producer(b, (char)item);

        }

    }

    

    void consumer_driver(buffer_t * b)

    {

        char item;

        

        while(1)

        {

            if((item = consumer(b)) == '\0')

                break;

            putchar(item);

        }

    }