RFC 47: 데이터셋별 캐시 작업 및 GDALRasterBand 멀티스레딩 (구현되지 않음)

저자: 블레이크 톰슨(Blake Thompson)

연락처: flippmoke@gmail.com

상태: 개발 중

요약

멀티스레드 코드에서 GDAL을 활용할 때, 코드 가운데 제한되는 부분이 GDAL 내부의 LRU(Least Recently Used) 블록 캐시였던 경우가 많았습니다. 이 RFC는 데이터셋별로 여러 LRU를 가질 수 있게 하고 잠금(locking) 발생 시 최적화해서 멀티스레드 상황에서 LRU 캐시를 보다 효율적으로 만들기 위한 시도입니다. 뿐만 아니라 이 변경 사항은 캐시 내부에서 데이터를 효율적으로 관리할 수 있는 방식을 찾으려는 시도를 간략하게 서술합니다.

이 변경 사항은 다음을 시도합니다:

  • 래스터 데이터셋 내부의 캐시 작업 시스템을 만듭니다:

    • 스레드 안전(Thread Safety)

    • 증가하는 스레드 개수와 비례하는 보다 선형적인 성능 제공

  • 현재 캐시 잠금의 범위를 축소합니다.

  • 선택적으로, (전체 수준 캐시보다는) 데이터셋별 캐시를 활성화합니다.

  • Mem 데이터셋의 READ 스레드를 데이터셋별로 안전하게 만듭니다.

  • 드라이버에서의 스레드 안전을 높이는 향후 개발을 위한 토대를 마련합니다.

이 변경 사항은 다음을 시도하지 않습니다:

  • 모든 드라이버를 스레드 안전하게 만들기

  • 데이터셋을 스레드 안전하게 만들기

두 가지 서로 다른 해결책

이 문제를 해결하기 위한 두 가지 서로 다른 방법을 제안하고 둘 다 코드 작업을 완료했습니다. (각 해결책에 대한 테스트 코드를 작성해야 합니다.) 하지만, 두 해결책들은 몇몇 공통 해결책을 공유합니다. 먼저 이 두 가지 서로 다른 해결책의 공통 변경 사항들을 설명한 다음 이 두 해결책이 어떻게 다른지를 설명하겠습니다.

풀 요청

공통 해결책

데이터셋 캐시 작업

성능을 제한하는 정적 전체 수준 뮤텍스(mutex)는 gcore/gdalrasterblock.cpp 안에 있습니다. 이 뮤텍스가 해당 파일에 위치한 이유는 최대 캐시, LRU 캐시 자체 및 현재 캐시 용량의 설정을 보호하기 위해서입니다. 캐시가 가득 차면 이 뮤텍스의 현재 범위가 한동안 캐시를 잠그게 만들고, 그 동안 GDALRasterBlock::Internalize() 메소드가 새 메모리를 초기화합니다.

이 LRU 캐시를 더 자주 잠가야 할 필요가 없게 하기 위해 새로운 전체 수준 환경설정 옵션 “GDAL_DATASET_CACHING”을 도입했습니다. 이 옵션을 YES로 설정하면 LRU 캐시를 전체 수준 캐시가 아니라 데이터별 캐시로 만듭니다. (기본값은 NO입니다.) 이렇게 하면 스레드를 사용하는 응용 프로그램이 단일 데이터셋에 대한 캐시만 플러시할 수 있게 해주기 때문에, 어떤 상황에서는 두 가지 이유로 성능을 개선합니다. 첫째, 좀 더 자주 사용되는 데이터셋의 캐시는 다른 데이터셋의 캐시와는 별도로 설정될 수도 있습니다. 즉 캐시된 상태로 유지될 가능성이 더 크다는 뜻입니다. 둘째, 공통 전체 수준 뮤텍스가 없다는 것은 서로 다른 데이터셋들을 작업하는 경우 스레드 2개가 동일한 뮤텍스를 잠글 가능성이 낮아진다는 의미입니다.

