RFC 47:每个数据集缓存和gdalMasterBand多线程(未实现)
作者:布莱克汤普森
联系人:gmail.com上的flippmoke
现状:发展
总结
在多线程代码中使用GDAL时,发现代码的限制部分通常是GDAL中的LRU块缓存。这是一种尝试,通过使每个数据集有多个LRU并在发生锁定时进行优化,使LRU缓存在多线程情况下更加高效。此外,概述的更改试图找到一种有效的方式来管理缓存中的数据。
此更改尝试:
在栅格数据集中创建缓存系统:
线程安全
随着线程数的增加,提供的性能更加线性化
减小当前缓存锁定的范围。
可选地启用每个数据集缓存(而不是全局缓存)
使每个数据集的Mem数据集读取线程安全。
为今后的开发打下基础,提高驱动程序的线程安全性。
此更改不会尝试:
使所有驱动程序线程安全
使数据集线程安全
两种不同的解决方案
提出了两种不同的方法来解决这个问题,并且两种方法都进行了编码(每种方法的测试代码都有待编写)。然而,两者都有一些共同的解决方案。首先,我将介绍两种不同方法的常见变化,然后介绍两种解决方案的不同之处。
拉动请求
Pull Request #1 -解决方案1(数据集RW锁定)
Pull Request #2 -解决方案2(块RW锁定)
共同解决方案
数据集缓存
限制性能的静态全局互斥锁位于gcore/gdalrasterblock.cpp中。这个互斥锁用于保护最大缓存的设置、LRU缓存本身以及缓存的当前大小。此互斥锁的当前作用域使其在缓存已满且新内存正在GDALRasterBlock::Internalize()中初始化时锁定较长时间。
为了消除这种LRU缓存更经常被锁定的需要,引入了一个新的全局配置选项“GDAL_DATASET_CACHING”。当设置为“是”时,这将导致LRU缓存为每个数据集,而不是全局缓存(“否”默认值)。这样做还将允许线程应用程序只刷新单个数据集的缓存,在某些情况下提高性能有两个原因。首先,一个更常用的数据集的缓存可以与其他数据集分开设置,这意味着它更可能保持缓存状态。第二种情况是,如果在不同的数据集上执行操作,缺少通用全局互斥锁将导致两个线程锁定同一互斥锁的可能性较小。
为了管理不同的缓存,引入了一个GDALRasterBlockManager类。这个类负责管理全局或每个数据集情况下的缓存。
GDALRasterBlockManager
class CPL_DLL GDALRasterBlockManager
{
friend class GDALRasterBlock;
int bCacheMaxInitialized;
GIntBig nCacheMax;
volatile GIntBig nCacheUsed;
volatile GDALRasterBlock *poOldest; /* tail */
volatile GDALRasterBlock *poNewest; /* head */
void *hRBMMutex;
public:
GDALRasterBlockManager();
virtual ~GDALRasterBlockManager();
void SetCacheMax( GIntBig nBytes );
GIntBig GetCacheMax(void);
GIntBig GetCacheUsed(void);
int FlushCacheBlock(void);
void FlushTillBelow();
void Verify();
int SafeLockBlock( GDALRasterBlock ** );
void DestroyRBMMutex();
};
最初由statistics完成的许多操作:*在GDALRasterBlock中,现在被移动到GDALRasterBlockManager中。
GDALDataset
每个GDALDataset现在都有一个:
GDALRasterBlockManager *poRasterBlockManager;
这是在数据集初始化时通过以下方式设置的:
bDatasetCache = CSLTestBoolean(
CPLGetConfigOption( "GDAL_DATASET_CACHING", "NO") );
if ( bDatasetCache )
{
poRasterBlockManager = new GDALRasterBlockManager();
}
else
{
poRasterBlockManager = GetGDALRasterBlockManager();
}
GDALRasterBand
为了使缓存更安全、更高效,GDALRasterBand中还引入了互斥锁。这个互斥锁的任务是保护每个波段的RasterBlock数组(papoBlocks)。
线程安全与两种解决方案
GDAL的多线程处理是一件复杂的事情,而这些更改确实试图 改善 在GDAL内穿线。它没有 解决 GDAL中的线程问题,并使其真正线程安全。此更改的目标只是使缓存线程安全,以便实现这三个互斥体的使用。这三个互斥体的位置在所提出的两个解决方案之间是不同的。
解决方案1(GDALDataset中的RW Mutex)
互斥量
对于解决方案1,三个互斥体是:
数据集RW Mutex(每个GDALDataset)
波段互斥量(每个GdalMasterBand)
RBM互斥锁(每个GdalMasterBlockManager)
为了防止死锁,互斥锁的优先级按照它们的列出顺序建立。例如,如果您有带互斥锁,则可能无法获取数据集RW互斥锁,除非它是在获取带互斥锁之前获取的。但是,目标应该始终是一次不要有超过mutex!
数据集RW互斥
数据集RW Mutex的目标是保护与数据集关联的GDALRasterBlocks中存储的数据,并在大型读或写操作期间锁定。这可以防止两个不同的线程同时在同一个GDALRasterBlock上使用memcpy。这个互斥通常位于GDALDataset中,但是对于独立的GDALRasterBand,它在Band上使用一个新的互斥。
波段互斥
Band Mutex的目标是管理GDALRasterBand中块数组的控制,并管理GDALRasterBlocks的锁定。这是每个gdalMasterBand互斥体。
RBM互斥
RBM互斥锁的目标是管理对LRU缓存的控制。这个互斥锁负责控制缓存链表的管理和缓存中存储的数据总量。
赞成的意见
这是两种不同可能解决方案的更简单的解决方案。由于块的保护是在数据集级别完成的,因此它避免了某些驱动程序(如geotiff)的问题,在读或写一个带时可能会访问多个带。因此,在没有这种保护的情况下,如果锁定只是在每个块的数据的带级上,可能会导致问题。
欺骗
这个解决方案可能不是最理想的锁定方式,因为IReadBlock、IWriteBlock和IRasterIO例程的保护覆盖整个数据集。当您在线程环境中读取同一个数据集时,这是非常有限的,因为不可能一次读取多个块。
解决方案2(GDALRasterBlock中的RW Mutex)
互斥量
对于解决方案2,三个互斥体是:
波段互斥量(每个GdalMasterBand)
RBM互斥锁(每个GdalMasterBlockManager)
块RW互斥锁(每个GDALRasterBlock)
为了防止死锁,带互斥锁具有优先权。这意味着,如果您有RBM或Blow RW互斥,则无法获得波段互斥,除非在此之前您已经有了波段互斥。不能同时获得块互斥和RBM互斥。
波段互斥
Band Mutex的目标是管理GDALRasterBand中块数组的控制,并管理GDALRasterBlocks的锁定。这是每个gdalMasterBand互斥体。
RBM互斥
RBM互斥锁的目标是管理对LRU缓存的控制。这个互斥锁负责控制缓存链表的管理和缓存中存储的数据总量。
块RW互斥
块RW互斥锁的目标是保护与数据集相关联的GDALRasterBlocks中存储的数据,并在大型读或写操作期间锁定。这可以防止两个不同的线程同时在同一个GDALRasterBlock上使用memcpy。它是按块创建的。
赞成的意见
这可能是最完整的解决方案,使密集和快速线程的阻塞解决方案。这是因为IReadWrite、IWriteBlock和IRasterIO现在有可能通过它们的调用传递一个互斥体,作为一个空指针指针。对互斥对象也进行了更改,以便传递给CPLMutexHolderD的空指针指针不会导致创建任何指针或发生任何锁定。这意味着现有代码的大部分行为可以通过简单地为互斥量传递一个空值来维护。所有这些更改都允许驱动程序在保护块中的数据时对锁定的方式保持更多的控制。
欺骗
显然,这是一个更复杂的解决方案,因此更难管理。这意味着编写驱动程序并不像以前那么简单,为了防止死锁和维护线程安全,必须注意如何在驱动程序中执行锁定。另一个可能由此产生的问题是,由于锁定数据所花费的额外周期无法以线程方式访问,所以非线程代码的速度会稍微慢一些。此外,如果创建了太多的互斥锁,它在windows中可能会有问题(因为它是每个GDALRasterBlock互斥锁,所以还有很多问题)。(注:不确定如何才能正确测试?)