如何通过pthreads管理两个或更多的消费者?

时间:2014-06-26 10:01:57

标签: c multithreading io pthreads buffer

我有一个我想要解决的通用问题,其中大量的二进制数据从标准输入或常规文件流发送到应用程序,然后应用程序将该二进制数据转换为文本。使用线程,我想在将文本传递给下一个应用程序之前对其进行处理,然后进一步修改该文本,依此类推。

作为一个简单的测试用例,我想通过gunzip提取压缩数据。具体来说,我正在考虑使用gunzip -c -来提取通过其(重新分配的)stdin文件描述符发送给它的二进制数据块,然后从其(重新分配的){{1}中提取出来的文本块文件描述符。然后,我可以将这些文本块打印到真实的stdoutstdout(或稍后执行其他操作)。

(我意识到我可以在命令行上进行基于stderr的压缩和提取。我的目标是使用这个测试用例来学习如何在线程之间正确传递二进制和文本数据的通用块通过二进制文件运行该数据,或进一步处理它。)

对于我的测试程序,我设置了三个gzip个线程:

  • pthread_t
  • produce_gzip_chunk_thread
  • consume_gzip_chunk_thread

我为每个线程传递一个名为consume_gunzip_chunk_thread的共享数据实例,它包含一个线程锁,两个条件,以及一些缓冲区和计数器变量。我还为thread_data打开了gunzip进程的一组文件描述符:

popen3()

typedef struct pthread_data pthread_data_t; typedef struct popen3_desc popen3_desc_t; struct pthread_data { pthread_mutex_t in_lock; pthread_cond_t in_cond; pthread_cond_t out_cond; unsigned char in_buf[BUF_LENGTH_VALUE]; size_t n_in_bytes; size_t n_in_bytes_written_to_gunzip; size_t n_out_bytes_read_from_gunzip; FILE *in_file_ptr; boolean in_eof; char in_line[LINE_LENGTH_VALUE]; popen3_desc_t *gunzip_ptr; }; struct popen3_desc { int in; int out; int err; }; 读取一个1024字节的produce_gzip_chunk_thread块 - 来自名为gzip的常规文件的压缩字节。

这些字节被写入名为foo.gz的{​​{1}}缓冲区,该缓冲区是我传递给每个线程的共享数据结构的一部分:

unsigned char

一旦in_buf中存储了正数字节 - 也就是说,我们已从我们的输入void * produce_gzip_chunk(void *t_data) { #ifdef DEBUG fprintf(stderr, "Debug: Entering --> produce_gzip_chunk()\n"); #endif pthread_data_t *d = (pthread_data_t *)t_data; unsigned char in_buf[BUF_LENGTH_VALUE]; size_t n_in_bytes = 0; d->in_eof = kFalse; pthread_mutex_lock(&d->in_lock); while(kTrue) { n_in_bytes = fread(in_buf, sizeof(in_buf[0]), sizeof(in_buf), d->in_file_ptr); if (n_in_bytes > 0) { while (d->n_in_bytes != 0 || d->n_out_bytes_read_from_gunzip != 0) pthread_cond_wait(&d->in_cond, &d->in_lock); memcpy(d->in_buf, in_buf, n_in_bytes); d->n_in_bytes = n_in_bytes; #ifdef DEBUG fprintf(stderr, "Debug: ######## [%07zu] produced chunk\n", d->n_in_bytes); #endif pthread_cond_signal(&d->in_cond); } else if (feof(d->in_file_ptr) || ferror(d->in_file_ptr)) break; } d->in_eof = kTrue; pthread_mutex_unlock(&d->in_lock); pthread_cond_signal(&d->in_cond); #ifdef DEBUG fprintf(stderr, "Debug: Leaving --> produce_gzip_chunk()\n"); #endif return NULL; } 存档中提取了需要使用n_bytes处理的数据 - 这会触发一个允许第二个线程gzip运行的条件:

gunzip

在使用consume_gzip_chunk_thread数据块时,我们使用void * consume_gzip_chunk(void *t_data) { #ifdef DEBUG fprintf(stderr, "Debug: Entering --> consume_gzip_chunk()\n"); #endif pthread_data_t *d = (pthread_data_t *)t_data; long n_in_bytes_written_to_gunzip; pthread_mutex_lock(&d->in_lock); while(kTrue) { while (d->n_in_bytes == 0 && !d->in_eof) pthread_cond_wait(&d->in_cond, &d->in_lock); if (d->n_in_bytes) { #ifdef DEBUG fprintf(stderr, "Debug: ........ [%07zu] processing chunk\n", d->n_in_bytes); #endif if (!d->gunzip_ptr) { #ifdef DEBUG fprintf(stderr, "Debug: * setting up gunzip ptr\n"); #endif d->gunzip_ptr = malloc(sizeof(popen3_desc_t)); if (!d->gunzip_ptr) { fprintf(stderr, "Error: Could not create gunzip file handle struct\n"); exit(EXIT_FAILURE); } popen3("gunzip -c -", &(d->gunzip_ptr->in), &(d->gunzip_ptr->out), &(d->gunzip_ptr->err), kTrue, kTrue); memset(d->in_line, 0, LINE_LENGTH_VALUE); } n_in_bytes_written_to_gunzip = (long) write(d->gunzip_ptr->in, d->in_buf, d->n_in_bytes); #ifdef DEBUG fprintf(stderr, "Debug: ................ wrote [%07ld] bytes into the gunzip process\n", n_in_bytes_written_to_gunzip); #endif if (n_in_bytes_written_to_gunzip > 0) d->n_in_bytes_written_to_gunzip = n_in_bytes_written_to_gunzip; d->n_in_bytes = 0; pthread_cond_signal(&d->out_cond); } if (d->in_eof) break; } pthread_mutex_unlock(&d->in_lock); #ifdef DEBUG fprintf(stderr, "Debug: Leaving --> consume_gzip_chunk()\n"); #endif return NULL; } 函数将gzip write发送到n_bytes进程的输入文件描述符。最后,我们发送另一个线程信号,但这次是in_buf,以帮助重新唤醒gunzip,它从out_cond的输出读取更多工作:

consume_gunzip_chunk_thread

尝试gunzip来自void * consume_gunzip_chunk(void *t_data) { #ifdef DEBUG fprintf(stderr, "Debug: Entering --> consume_gunzip_chunk()\n"); #endif pthread_data_t *d = (pthread_data_t *)t_data; long n_out_bytes_read_from_gunzip; pthread_mutex_lock(&d->in_lock); while(kTrue) { while (d->n_in_bytes_written_to_gunzip == 0) { pthread_cond_wait(&d->out_cond, &d->in_lock); } if (d->n_in_bytes_written_to_gunzip) { sleep(1); n_out_bytes_read_from_gunzip = read(d->gunzip_ptr->out, d->in_line, LINE_LENGTH_VALUE); #ifdef DEBUG fprintf(stderr, "Debug: ------------------------ read [%07ld] bytes out from the gunzip process\n", n_out_bytes_read_from_gunzip); fprintf(stderr, "Debug: ------------------------ gunzip output chunk:\n[%s]\n", d->in_line); #endif memset(d->in_line, 0, strlen(d->in_line)); if (n_out_bytes_read_from_gunzip > 0) d->n_out_bytes_read_from_gunzip = n_out_bytes_read_from_gunzip; d->n_in_bytes_written_to_gunzip = 0; pthread_cond_signal(&d->in_cond); } if (d->in_eof && (d->n_in_bytes_written_to_gunzip == 0)) break; } pthread_mutex_unlock(&d->in_lock); #ifdef DEBUG fprintf(stderr, "Debug: Leaving --> consume_gunzip_chunk()\n"); #endif return NULL; } 进程的输出文件描述符的任何可用字节。出于调试目的,我现在只想将它们打印到read

我遇到的问题是,在gunzip之前,我需要在stderr中添加sleep(1)语句,以便让事情正常运行。

如果没有这个consume_gunzip_chunk语句,我的测试程序通常不输出任何内容 - 除非每次尝试8-10次,否则正确提取压缩数据。

问题 - 对于我的条件安排,我做错了什么,以便read调用是否需要sleep(1) - 提取工作正常?在生产场景中,使用更大的输入文件,强制每1kB等待一秒似乎是一个坏主意。


为了使用完整的源代码进行再现,以下是两个相关文件。这是标题:

sleep(1)

以下是实施:

gzip

以下是构建过程:

/*
 * convert.h
 */

#ifndef CONVERT_H
#define CONVERT_H

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <getopt.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>

#define CB_VERSION "1.0"
#define LINE_LENGTH_VALUE 65536
#define BUF_LENGTH_VALUE 1024
#define POPEN3_READ 0
#define POPEN3_WRITE 1

typedef int boolean;
extern const boolean kTrue;
extern const boolean kFalse;
const boolean kTrue = 1;
const boolean kFalse = 0;

typedef enum {
    kGzip,
    kUnknown
} format_t;

typedef struct pthread_data pthread_data_t;
typedef struct popen3_desc popen3_desc_t;

struct pthread_data {
    pthread_mutex_t in_lock;
    pthread_cond_t in_cond;
    pthread_cond_t out_cond;
    unsigned char in_buf[BUF_LENGTH_VALUE];
    size_t n_in_bytes;
    size_t n_in_bytes_written_to_gunzip;
    size_t n_out_bytes_read_from_gunzip;
    boolean in_eof;
    FILE *in_file_ptr;
    popen3_desc_t *gunzip_ptr;
    char in_line[LINE_LENGTH_VALUE];
};

struct popen3_desc {
    int in;
    int out;
    int err;
};

static const char *name = "convert";
static const char *version = CB_VERSION;
static const char *authors = "Alex Reynolds";
static const char *usage = "\n" \
    "Usage: convert --input-format=str <input-file>\n" \
    "  Process Flags:\n\n" \
    "  --input-format=str            | -f str  Input format (str = [ gzip ]; required)\n" \
    "  --help                        | -h      Show this usage message\n";

static struct convert_globals_t {
    char *input_format_str;
    format_t input_format;
    char **filenames;
    int num_filenames;
} convert_globals;

static struct option convert_client_long_options[] = {
    { "input-format",           required_argument,  NULL,   'f' },
    { "help",               no_argument,        NULL,   'h' },
    { NULL,             no_argument,        NULL,    0  }
}; 

static const char *convert_client_opt_string = "f:h?";

void * consume_gunzip_chunk        (void *t_data);
void * consume_gzip_chunk          (void *t_data);
void * produce_gzip_chunk          (void *t_data);
FILE * new_file_ptr                (const char *in_fn);
void   delete_file_ptr             (FILE **file_ptr);
pid_t  popen3                      (const char *command, 
                                    int *in_desc, 
                                    int *out_desc, 
                                    int *err_desc, 
                                    boolean nonblock_in, 
                                    boolean nonblock_outerr);
off_t  fsize                       (const char *fn);
void   initialize_globals          ();
void   parse_command_line_options  (int argc, 
                                    char **argv);
void   print_usage                 (FILE *stream);

#endif

我已经能够在OS X和Linux主机上使用相当现代的编译环境构建这个测试代码。

提前感谢任何有用的建议!

3 个答案:

答案 0 :(得分:7)

我首先要说的是我觉得pthreads条件和互斥量在这里并不是真的有必要,非阻塞I / O也不是对你描述的问题的最佳反应。

在我看来,你用条件和互斥量版本描述的问题是忘记close()刻苦地管道末端的症状,结果是写入结束文件描述符的副本导致儿童进程stdin泄露(进入该孩子或其他人)的管道活着。

然后,鉴于对应于stdin的阅读结束的写入结尾仍然存在,系统没有给出EOF,而是无限期地阻止。

在您的情况下,您 阻止管道末端文件描述符泄漏给生成的子级(close()的子级端正确fork()次调用在你的popen3()内,虽然忘了close()父端的错误端管道结束了。但是,你并没有阻止所有其他孩子的泄漏!如果您两次调用popen3(),则会阻止将三个描述符集泄漏到子项中,但是当父级仍然拥有它们时,在popen3()的下一次调用发生时,{{1}之后现在有 6 文件描述符要关闭(旧的三个和你刚创建的三个新的一组)。

因此,在您的情况下,您应该在这些管道末端设置close-on-exec标志,因此:

fork()

这是生成6个线程和3个进程的代码,并在内部压缩然后解压缩后将其输入未修改传递给输出。它有效地实现了fcntl(fdIn [PIPEWR], F_SETFD, fcntl(fdIn [PIPEWR], F_GETFD) | FD_CLOEXEC); fcntl(fdOut[PIPERD], F_SETFD, fcntl(fdOut[PIPERD], F_GETFD) | FD_CLOEXEC); fcntl(fdErr[PIPERD], F_SETFD, fcntl(fdErr[PIPERD], F_GETFD) | FD_CLOEXEC); ,其中:

  1. 标准输入由线程gzip -c - | XOR 0x55 | XOR 0x55 | gunzip -c - | cat传送到gzip
  2. srcThrd的输出由线程gzip读取,并提供给线程a2xor0Thrd
  3. 线程xor0Thrd将其输入与xor0Thrd进行异或,然后再将其传递给线程0x55
  4. 线程xor1Thrd将其输入与xor1Thrd进行异或,然后再将其传递给线程0x55
  5. 主题xor22BThrd将其输入提供给流程xor22BThrd
  6. 流程gunzip直接将其输出(不经过线程)提供给gunzip
  7. 流程cat的输出由线程cat读取并打印到标准输出。
  8. 压缩是通过进程间管道通信完成的,而XORing是通过进程内管道通信完成的。不使用互斥锁或条件变量。 dstThrd非常容易理解。此代码应该很容易扩展到您的情况。

    main()

    对您自己的代码的评论

    您的代码存在许多问题,其中大部分都与线程无关。

    • 您不必/* Includes */ #include <stdlib.h> #include <pthread.h> #include <unistd.h> #include <stdio.h> #include <fcntl.h> /* Defines */ #define PIPERD 0 #define PIPEWR 1 /* Data structures */ typedef struct PIPESET{ int Ain[2]; int Aout[2]; int Aerr[2]; int xor0[2]; int xor1[2]; int xor2[2]; int Bin[2]; int BoutCin[2]; int Berr[2]; int Cout[2]; int Cerr[2]; } PIPESET; /* Function Implementations */ /** * Source thread main method. * * Slurps from standard input and feeds process A. */ void* srcThrdMain(void* arg){ PIPESET* pipeset = (PIPESET*)arg; char c; while(read(0, &c, 1) > 0){ write(pipeset->Ain[PIPEWR], &c, 1); } close(pipeset->Ain[PIPEWR]); pthread_exit(NULL); } /** * A to XOR0 thread main method. * * Manually pipes from standard output of process A to input of thread XOR0. */ void* a2xor0ThrdMain(void* arg){ PIPESET* pipeset = (PIPESET*)arg; char buf[65536]; ssize_t bytesRead; while((bytesRead = read(pipeset->Aout[PIPERD], buf, 65536)) > 0){ write(pipeset->xor0[PIPEWR], buf, bytesRead); } close(pipeset->xor0[PIPEWR]); pthread_exit(NULL); } /** * XOR0 thread main method. * * XORs input with 0x55 and outputs to input of XOR1. */ void* xor0ThrdMain(void* arg){ PIPESET* pipeset = (PIPESET*)arg; char c; while(read(pipeset->xor0[PIPERD], &c, 1) > 0){ c ^= 0x55; write(pipeset->xor1[PIPEWR], &c, 1); } close(pipeset->xor1[PIPEWR]); pthread_exit(NULL); } /** * XOR1 thread main method. * * XORs input with 0x55 and outputs to input of process B. */ void* xor1ThrdMain(void* arg){ PIPESET* pipeset = (PIPESET*)arg; char c; while(read(pipeset->xor1[PIPERD], &c, 1) > 0){ c ^= 0x55; write(pipeset->xor2[PIPEWR], &c, 1); } close(pipeset->xor2[PIPEWR]); pthread_exit(NULL); } /** * XOR2 to B thread main method. * * Manually pipes from input (output of XOR1) to input of process B. */ void* xor22BThrdMain(void* arg){ PIPESET* pipeset = (PIPESET*)arg; char buf[65536]; ssize_t bytesRead; while((bytesRead = read(pipeset->xor2[PIPERD], buf, 65536)) > 0){ write(pipeset->Bin[PIPEWR], buf, bytesRead); } close(pipeset->Bin[PIPEWR]); pthread_exit(NULL); } /** * Destination thread main method. * * Manually copies the standard output of process C to the standard output. */ void* dstThrdMain(void* arg){ PIPESET* pipeset = (PIPESET*)arg; char c; while(read(pipeset->Cout[PIPERD], &c, 1) > 0){ write(1, &c, 1); } pthread_exit(NULL); } /** * Set close on exec flag on given descriptor. */ void setCloExecFlag(int fd){ fcntl(fd, F_SETFD, fcntl(fd, F_GETFD) | FD_CLOEXEC); } /** * Set close on exec flag on given descriptor. */ void unsetCloExecFlag(int fd){ fcntl(fd, F_SETFD, fcntl(fd, F_GETFD) & ~FD_CLOEXEC); } /** * Pipe4. * * Create a pipe with some ends possibly marked close-on-exec. */ #define PIPE4_FLAG_NONE (0U) #define PIPE4_FLAG_RD_CLOEXEC (1U << 0) #define PIPE4_FLAG_WR_CLOEXEC (1U << 1) int pipe4(int fd[2], int flags){ int ret = pipe(fd); if(flags&PIPE4_FLAG_RD_CLOEXEC){setCloExecFlag(fd[PIPERD]);} if(flags&PIPE4_FLAG_WR_CLOEXEC){setCloExecFlag(fd[PIPEWR]);} return ret; } /** * Pipe4 explicit derivatives. */ #define pipe4_cloexec(fd) pipe4((fd), PIPE4_FLAG_RD_CLOEXEC|PIPE4_FLAG_WR_CLOEXEC) /** * Popen4. * * General-case for spawning a process and tethering it with cloexec pipes on stdin, * stdout and stderr. * * @param [in] cmd The command to execute. * @param [in/out] pin The pointer to the cloexec pipe for stdin. * @param [in/out] pout The pointer to the cloexec pipe for stdout. * @param [in/out] perr The pointer to the cloexec pipe for stderr. * @param [in] flags A bitwise OR of flags to this function. Available * flags are: * * POPEN4_FLAG_NONE: * Explicitly specify no flags. * POPEN4_FLAG_NOCLOSE_PARENT_STDIN, * POPEN4_FLAG_NOCLOSE_PARENT_STDOUT, * POPEN4_FLAG_NOCLOSE_PARENT_STDERR: * Don't close pin[PIPERD], pout[PIPEWR] and perr[PIPEWR] in the parent, * respectively. * POPEN4_FLAG_CLOSE_CHILD_STDIN, * POPEN4_FLAG_CLOSE_CHILD_STDOUT, * POPEN4_FLAG_CLOSE_CHILD_STDERR: * Close the respective streams in the child. Ignores pin, pout and perr * entirely. Overrides a NOCLOSE_PARENT flag for the same stream. */ #define POPEN4_FLAG_NONE (0U) #define POPEN4_FLAG_NOCLOSE_PARENT_STDIN (1U << 0) #define POPEN4_FLAG_NOCLOSE_PARENT_STDOUT (1U << 1) #define POPEN4_FLAG_NOCLOSE_PARENT_STDERR (1U << 2) #define POPEN4_FLAG_CLOSE_CHILD_STDIN (1U << 3) #define POPEN4_FLAG_CLOSE_CHILD_STDOUT (1U << 4) #define POPEN4_FLAG_CLOSE_CHILD_STDERR (1U << 5) pid_t popen4(const char* cmd, int pin[2], int pout[2], int perr[2], int flags){ /******************** ** FORK PROCESS ** ********************/ pid_t ret = fork(); if(ret < 0){ /** * Error in fork(), still in parent. */ fprintf(stderr, "fork() failed!\n"); return ret; }else if(ret == 0){ /** * Child-side of fork */ if(flags & POPEN4_FLAG_CLOSE_CHILD_STDIN){ close(0); }else{ unsetCloExecFlag(pin [PIPERD]); dup2(pin [PIPERD], 0); } if(flags & POPEN4_FLAG_CLOSE_CHILD_STDOUT){ close(1); }else{ unsetCloExecFlag(pout[PIPEWR]); dup2(pout[PIPEWR], 1); } if(flags & POPEN4_FLAG_CLOSE_CHILD_STDERR){ close(2); }else{ unsetCloExecFlag(perr[PIPEWR]); dup2(perr[PIPEWR], 2); } execl("/bin/sh", "sh", "-c", cmd, NULL); fprintf(stderr, "exec() failed!\n"); exit(-1); }else{ /** * Parent-side of fork */ if(~flags & POPEN4_FLAG_NOCLOSE_PARENT_STDIN && ~flags & POPEN4_FLAG_CLOSE_CHILD_STDIN){ close(pin [PIPERD]); } if(~flags & POPEN4_FLAG_NOCLOSE_PARENT_STDOUT && ~flags & POPEN4_FLAG_CLOSE_CHILD_STDOUT){ close(pout[PIPEWR]); } if(~flags & POPEN4_FLAG_NOCLOSE_PARENT_STDERR && ~flags & POPEN4_FLAG_CLOSE_CHILD_STDERR){ close(perr[PIPEWR]); } return ret; } /* Unreachable */ return ret; } /** * Main Function. * * Sets up the whole piping scheme. */ int main(int argc, char* argv[]){ pthread_t srcThrd, a2xor0Thrd, xor0Thrd, xor1Thrd, xor22BThrd, dstThrd; pid_t gzip, gunzip, cat; PIPESET pipeset; pipe4_cloexec(pipeset.Ain); pipe4_cloexec(pipeset.Aout); pipe4_cloexec(pipeset.Aerr); pipe4_cloexec(pipeset.Bin); pipe4_cloexec(pipeset.BoutCin); pipe4_cloexec(pipeset.Berr); pipe4_cloexec(pipeset.Cout); pipe4_cloexec(pipeset.Cerr); pipe4_cloexec(pipeset.xor0); pipe4_cloexec(pipeset.xor1); pipe4_cloexec(pipeset.xor2); /* Spawn processes */ gzip = popen4("gzip -c -", pipeset.Ain, pipeset.Aout, pipeset.Aerr, POPEN4_FLAG_NONE); gunzip = popen4("gunzip -c -", pipeset.Bin, pipeset.BoutCin, pipeset.Berr, POPEN4_FLAG_NONE); cat = popen4("cat", pipeset.BoutCin, pipeset.Cout, pipeset.Cerr, POPEN4_FLAG_NONE); /* Spawn threads */ pthread_create(&srcThrd, NULL, srcThrdMain, &pipeset); pthread_create(&a2xor0Thrd, NULL, a2xor0ThrdMain, &pipeset); pthread_create(&xor0Thrd, NULL, xor0ThrdMain, &pipeset); pthread_create(&xor1Thrd, NULL, xor1ThrdMain, &pipeset); pthread_create(&xor22BThrd, NULL, xor22BThrdMain, &pipeset); pthread_create(&dstThrd, NULL, dstThrdMain, &pipeset); pthread_join(srcThrd, (void**)NULL); pthread_join(a2xor0Thrd, (void**)NULL); pthread_join(xor0Thrd, (void**)NULL); pthread_join(xor1Thrd, (void**)NULL); pthread_join(xor22BThrd, (void**)NULL); pthread_join(dstThrd, (void**)NULL); return 0; } 文件描述符close()。这意味着d->gunzip_ptr->in永远不会知道其gunzip上不会再有任何输入,因此它永远不会退出。
    • 由于stdin没有退出,因此它永远不会gunzip它的标准输出,因此另一端的阻止close()将永远不会取消阻止。相反,非阻止性读取将始终为read()提供-1
    • errno == EAGAIN的父popen3() close() p_stdin[POPEN3_READ] p_stdout[POPEN3_WRITE]p_stderr[POPEN3_WRITE]fork()。只有孩子应该有这些描述符。未能关闭这些意味着当父本身试图读取子节点的stdout和stderr时,它将永远不会看到EOF,原因与上面相同:因为它本身仍然拥有一个写端管道,它可以在其中写入,使新数据显示在读取端。
    • 您的代码隐含地依赖gunzip为您写入的每1024个写出至少一个字节。无法保证会出现这种情况,因为gunzip可能会在其闲暇时缓冲内部。
      • 这是因为您的代码读取后会将最多BUF_LENGTH_VALUE个字节的块复制到d->in_buf。然后,将通过fread()读取的字节数分配给d->n_in_bytes。您的d->n_in_bytes调用中使用了相同的write()来写入gunzip的标准符号。然后,您发信号通知consume_gunzip_chunk()唤醒,然后pthread_cond_wait()发出下一个gzip压缩块的信号。但是这个gzip压缩的块可能永远不会出现,因为不能保证gunzip能够从输入的前1024个字节解压缩有用的输出,甚至不能保证它write()它而不是缓冲它,直到它有4096字节(整页)的输出。因此,read()中的consume_gunzip_chunk()来电可能永远不会成功(如果read()阻止,则甚至会返回)。如果read()永远不会返回,则consume_gunzip_chunk()不会发出d->in_cond信号,因此所有三个线程都会被卡住。即使read()是非阻塞的,gzip的最后一个输出块也可能永远不会出现,因为gzip的输入永远不会被关闭,所以它不会清除它缓冲区write()将它们排除在外,因此另一端的read()永远不会获得有用的数据,如果没有close(),任何数量的请求都不会引发它。
    • 可能(可能?)BUG原因d->n_out_bytes_read_from_gunzip一旦变为非0,将永远不会再次成为0。这意味着极其莫名其妙

      while (d->n_in_bytes != 0 || d->n_out_bytes_read_from_gunzip != 0)
          pthread_cond_wait(&d->in_cond, &d->in_lock);
      
      produce_gzip_chunk()内的

      一旦与d->n_out_bytes_read_from_gunzip != 0一起输入,将永远停滞不前。通过调用设置sleep(1)的{​​{1}}中的consume_gunzip_chunk(),您可以通过设置d->n_out_bytes_read_from_gunzip consume_gunzip_chunk()锁定系统之前读取所有输入来解决问题到非零值。

    • 两个线程调用d->n_out_bytes_read_from_gunzip,这些线程为pthread_cond_wait(&d->in_cond, &d->in_lock);produce_gzip_chunk()。绝对不能保证在consume_gzip_chunk()调用consume_gunzip_chunk()时,&#34;正确&#34;线程(无论你的设计中的哪一个)都会收到信号。为了确保所有这些都使用pthread_cond_signal(&d->in_cond);,然后您将自己暴露给thundering herd problem。在这种情况下,需要使用pthread_cond_broadcast()再次是我认为设计糟糕的症状。
    • 相关,您在调用pthread_cond_broadcast()的线程(实际上是函数)中调用pthread_cond_signal(&d->in_cond)。这有什么用途?
    • 您使用pthread_cond_wait(&d->in_cond, &d->in_lock)用于太多不同的目的,使自己暴露于死锁的可能性,或由于过度保护而导致性能低下。特别是,您可以将其用作d->in_lockd->in_cond的保护。这是一个过于强大的保护 - d->out_condgunzip的输出应该能够与d->in_line输入和输出gunzip同时发生。
    • d->in_buf内,您有

      consume_gunzip_chunk()

      while (d->n_in_bytes_written_to_gunzip == 0) { pthread_cond_wait(&d->out_cond, &d->in_lock); } if (d->n_in_bytes_written_to_gunzip) { ... 永远不会失败!你有没有想过的案例?

    • 考虑制作整个if volatile(或至少是多个线程使用的那些整数元素),因为编译器可能会决定优化实际应该保留的加载和存储。

    称赞

    为了听起来不太消极,我想说一般来说你的问题不是因为滥用pthreads API而是因为错误的消费者 - 生产者逻辑和缺乏struct pthread_data s。此外,您似乎明白close()可能会唤醒spuriously,因此您将其包装在一个检查不变量的循环中。

    将来:

    我会使用管道,甚至在线程之间。这使您无需实施自己的消费者 - 生产者计划;内核已经为您解决了这个问题,并为您提供了pthread_cond_wait()pipe()read()原语,这些都是您利用这个现成解决方案所需的全部内容。它还使代码更清晰,没有互斥锁和条件变量。一个人必须勤奋地关闭两端,并且必须在write()面前对管道进行极其小心。规则很简单:

    • 如果存在管道的写入端,则打开的读取端上的fork()将不会提供EOF,但会阻止或read()
    • 如果管道的写入端都已关闭,则打开的读取端上的EAGAIN将提供EOF。
    • 如果管道的阅读端已全部关闭,则read()任何一个写入端都将导致write()
    • SIGPIPE重复整个过程,包括所有描述符(模数可能是fork()中的疯狂内容)!

答案 1 :(得分:4)

阿。所以我想我误解了这个问题......抱歉。

我原本以为你想要使用gunzip然后另外一个内部过滤器,并希望这样做&#39; N&#39;次。

看起来你真正想要做的是运行多个过滤器阶段,一个接一个地使用...一些使用外部命令,一些(可能是?)程序内部。因此,需要管理一些阶段间缓冲。

所以......我还有另外一个去做。目标是运行任意数量的阶段,从输入阶段开始,然后是extrenal命令或内部功能&#34; filter&#34;阶段,最后是输出阶段。每个外部命令阶段都有三个pthreads - 用于stdin,stdout和stderr。内部函数阶段使用一个pthread,初始输入和最终输出各一个pthread。阶段之间是一个小管道结构(称为&#34;稻草&#34;)到&#34;双缓冲区&#34;并且将各个阶段分离......我希望这更接近你的想法。

&#34;稻草&#34;是事物的本质:

struct straw
{
  pthread_mutex_t*  mutex ;

  struct lump*  free ;
  pthread_cond_t* free_cond ;
  bool          free_waiting ;

  struct lump*  ready ;
  pthread_cond_t* ready_cond ;
  bool          ready_waiting ;

  struct lump*  lumps[2] ;
} ;

其中struct lump包含缓冲区和什么不包含缓冲区。 &#34;稻草&#34;有两个这样的&#34;肿块&#34;,并且在任何时刻一个pthread可能填充一个肿块,而另一个pthread可能正在填充另一个。或者两个肿块都可以是免费的(在免费清单上)或两者都准备好(全部)等待就绪清单。

然后获取一个空的块来填充它(例如从管道读取时):

static struct lump*
lump_acquire(struct straw* strw)
{
  struct lump* lmp ;

  pthread_mutex_lock(strw->mutex) ;

  while (strw->free == NULL)
    {
      strw->free_waiting = true ;
      pthread_cond_wait(strw->free_cond, strw->mutex) ;
      strw->free_waiting = false ;
    } ;

  lmp = strw->free ;
  strw->free = lmp->next ;

  pthread_mutex_unlock(strw->mutex) ;

  lmp->next = NULL ;                    /* tidy                 */

  lmp->ptr  = lmp->end = lmp->buff ;    /* empty                */
  lmp->done = false ;

  return lmp ;
} ;

然后将完成的肿块吹入吸管的一端(

static void
lump_blow(struct lump* lmp)
{
  struct straw* strw ;

  strw = lmp->strw ;

  qassert((lmp == strw->lumps[0]) || (lmp == strw->lumps[1])) ;
  qassert( (lmp->buff <= lmp->ptr)
                     && (lmp->ptr <= lmp->end)
                                 && (lmp->end <= lmp->limit) ) ;

  lmp->ptr = lmp->buff ;

  pthread_mutex_lock(strw->mutex) ;

  if (strw->ready == NULL)
    strw->ready = lmp ;
  else
    strw->ready->next = lmp ;
  lmp->next = NULL ;

  if (strw->ready_waiting)
    pthread_cond_signal(strw->ready_cond) ;

  pthread_mutex_unlock(strw->mutex) ;
} ;

从吸管的另一端吸出一个肿块:

static struct lump*
lump_suck(struct straw* strw)
{
  struct lump* lmp ;

  pthread_mutex_lock(strw->mutex) ;

  while (strw->ready == NULL)
    {
      strw->ready_waiting = true ;
      pthread_cond_wait(strw->ready_cond, strw->mutex) ;
      strw->ready_waiting = false ;
    } ;

  lmp = strw->ready ;
  strw->ready = lmp->next ;

  pthread_mutex_unlock(strw->mutex) ;

  qassert( (lmp->buff <= lmp->ptr)
                     && (lmp->ptr <= lmp->end)
                                 && (lmp->end <= lmp->limit) ) ;

  lmp->ptr = lmp->buff ;        /* lmp->ptr..lmp->end   */
  lmp->next = NULL ;            /* tidy                 */

  return lmp ;
} ;

最后一块,一旦它被排干就释放出一块:

static void
lump_free(struct lump* lmp)
{
  struct straw* strw ;

  strw = lmp->strw ;

  qassert((lmp == strw->lumps[0]) || (lmp == strw->lumps[1])) ;
  qassert( (lmp->buff <= lmp->ptr)
                     && (lmp->ptr <= lmp->end)
                                 && (lmp->end <= lmp->limit) ) ;

  pthread_mutex_lock(strw->mutex) ;

  if (strw->free == NULL)
    strw->free = lmp ;
  else
    strw->free->next = lmp ;

  lmp->next = NULL ;                    /* end of list of free  */

  lmp->ptr  = lmp->end = lmp->buff ;    /* empty                */
  lmp->done = false ;

  if (strw->free_waiting)
    pthread_cond_signal(strw->free_cond) ;

  pthread_mutex_unlock(strw->mutex) ;
} ;

整个程序太大而无法回答 - 请参阅:pipework.c从哪里开始:

/*==============================================================================
 * pipework.c
 *
 * Copyright (c) Chris Hall (GMCH) 2014, All rights reserved.
 *
 * Though you may do what you like with this, provided you recognise that
 * it is offered "as is", gratis, and may or may not be fit for any purpose
 * whatsoever -- you are on your own.
 *
 *------------------------------------------------------------------------------
 *
 * This will read from stdin, pass the data through an arbitrary number of
 * "filter" stages and finally write the result to stdout.
 *
 * A filter stage may be an external command taking a piped stdin and
 * outputting to a piped stdout.  Anything it says to stderr is collected
 * and output to the program's stderr.
 *
 * A filter stage may also be an internal function.
 *
 * The input, filter and output stages are implemented as a number of pthreads,
 * with internal, miniature pipes (called "straws") between them.  All I/O is
 * blocking.  This is an experiment in the use of pthreads to simplify things.
 *
 * ============================
 * This is v0.08 of  4-Jul-2014
 * ============================
 *
 * The 'main' below runs eight stages: input, 4 commands, 2 internal filters
 * and the output.  The output should be an exact copy of the input.
 *
 * In order to test the stderr handling, the following small perl script is
 * used as two of the command filters:
 *
 *   chatter.pl
 *   --------------------------------------------------------
     use strict ;
     use warnings ;

     my $line = 0 ;
     while (<STDIN>)
       {
         my $len = length($_) ;
         my $r   = rand ;

         $line += 1 ;
         print STDERR "|$line:$len:$r|" ;

         if (int($r * 100) == 0)
           {
             print STDERR "\n" ;
           } ;

         print $_ ;
       } ;
 *   --------------------------------------------------------
 *
 *------------------------------------------------------------------------------
 * Limitations
 *
 *   * this will crash if it gets some error its not expecting or not
 *     designed to overcome.  Clearly, to be useful this needs to be more
 *     robust and more informative.
 *
 *   * some (possible/theoretical) errors are simply ignored.
 *
 *   * no attempt is made to collect up the child processes or to discover
 *     their return codes.  If the child process reports errors or anything
 *     else on stderr, then that will be visible.  But otherwise, if it just
 *     crashes then the pipeline will run to completion, but the result may
 *     be nonsense.
 *
 *   * if one of the child processes stalls, the whole thing stalls.
 *
 *   * an I/O error in a stage will send 'end' downstream, but the program
 *     will continue until everything upstream has completed.
 *
 *   * generally... not intended for production use !!
 */

perl脚本可用:chatter.pl

HTH

答案 2 :(得分:2)

关于“如何通过pthreads管理两个或更多消费者?”部分你的帖子让我引用关于'Designing Threaded Programs'的这些观点:

  

一般情况下,为了让程序利用   Pthreads,它必须能够组织成离散的,独立的   可以并发执行的任务。例如,如果routine1和   routine2可以实际互换,交错和/或重叠   时间,他们是线程的候选人。

  

存在几种用于线程程序的常见模型:

     
      
  • 经理/工人:一个线程,经理将工作分配给其他线程,工人。通常,经理处理所有输入和   包裹出去完成其他任务。至少有两种形式的   经理/工人模型很常见:静态工作者池和动态
      工人池。
  •   
  • 管道:任务分为一系列子操作,每个子操作都是串行处理,但同时由不同的线程处理。
      汽车装配线最能描述这种模型。
  •   
  • Peer:类似于经理/工人模型,但在主线程创建其他线程后,它参与了工作。
  •   

关于你的问题...

  

我面临的问题是我需要添加sleep(1)语句   consume_gunzip_chunk,在执行读操作之前,为了得到东西   工作正常。

Eric Lippert Best Practices with Multithreading in C#可能无法解决,但是,它们应该可以帮助您找到适合您的多线程程序的解决方案,尤其是第5点和第8点:

  

5.所有费用都避免共享内存。大多数线程错误都是由于无法理解现实世界的共享内存语义造成的。如果你   必须制作线程,将它们视为进程:给予它们   他们完成工作所需的一切,让他们无需工作   修改与任何其他线程关联的内存。就像一个   进程无法修改任何其他进程的内存。

     

8.如果在任何生产代码中使用带有非零或一个参数的Thread.Sleep,那么您可能做错了什么。主题   很贵;你不付工人睡觉,所以不要付钱   也要睡觉。如果您正在使用睡眠来解决正确性问题   通过避免计时问题 - 因为您似乎在您的代码中 -   那你绝对做错了。多线程   无论时间安排如何,代码都必须正确。