1. 引言
在使用GDAL库对TIF文件进行切割和创建瓦片金字塔时,为了提高创建效率,不得不考虑使用多线程处理。然而,在实际实现过程中,我遇到了许多问题。通过不断的尝试和优化,最终找到了有效的解决方案。本文将详细记录这一过程中的问题和解决方法。
2. 初始多线程尝试与问题
2.1 常规多线程实现
最初,我尝试使用常规的多线程方法来处理TIF文件切割,但很快发现这一方法导致了大量错误:
ERROR1: GF2-035009809.tif,band 2: IReadBlock failed at X offset 0, Y offset 4373,
ERROR1: GetBlockRef failed at X block offset 0, Y block offset 4373,
ERROR1: TIFFFillStrip: Read error at scanline 2; got 0 bytes, expected 18438,
ERROR1: RIFFReadEncodedStrip() failed.
随着线程数量的增加,这些错误发生的次数也随之增加,有时甚至可以超过几千次。
2.2 问题分析
分析这些错误后,我发现其根本原因在于多线程环境下GDAL库访问资源时可能发生冲突。具体来说,当多个线程同时读取某一位置的影像块时,会引发冲突,因为每个像素在同一时刻只能由一个线程读取。
3. 改进方案:使用互斥锁
3.1 使用互斥锁保护资源
为了解决上述问题,我决定使用互斥锁来保护共享资源,从而确保线程安全。
3.2 核心代码
以下是使用互斥锁的核心代码:
//与多线程无关代码已删除
void run() override
{
// 获取信号量, 控制并发数,限制同时运行的线程数量
semaphore->acquire();
// 使用互斥锁保护共享资源,确保线程安全
QMutexLocker locker(mutex);
// 创建GeoTIFF驱动并创建目标数据集
GDALDriver *driver = GetGDALDriverManager()->GetDriverByName("GTiff");
GDALDataset *dstDataset = driver->Create(outputFile.toUtf8().constData(), 256, 256, srcDataset->GetRasterCount(), GDT_Byte, NULL);
// 缓冲区
std::vector<uint8_t> buffer(256 * 256);
//srcDataset为源数据集
// 对每个波段进行处理
for (int band = 1; band <= srcDataset->GetRasterCount(); ++band) {
GDALRasterBand *srcBand = srcDataset->GetRasterBand(band);
GDALRasterBand *dstBand = dstDataset->GetRasterBand(band);
// 读取源数据并写入目标数据集
srcBand->RasterIO(GF_Read, srcX, srcY, tileWidth, tileHeight, buffer.data(), 256, 256, GDT_Byte, 0, 0);
dstBand->RasterIO(GF_Write, 0, 0, 256, 256, buffer.data(), 256, 256, GDT_Byte, 0, 0);
}
// 关闭目标数据集
GDALClose(dstDataset);
// 解锁互斥锁
locker.unlock();
// 释放信号量,允许其他线程继续执行
semaphore->release();
}
3.3 结果分析
尽管使用互斥锁成功解决了线程冲突问题,但测试发现,处理效率却有所下降。这可能是因为:
- 代码的主要耗时操作在I/O操作中。
- 使用互斥锁后,实际上仍然是单线程I/O。
- 线程切换带来了额外的开销。
通过任务管理器观察,发现进程的CPU、内存以及磁盘的利用率都不高。
同时通过查阅一些资料,发现gdal库好像并不支持多线程读取
4. 改进方案:独立数据集
4.1 独立数据集实现
由于之前所有线程共用一个数据集,需要使用互斥锁保护资源,导致处理效率不高。因此,我尝试为每个线程创建一个独立的数据集,从而避免使用互斥锁。
4.2 核心代码
以下是为每个线程创建独立数据集的核心代码:
void run() override
{
semaphore->acquire();
// 为每个线程创建独立的数据集副本
GDALDataset *srcDataset = (GDALDataset*)GDALOpen(inputFile.toUtf8().constData(), GA_ReadOnly);
if (!srcDataset) {
semaphore->release();
return;
}
// 创建GeoTIFF驱动并创建目标数据集
GDALDriver *driver = GetGDALDriverManager()->GetDriverByName("GTiff");
GDALDataset *dstDataset = driver->Create(outputFile.toUtf8().constData(), 256, 256, srcDataset->GetRasterCount(), GDT_Byte, NULL);
std::vector<uint8_t> buffer(256* 256);
for (int band = 1; band <= srcDataset->GetRasterCount(); ++band) {
GDALRasterBand *srcBand = srcDataset->GetRasterBand(band);
GDALRasterBand *dstBand = dstDataset->GetRasterBand(band);
// 读取源数据并写入目标数据集
srcBand->RasterIO(GF_Read, srcX, srcY, tileWidth, tileHeight,
buffer.data(), blockSize, blockSize, GDT_Byte, 0, 0);
dstBand->RasterIO(GF_Write, 0, 0, blockSize, blockSize,
buffer.data(), blockSize, blockSize, GDT_Byte, 0, 0);
}
// 关闭数据集
GDALClose(dstDataset);
GDALClose(srcDataset);
semaphore->reease();
}
4.3 结果分析
虽然给每个线程创建独立的数据集后,不再需要使用互斥锁,也没有再出现大量的读取错误,但处理效率依然没有显著变化。
5. 最终解决方案:GDAL多线程环境变量
通过进一步研究GDAL库的官方文档,我发现GDAL库本身其实是支持多线程读取功能,只需在程序启动时设置相应的环境变量即可。这意味着之前的多线程优化尝试未能成功的原因在于GDAL库默认没有开启多线程支持。
5.1 环境变量设置
通过设置GDAL_NUM_THREADS环境变量,GDAL库可以解锁多线程读取功能。以下是设置环境变量的代码:
// 在注册GDAL驱动【GDALAllRegister()】后马上设置环境变量
CPLSetConfigOption("GDAL_NUM_THREADS", "16"); // 使用16个线程
CPLSetConfigOption("GDAL_NUM_THREADS", "ALL_CPUS"); // 使用所有CPU核心
5.2 优化效果
经过设置GDAL_NUM_THREADS环境变量后,处理效率得到了显著提升。
6. 结论
在使用GDAL库进行多线程处理时,直接使用常规的多线程方法可能会导致资源冲突和处理效率低下。通过合理使用互斥锁或创建独立数据集可以在一定程度上解决这些问题,但效果有限。
通过设置GDAL库的环境变量来解锁多线程读取功能,我们可以显著提升处理效率。这个经验说明,深入了解第三方库的特性和配置选项是非常重要的,这样可以帮助我们最大限度地发挥其性能。