서로 다른 캐시들을 관리하기 위해 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();
};

원래 GDALRasterBlock 내부에서 statistics:* 가 수행했던 작업들 가운데 다수가 이제 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 내부의 스레드 작업 문제들을 ‘해결’하고 정말로 스레드 안전하게 만들려는 것이 아닙니다. 이 변경 사항들의 목적은 단순히 캐시 스레드를 안전하게 만드는 것입니다. 이를 위해 다음 3개의 뮤텍스를 사용합니다. 이 뮤텍스 3개의 위치는 제안 해결책이 무엇이냐에 따라 달라집니다.

첫 번째 해결책 (GDALDataset의 RW 뮤텍스)

뮤텍스

첫 번째 해결책을 위한 뮤텍스 3개는 다음과 같습니다:

  • 데이터셋 RW 뮤텍스 (GDALDataset 별)

  • 밴드 뮤텍스 (GDALRasterBand 별)

  • RBM 뮤텍스 (GDALRasterBlockManager 별)

교착 상태(deadlock)를 피하기 위해, 이 뮤텍스들의 우선 순위는 목록화되는 순서를 확립하는 것입니다. 예를 들어 밴드 뮤텍스를 가지고 있다면, 밴드 뮤텍스를 가져오기 전에 가져오지 않는 이상 데이터셋 RW 뮤텍스를 가져오지 못 할 수도 있습니다. 하지만 한 번에 하나 이상의 뮤텍스를 가지고 있지 않는 것이 목표입니다!

데이터셋 RW 뮤텍스

데이터셋 RW 뮤텍스의 목적은 데이터셋과 연결된 GDALRasterBlock 들 안에 저장된 데이터를 보호하고 대용량 읽기 또는 쓰기 작업 도중 잠그는 것입니다. 이렇게 하면 서로 다른 스레드 2개가 동일한 GDALRasterBlock 에 대해 memcpy()를 동시에 사용하는 일을 방지합니다. 이 뮤텍스는 일반적으로 GDALDataset 안에 있지만, 독립형 GDALRasterBand 의 경우 밴드에 대해 새 뮤텍스를 사용합니다.

밴드 뮤텍스

밴드 뮤텍스의 목적은 GDALRasterBand 에 있는 블록들의 배열의 제어를 관리하고 GDALRasterBlock 들의 잠금을 관리하는 것입니다. 이 뮤텍스는 GDALRasterBand 별 뮤텍스입니다.

RBM 뮤텍스

RBM(Restricted Boltzmann Machine) 뮤텍스의 목적은 LRU 캐시의 제어를 관리하는 것입니다. 이 뮤텍스는 캐시의 링크된 목록의 관리 및 캐시에 저장된 데이터의 총용량을 제어하는 책임을 집니다.

장점

이 해결책은 두 가지 서로 다른 해결책 가운데 훨씬 단순한 해결책입니다. 데이터셋 수준에서 블록 보호가 이루어지기 때문에 밴드 하나를 읽거나 쓰는 과정에서 하나 이상의 밴드에 접근할 수도 있는 GeoTIFF 드라이버 같은 일부 드라이버들의 문제를 방지합니다. 즉 블록 데이터별로 밴드 수준에서만 잠금이 일어나는 경우 이 해결책이 제안하는 블록 보호가 없다면 문제점이 발생할 수 있습니다.

단점

이 해결책은 최적의 잠금 방법이 아닐 수도 있습니다. IReadBlock, IWriteBlock, 그리고 IRasterIO 루틴을 전체 데이터셋에 걸쳐 보호하기 때문입니다. 스레드를 사용하는 환경에서 동일한 데이터셋을 읽어오는 경우 이 해결책은 아주 제한적입니다. 한 번에 하나 이상의 블록을 읽을 수 없기 때문입니다.

