jpeg_write_scanlines和glTexImage2D线程安全。为什么不崩溃?

jpeg_write_scanlines and glTexImage2D thread safety. Why doesn't this crash?

本文关键字:为什么不 安全 崩溃 glTexImage2D write scanlines jpeg 线程      更新时间:2023-10-16

我正在制作一个视频软件,并使用一些现有的代码。现有的代码包括一个循环缓冲区。作为制作人,我有摄像机,作为消费者,我有两条不同的线。一个是GLThread,使用OpenGL绘制帧,另一个是VideoCompressorThread,将帧压缩为jpeg格式保存到视频文件中。奇怪的是,目前这两个线程同时处理相同的数据,但这不会产生竞争条件。在GLThread中我有:

while(!shouldStop) {
        mutex_.lock();
        glw_->makeCurrent();
        shaderProgram_.bind();
        shaderProgram_.setUniformValue("texture", 0);
        shaderProgram_.setAttributeArray("vertex", vertices_.constData());
        shaderProgram_.enableAttributeArray("vertex");
        shaderProgram_.setAttributeArray("textureCoordinate", textureCoordinates_.constData());
        shaderProgram_.enableAttributeArray("textureCoordinate");
        qDebug() << "GLThread: " << "data address: " << static_cast<void*>(imBuf_)  << "time: " << QDateTime::currentMSecsSinceEpoch();
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB8, VIDEO_WIDTH, VIDEO_HEIGHT, 0, GL_RGB, GL_UNSIGNED_BYTE, (GLubyte*)imBuf_);
        qDebug() << "GLThread finished";
        glClear(GL_COLOR_BUFFER_BIT);
        glDrawArrays(GL_TRIANGLES, 0, 6);
        glw_->swapBuffers();
        shaderProgram_.disableAttributeArray("vertex");
        shaderProgram_.disableAttributeArray("textureCoordinate");
        shaderProgram_.release();
        glw_->doneCurrent();
        mutex_.unlock();
}

和VideoCompressorThread:

while(!shouldStop)
{
    // JPEG-related stuff
    struct jpeg_compress_struct cinfo;
    struct jpeg_error_mgr       jerr;
    JSAMPROW                    row_pointer;
    unsigned char*              jpgBuf=NULL;
    unsigned long               jpgBufLen=0;
    unsigned char*              data;
    ChunkAttrib                 chunkAttrib;
    // Get raw image from the input buffer
    data = inpBuf->getChunk(&chunkAttrib);
    // Initialize JPEG
    cinfo.err = jpeg_std_error(&jerr);
    jpeg_create_compress(&cinfo);
    jpeg_mem_dest(&cinfo, &jpgBuf, &jpgBufLen);
    // Set the parameters of the output file
    cinfo.image_width = VIDEO_WIDTH;
    cinfo.image_height = VIDEO_HEIGHT;
    cinfo.input_components = 3;
    cinfo.in_color_space = JCS_RGB;
    // Use default compression parameters
    jpeg_set_defaults(&cinfo);
    jpeg_set_quality(&cinfo, jpgQuality, TRUE);
    // Do the compression
    jpeg_start_compress(&cinfo, TRUE);
    // write one row at a time
    qDebug() << "VideoCompressorThread: " << "data address: " << static_cast<void*>(data) << "time: " << QDateTime::currentMSecsSinceEpoch();
    while(cinfo.next_scanline < cinfo.image_height)
    {
        row_pointer = (data + (cinfo.next_scanline * cinfo.image_width * 3));
        jpeg_write_scanlines(&cinfo, &row_pointer, 1);
    }
    qDebug() << "VideoCompressorThread finished";
    // clean up after we're done compressing
    jpeg_finish_compress(&cinfo);

    // Insert compressed image into the output buffer
    chunkAttrib.chunkSize = jpgBufLen;
    outBuf->insertChunk(jpgBuf, chunkAttrib);
    // The output buffer needs to be explicitly freed by the libjpeg client
    free(jpgBuf);
    jpeg_destroy_compress(&cinfo);
}

作为输出,我得到:

