求助:解决obbfile->getFileData()线程不安全的问题

引擎版本:3.13.1

###资源预加载场景中,异步加载obbfile中的资源闪退,目前已经定位到原因:(多线程访问竞争资源)

  1. 线程1开始读取battleUI.png;
  2. 线程2开始读取zd_diaoxue.png,但此时线程1并未执行结束;
  3. 竞争资源被修改,导致读取中断,触发断言,程序中断
    如下图所示:(打印了多线程执行的顺序)

###罗列相关的代码段

代码1:CCFileUtils-android.cpp

    if (obbfile)
    {
        if (obbfile->getFileData(relativePath, buffer))
            return FileUtils::Status::OK;
    }

代码2:ZipUtils.cpp

bool ZipFile::getFileData(const std::string &fileName, ResizableBuffer* buffer)
{
    bool res = false;
    do
    {
        CC_BREAK_IF(!_data->zipFile);
        CC_BREAK_IF(fileName.empty());

        CCLOG("Enter thread: %d, fileName: %s", std::this_thread::get_id(), fileName.c_str());

        ZipFilePrivate::FileListContainer::const_iterator it = _data->fileList.find(fileName);
        CC_BREAK_IF(it ==  _data->fileList.end());
        
        ZipEntryInfo fileInfo = it->second;

        int nRet = unzGoToFilePos(_data->zipFile, &fileInfo.pos);
        CC_BREAK_IF(UNZ_OK != nRet);
        
        nRet = unzOpenCurrentFile(_data->zipFile);
        CC_BREAK_IF(UNZ_OK != nRet);

        CCLOG("Enter thread: %d, fileName: %s, fileInfo.uncompressed_size: %d", std::this_thread::get_id(), fileName.c_str(), (int)fileInfo.uncompressed_size);

        buffer->resize(fileInfo.uncompressed_size);
        int CC_UNUSED nSize = unzReadCurrentFile(_data->zipFile, buffer->buffer(), static_cast<unsigned int>(fileInfo.uncompressed_size));
        
        CCLOG("Enter thread: %d, fileName: %s, nSize: %d", std::this_thread::get_id(), fileName.c_str(), (int)nSize);
        CCLOG("Leave thread: %d, fileName: %s", std::this_thread::get_id(), fileName.c_str());
        CCASSERT(nSize == 0 || nSize == (int)fileInfo.uncompressed_size, "the file size is wrong");
        unzCloseCurrentFile(_data->zipFile);
        res = true;
    } while (0);
    
    return res;
}

需要加锁,参考: https://github.com/halx99/x-studio365/blob/master/cocos2d-x-patch/cocos/base/ZipUtils.cpp

1赞

非常感谢~
完美的解决了这个问题!

我觉得加锁可能还是会有问题:

  • 加锁只能保证读取数据没有问题,但是读取数据后是否会调用到 opengl 代码?如果是的话,那么多个线程调用 opengl 代码也会有问题。
  • 加锁锁会导致所有的代码都执行加锁操作,这个对于非多线程环境时个消耗。而在 android,读取资源会不断用到这个函数。
  • 引擎的代码没有线程安全的保证。部分线程安全,部分不安全也会让逻辑变得不统一,比较混乱。

这里的异步加载逻辑是怎样的?

通过查看源码确认 lite 是支持 OBB 的,代码和 -x 也没什么区别,问题不是不支持 OBB 引起的。问题是开发者在不同线程中调用引擎代码导致出问题。引擎的代码不是线程安全的。如果把引擎的部分代码改成线程安全也是没有意义的,而且也会降低引擎的性能。
因此该问题不建议修改,开发者如果要在多线程使用的话,自己去定制引擎吧。

目前是使用std::mutex互斥量来解决数据共享的问题:

#include <mutex>

static std::mutex obb_mutex;

FileUtils::Status FileUtilsAndroid::getContents(const std::string& filename, ResizableBuffer* buffer)
{
    // ......

    if (obbfile)
    {
        std::lock_guard<std::mutex> lock(obb_mutex);
        if (obbfile->getFileData(relativePath, buffer))
            return FileUtils::Status::OK;
    }

    // ......
}

我的使用场景是loading页面,异步加载资源:
cc.Director:getInstance():getTextureCache():addImageAsync()

估计你以为是开发者自己在自己逻辑起的多线中调用了非线程安全的接口。

实际上只要开启 obbfile,引擎本身的接口也会在多线程中调用 obbfile 的 getFileData。

例如下面的 initWithImageFile 引擎本身就会有可能在多线程中调用,里面的注释也写的很清楚了。
说明之前一直都是需要 FileUtils::getInstance()->getDataFromFile(_filePath) 是线程安全的(_filePath 是全路径的情况下)。

现在,当 obbfile 开启,getDataFromFile 本身就不是线程安全的。

引擎在多线程环境下调 getDataFromFile 的地方还挺多的

bool Image::initWithImageFile(const std::string& path)
{
    bool ret = false;
    //NOTE: fullPathForFilename isn't threadsafe. we should make sure the parameter is a full path.
//    _filePath = FileUtils::getInstance()->fullPathForFilename(path);
    _filePath = path;

    Data data = FileUtils::getInstance()->getDataFromFile(_filePath);

    if (!data.isNull())
    {
        ret = initWithImageData(data.getBytes(), data.getSize());
    }

    return ret;
}