두 번째 해결책 (GDALRasterBlock의 RW 뮤텍스)

뮤텍스

두 번째 해결책을 위한 뮤텍스 3개는 다음과 같습니다:

  • 밴드 뮤텍스 (GDALRasterBand 별)

    • RBM 뮤텍스 (GDALRasterBlockManager 별)

    • 블록 RW 뮤텍스 (GDALRasterBlock 별)

교착 상태를 피하기 위해 밴드 뮤텍스가 우선합니다. 즉 RBM 또는 블록 RW 뮤텍스를 가지고 있는 경우, 이전에 이미 밴드 뮤텍스를 가지고 있지 않은 이상 밴드 뮤텍스를 가져올 수 없다는 뜻입니다. 블록 뮤텍스와 RBM 뮤텍스를 동시에 가져오지 못 할 수도 있습니다.

밴드 뮤텍스

밴드 뮤텍스의 목적은 GDALRasterBand 에 있는 블록들의 배열의 제어를 관리하고 GDALRasterBlock 들의 잠금을 관리하는 것입니다. 이 뮤텍스는 GDALRasterBand 별 뮤텍스입니다.

RBM 뮤텍스

RBM 뮤텍스의 목적은 LRU 캐시의 제어를 관리하는 것입니다. 이 뮤텍스는 캐시의 링크된 목록의 관리 및 캐시에 저장된 데이터의 총용량을 제어하는 책임을 집니다.

블록 RW 뮤텍스

블록 RW 뮤텍스의 목적은 데이터셋과 연결된 GDALRasterBlock 들 안에 저장된 데이터를 보호하고 대용량 읽기 또는 쓰기 작업 도중 잠그는 것입니다. 이렇게 하면 서로 다른 스레드 2개가 동일한 GDALRasterBlock 에 대해 memcpy()를 동시에 사용하는 일을 방지합니다. 이 뮤텍스는 블록별 기반으로 생성되었습니다.

장점

이 해결책이 아마도 블록 작업을 위한 집중적이고 빠른, 가장 완전한 해결책일 것입니다. 이렇게 하면 IReadWrite, IWriteBlock, 그리고 IRasterIO가 이제 자신의 호출을 통해 뮤텍스를 void ** (void pointer pointer)로 전송할 수 있기 때문입니다. CPLMutexHolderD에 전송된 NULL인 void ** 가 포인터를 생성하거나 잠금을 발생시키지 않도록 뮤텍스에도 변경 사항을 적용했습니다. 다시 말해 뮤텍스에 대해 NULL 값을 전송하는 것만으로도 기존 코드의 습성을 대부분 유지할 수 있다는 의미입니다. 이런 모든 변경 사항들은 드라이버가 블록 안에 있는 데이터를 보호할 때 잠금이 발생하는 방식을 더 광범위하게 제어할 수 있게 해줍니다.

단점

이 해결책이 더 복잡한 해결책이기 때문에 유지/관리도 더 어려울 것이 분명합니다. 드라이버 작성도 이전처럼 대수로운 작업이 아니라, 교착 상태를 방지하고 스레드 안전을 유지/관리하기 위해 드라이버 안에서 잠금이 어떻게 수행되는지에 대해 더 주의를 기울여야만 합니다. 이 해결책으로 인해 발생할 수도 있는 다른 문제점은 스레드를 사용하지 않는 코드에서 속도가 약간 줄어든다는 것입니다. 스레드를 사용하는 방식으로 접근하지 않을 데이터를 잠그는 데 추가 사이클이 소요되기 때문입니다. 뿐만 아니라, 뮤텍스가 너무 많이 생성되는 경우 윈도우에서 문제점이 발생할 수도 있습니다. (GDALRasterBlock 별 뮤텍스이기 때문에 윈도우 상에서 더 많이 생성되기 때문입니다.) (주의: 이 문제점을 어떻게 제대로 테스트할 수 있을지 확실하지 않습니다.)