VideoCompressorThread:  data address:  0x7fffbdcd1060 time:  1438594694479 
VideoCompressorThread finished 
GLThread:  data address:  0x7fffbdcd1060 time:  1438594694488 
GLThread finished 
GLThread:  data address:  0x7fffbddb20b0 time:  1438594694497 
GLThread finished 
VideoCompressorThread:  data address:  0x7fffbddb20b0 time:  1438594694498 
VideoCompressorThread finished 
VideoCompressorThread:  data address:  0x7fffbde93100 time:  1438594694521 
GLThread:  data address:  0x7fffbde93100 time:  1438594694521 
GLThread finished 
VideoCompressorThread finished 
VideoCompressorThread:  data address:  0x7fffbdf74150 time:  1438594694538 
GLThread:  data address:  0x7fffbdf74150 time:  1438594694538 
GLThread finished 
VideoCompressorThread finished 
VideoCompressorThread:  data address:  0x7fffbe0551a0 time:  1438594694555 
GLThread:  data address:  0x7fffbe0551a0 time:  1438594694555 
GLThread finished 
VideoCompressorThread finished 
VideoCompressorThread:  data address:  0x7fffbe1361f0 time:  1438594694571 
GLThread:  data address:  0x7fffbe1361f0 time:  1438594694571 
GLThread finished 
VideoCompressorThread finished 
VideoCompressorThread:  data address:  0x7fffbe217240 time:  1438594694588 
GLThread:  data address:  0x7fffbe217240 time:  1438594694588 
GLThread finished 
VideoCompressorThread finished 
VideoCompressorThread:  data address:  0x7fffbe2f8290 time:  1438594694604 
GLThread:  data address:  0x7fffbe2f8290 time:  1438594694604 
GLThread finished 
VideoCompressorThread finished 

可以看到,有时两个线程同时访问相同的数据,但没有崩溃。这是纯粹的运气,还是有什么我不明白的地方?我用的是Xubuntu 14.04,如果这有什么不同的话。

编辑。insertChunk和getChunk()函数。注意,只有VideoCompressorThread使用getChunk()获取数据指针。GLThread连接到chunkReady qt信号。这允许为缓冲区使用一个主消费者和多个辅助消费者。

void CycDataBuffer::insertChunk(unsigned char* _data, ChunkAttrib &_attrib)
{
    // Check for buffer overflow. CIRC_BUF_MARG is the safety margin against
    // race condition between consumer and producer threads when the buffer
    // is close to full.
    if (buffSemaphore->available() >=  bufSize * (1-CIRC_BUF_MARG))
    {
        cerr << "Circular buffer overflow!" << endl;
        abort();
    }
    // Make sure that the safety margin is at least several (four) times the
    // chunk size. This is necessary to prevent the race condition between
    // consumer and producer threads when the buffer is close to full.
    if(_attrib.chunkSize+sizeof(ChunkAttrib)+MAXLOG > bufSize*MAX_CHUNK_SIZE)
    {
        cerr << "The chunk size is too large!" << endl;
        abort();
    }
    // insert the data into the circular buffer
    _attrib.isRec = isRec;
    memcpy(dataBuf + insertPtr, (unsigned char*)(&_attrib), sizeof(ChunkAttrib));
    insertPtr += sizeof(ChunkAttrib);
    buffSemaphore->release(sizeof(ChunkAttrib));
    memcpy(dataBuf + insertPtr, _data, _attrib.chunkSize);
    buffSemaphore->release(_attrib.chunkSize);
    emit chunkReady(dataBuf + insertPtr);
    insertPtr += _attrib.chunkSize;
    if(insertPtr >= bufSize)
    {
        insertPtr = 0;
    }
}
unsigned char* CycDataBuffer::getChunk(ChunkAttrib* _attrib)
{
    unsigned char* res;
    buffSemaphore->acquire(sizeof(ChunkAttrib));
    memcpy((unsigned char*)_attrib, dataBuf + getPtr, sizeof(ChunkAttrib));
    getPtr += sizeof(ChunkAttrib);
    buffSemaphore->acquire(_attrib->chunkSize);
    res = dataBuf + getPtr;
    getPtr += _attrib->chunkSize;
    if(getPtr >= bufSize)
    {
        getPtr = 0;
    }
    return(res);
}

仅仅因为它没有崩溃并不意味着它不是一个bug。当另一个线程正在读取时写入缓冲区通常会导致数据损坏被读线程读取。一些字节读取为新值,一些读取为旧值。

您将看到发生的一件事是,当另一个线程正在处理它时,图像缓冲区的部分被覆盖,这将导致观看视频时屏幕撕裂。当对角线条纹在屏幕上快速移动时,你可以看到这一点。

两个线程读同一个缓冲区是完全没问题的,当一个线程开始写时,问题就开始了。

除了棘轮怪物的好回答"只是因为它没有崩溃并不意味着它不是一个bug",我想补充的是,我实际上没有看到这两个特定的代码片段不能并行工作的理由。两者都只读访问相同的图像数据,这是非常好的。

只有当你有至少两个线程(或进程,在共享内存的情况下)访问同一个缓冲区,并且其中至少一个正在修改它,即通过覆盖数据,或通过取消分配缓冲区时,才会出现并发访问缓冲区的问题。