스레딩 - C++ SDK
이 페이지의 내용
성능이 뛰어난 앱을 만들려면 개발자는 교착 상태 및 경쟁 상태와 같은 문제를 방지하는 스레드로부터 안전하고 유지 관리가 가능한 멀티스레드 코드를 작성해야 합니다. Realm은 성능이 뛰어난 멀티스레드 앱을 위해 특별히 설계된 도구를 제공합니다.
따라야 할 세 가지 규칙
멀티스레드 앱을 위한 Realm의 도구를 살펴보기 전에 다음 세 가지 규칙을 이해하고 따라야 합니다.
- 읽기 위해 락하지 않도록 합니다.
- Realm의 MVCC(Multiversion Concurrency Control) 아키텍처는 읽기 작업을 락 필요가 없습니다. 읽은 값은 절대 손상되거나 부분적으로 수정된 상태 가 되지 않습니다. 잠금이나 뮤텍스 없이도 모든 스레드에서 동일한 Realm 파일 을 자유롭게 읽을 수 있습니다. 각 스레드가 읽기 전에 차례를 기다려야 할 수 있으므로 불필요하게 잠그면 성능 병목 현상이 발생할 수 있습니다.
- 백그라운드 스레드에서 작성하는 경우 UI 스레드에서 동기식 쓰기를 방지합니다.
- 모든 스레드에서 Realm 파일에 쓸 수 있지만 한 번에 하나의 작성자만 있을 수 있습니다. 따라서 동기식 쓰기 트랜잭션(write transaction)은 서로를 차단합니다. UI 스레드에서 동기식 쓰기를 수행하면 백그라운드 스레드에서 쓰기가 완료될 때까지 기다리는 동안 앱이 응답하지 않는 것처럼 보일 수 있습니다. Device Sync 는 백그라운드 스레드에서 쓰기를 수행하므로 동기화된 Realm이 있는 UI 스레드에서는 동기식 쓰기를 피해야 합니다.
- 라이브 객체, 컬렉션 또는 영역을 다른 스레드에 전달하지 않도록 합니다.
- 라이브 객체, 컬렉션 및 Realm 인스턴스는 스레드에 한정되어 있으므로 해당 인스턴스가 생성된 스레드에서만 유효합니다. 실질적으로 이는 라이브 인스턴스를 다른 스레드로 전달할 수 없음을 의미합니다. 그러나 Realm은 스레드 간에 객체를 공유하기 위한 몇 가지 메커니즘을 제공합니다.
스레드 간 커뮤니케이션
다른 스레드에서 동일한 Realm 파일에 액세스하려면 액세스가 필요한 모든 스레드에서 영역 인스턴스를 인스턴스화해야 합니다. 동일한 구성을 지정하는 한 모든 영역 인스턴스는 디스크의 동일한 파일에 매핑됩니다.
멀티스레드 환경에서 Realm으로 작업할 때 주요 규칙 중 하나는 객체는 스레드에 한정되어 있다는 것입니다. 즉, 다른 스레드에서 시작된 Realm, 컬렉션 또는 객체의 인스턴스에 액세스할 수 없습니다. Realm의 MVCC(Multiversion Concurrency Control) 아키텍처는 객체의 활성 버전이 언제든지 여러 개일 수 있음을 의미합니다. 스레드 제한은 해당 스레드의 모든 인스턴스가 동일한 내부 버전인지 확인합니다.
스레드 간에 통신해야 하는 경우 사용 사례에 따라 여러 가지 옵션이 있습니다.
두 개의 스레드에서 객체를 수정하려면 두 스레드 모두에서 객체를 쿼리합니다.
스레드의 변경 사항에 대응하려면 Realm의 알림을사용하세요.
현재 스레드의 영역 인스턴스에 있는 다른 스레드에서 일어난 변화를 보려면 영역 인스턴스를 새로 고침합니다.
Realm 또는 특정 객체의 인스턴스를 다른 스레드와 공유하려면 Realm 인스턴스 또는 객체에 대한 thread_safe_reference 를 공유합니다.
객체의 빠른 읽기 전용 보기를 다른 스레드로 보내려면 객체를 '동결'합니다.
스레드 간 인스턴스 전달
realm::realm
, realm::results
및 realm::object
인스턴스는 스레드에 한정되어 있습니다. 즉, 해당 항목을 만든 스레드에서만 사용할 수 있습니다.
다음과 같이 스레드에 한정된 인스턴스를 다른 스레드로 복사할 수 있습니다.
스레드에 한정된 객체로 thread_safe_reference 를 초기화합니다.
참고를 대상 스레드에 전달합니다.
대상 스레드에서 참고를 확인합니다. 참고된 객체가 영역 인스턴스인 경우
.resolve()
을(를) 호출하여 해결합니다. 그렇지 않으면 참고를realm.resolve()
로 이동합니다. 반환된 객체는 이제 원래 스레드가 아닌 대상 스레드에서 생성된 것처럼 대상 스레드에 스레드 제한됩니다.
중요
thread_safe_reference
는 정확히 한 번만 해결해야 합니다. 그렇지 않으면 참고 할당이 해제될 때까지 소스 영역이 고정된 상태로 유지됩니다. 따라서 thread_safe_reference
는 수명이 짧아야 합니다.
// Put a managed object into a thread safe reference auto threadSafeItem = realm::thread_safe_reference<realm::Item>{managedItem}; // Move the thread safe reference to a background thread auto thread = std::thread([threadSafeItem = std::move(threadSafeItem), path]() mutable { // Open the database again on the background thread auto backgroundConfig = realm::db_config(); backgroundConfig.set_path(path); auto backgroundRealm = realm::db(std::move(backgroundConfig)); // Resolve the Item instance via the thread safe // reference auto item = backgroundRealm.resolve(std::move(threadSafeItem)); // ... use item ... }); // Wait for thread to complete thread.join();
다른 스레드에서 객체로 작업하는 또 다른 방법은 해당 스레드에서 객체를 다시 쿼리하는 것입니다. 그러나 객체에 기본 키가 없는 경우 이를 쿼리하는 것은 간단하지 않습니다. 기본 키가 있는지 여부에 관계없이 모든 객체에 thread_safe_reference
를 사용할 수 있습니다.
여러 스레드에서 동일한 Realm 사용
스레드 간에 영역 인스턴스를 공유할 수 없습니다.
여러 스레드에서 동일한 Realm 파일을 사용하려면 각 스레드에서 다른 Realm 인스턴스를 엽니다. 동일한 구성 을 사용하는 한 모든 Realm 인스턴스는 디스크의 동일한 파일에 매핑됩니다.
스레드 간에 불변 복사본 전달
스레드에 제한된 라이브 객체는 대부분의 경우 잘 작동합니다. 그러나 일부 앱(예시: 반응형 이벤트 스트림 기반 아키텍처를 기반으로 하는 앱)은 최종적으로 UI 스레드에 도달하기 전에 처리를 위해 불변의 복사본을 여러 스레드로 전송해야 합니다. 매번 딥 카피를 만드는 것은 비용이 많이 들고 영역은 라이브 인스턴스를 스레드 간에 공유하는 것을 허용하지 않습니다. 이 경우 객체, 컬렉션 및 영역을 동결 및 동결 해제할 수 있습니다.
동결하면 특정 객체, 컬렉션 또는 영역에 대한 변경할 수 없는 보기가 생성됩니다. 동결된 객체, 컬렉션 또는 영역은 여전히 디스크에 존재하며 다른 스레드로 전달될 때 딥 카피가 필요 없습니다. 스레드 문제에 대한 걱정 없이 스레드 간에 동결된 객체를 자유롭게 공유할 수 있습니다. 영역을 동결하면 그 하위 객체도 동결됩니다.
동결된 객체는 라이브 상태가 아니며 자동으로 업데이트되지 않습니다. 이는 사실상 정지 시점의 객체 상태에 대한 스냅샷입니다. 객체를 동결 해제하면 동결된 객체의 라이브 버전이 반환됩니다.
영역, collection 또는 객체를 고정하려면 .freeze()
메서드를 호출합니다.
auto realm = realm::db(std::move(config)); // Get an immutable copy of the database that can be passed across threads. auto frozenRealm = realm.freeze(); if (frozenRealm.is_frozen()) { // Do something with the frozen database. // You may pass a frozen realm, collection, or objects // across threads. Or you may need to `.thaw()` // to make it mutable again. } // You can freeze collections. auto managedItems = realm.objects<realm::Item>(); auto frozenItems = managedItems.freeze(); CHECK(frozenItems.is_frozen()); // You can read from frozen databases. auto itemsFromFrozenRealm = frozenRealm.objects<realm::Item>(); CHECK(itemsFromFrozenRealm.is_frozen()); // You can freeze objects. auto managedItem = managedItems[0]; auto frozenItem = managedItem.freeze(); CHECK(frozenItem.is_frozen()); // Frozen objects have a reference to a frozen realm. CHECK(frozenItem.get_realm().is_frozen());
동결된 객체로 작업할 때 다음 중 하나를 시도하면 예외가 발생합니다.
동결된 영역에서 쓰기 트랜잭션(write transaction) 열기
동결된 객체 수정
동결된 영역, 컬렉션 또는 객체에 변경 리스너 추가
.is_frozen()
을(를) 사용하여 객체가 동결되었는지 확인할 수 있습니다 이는 항상 스레드로부터 안전합니다.
if (frozenRealm.is_frozen()) { // Do something with the frozen database. // You may pass a frozen realm, collection, or objects // across threads. Or you may need to `.thaw()` // to make it mutable again. }
동결된 객체는 해당 객체를 생성한 라이브 영역이 열려 있는 한 유효합니다. 따라서 모든 스레드가 동결된 객체로 완료될 때까지 라이브 영역을 닫지 않도록 합니다. 라이브 영역이 닫히기 전에 동결된 영역을 닫을 수 있습니다.
중요
동결된 객체를 캐싱하는 경우
동결된 객체를 너무 많이 캐싱하면 영역 파일 크기에 부정적인 영향을 미칠 수 있습니다. '너무 많음'은 특정 대상 장치와 Realm 객체의 크기에 따라 다릅니다. 많은 수의 버전을 캐시해야 하는 경우에는 필요한 버전을 영역 외부로 복사하는 것이 좋습니다.
동결된 객체 수정
동결된 객체를 수정하려면 해당 객체를 동결 해제해야 합니다. 동결되지 않은 영역에서 쿼리한 다음 수정할 수도 있습니다. 라이브 객체, 컬렉션 또는 영역에서 .thaw()
을(를) 호출하면 자동으로 반환됩니다.
객체 또는 컬렉션을 동결 해제하면 객체 또는 컬렉션이 참조하는 영역도 동결 해제됩니다.
// Read from a frozen database. auto frozenItems = frozenRealm.objects<realm::Item>(); // The collection that we pull from the frozen database is also frozen. CHECK(frozenItems.is_frozen()); // Get an individual item from the collection. auto frozenItem = frozenItems[0]; // To modify the item, you must first thaw it. // You can also thaw collections and realms. auto thawedItem = frozenItem.thaw(); // Check to make sure the item is valid. An object is // invalidated when it is deleted from its managing database, // or when its managing realm has invalidate() called on it. REQUIRE(thawedItem.is_invalidated() == false); // Thawing the item also thaws the frozen database it references. auto thawedRealm = thawedItem.get_realm(); REQUIRE(thawedRealm.is_frozen() == false); // With both the object and its managing database thawed, you // can safely modify the object. thawedRealm.write([&] { thawedItem.name = "Save the world"; });
동결된 컬렉션에 추가
동결된 collection 에 추가할 때는 collection이 포함된 객체와 추가하려는 객체를 모두 동결 해제해야 합니다.
스레드 간에 고정된 객체를 전달할 때도 동일한 규칙이 적용됩니다. 일반적인 경우는 UI를 차단하는 대신 백그라운드 스레드에서 함수를 호출하여 일부 작업을 수행하는 경우일 수 있습니다.
이 예시에서는 동결된 Realm에 있는 두 객체를 쿼리합니다.
Item
객체의 목록 속성이 있는Project
객체Item
객체
Project
의 items
목록 collection에 Item
를 추가하기 전에 두 객체를 모두 동결 해제해야 합니다. Project
객체만 Item
동결 해제하고 객체는 동결 해제하지 않으면 Realm에서 오류가 발생합니다.
// Get frozen objects. // Here, we're getting them from a frozen database, // but you might also be passing them across threads. auto frozenItems = frozenRealm.objects<realm::Item>(); // The collection that we pull from the frozen database is also frozen. CHECK(frozenItems.is_frozen()); // Get the individual objects we want to work with. auto specificFrozenItems = frozenItems.where( [](auto const& item) { return item.name == "Save the cheerleader"; }); auto frozenProjects = frozenRealm.objects<realm::Project>().where( [](auto const& project) { return project.name == "Heroes: Genesis"; }); ; auto frozenItem = specificFrozenItems[0]; auto frozenProject = frozenProjects[0]; // Thaw the frozen objects. You must thaw both the object // you want to append and the object whose collection // property you want to append to. auto thawedItem = frozenItem.thaw(); auto thawedProject = frozenProject.thaw(); auto managingRealm = thawedProject.get_realm(); managingRealm.write([&] { thawedProject.items.push_back(thawedItem); });
struct Item { std::string name; }; REALM_SCHEMA(Item, name)
struct Project { std::string name; std::vector<realm::Item*> items; }; REALM_SCHEMA(Project, name, items)
스케줄러(이벤트 루프)
일부 플랫폼이나 프레임워크는 앱의 수명 동안 이벤트를 지속적으로 처리하는 스케줄러 (또는 이벤트 루프)를 자동으로 설정합니다. Realm C++ SDK는 다음 플랫폼 또는 프레임워크에서 스케줄러를 감지하고 사용합니다.
macOS, iOS, tvOS, watchOS
Android
Qt
Realm은 스케줄러를 사용하여 Realm Mobile Sync 업로드 및 다운로드와 같은 작업을 예약합니다.
플랫폼에 지원되는 스케줄러가 없거나 사용자 지정 스케줄러를 사용하려는 경우 realm::scheduler 를 구현하고 Realm 구성에 사용하는 realm::db_config 에 인스턴스를 전달할 수 있습니다. Realm은 전달된 스케줄러를 사용합니다.
struct MyScheduler : realm::scheduler { MyScheduler() { // ... Kick off task processor thread(s) and run until the scheduler // goes out of scope ... } ~MyScheduler() override { // ... Call in the processor thread(s) and block until return ... } void invoke(std::function<void()>&& task) override { // ... Add the task to the (lock-free) processor queue ... } [[nodiscard]] bool is_on_thread() const noexcept override { // ... Return true if the caller is on the same thread as a processor // thread ... } bool is_same_as(const realm::scheduler* other) const noexcept override { // ... Compare scheduler instances ... } [[nodiscard]] bool can_invoke() const noexcept override { // ... Return true if the scheduler can accept tasks ... } // ... }; int main() { // Set up a custom scheduler. auto scheduler = std::make_shared<MyScheduler>(); // Pass the scheduler instance to the realm configuration. auto config = realm::db_config{path, scheduler}; // Start the program main loop. auto done = false; while (!done) { // This assumes the scheduler is implemented so that it // continues processing tasks on background threads until // the scheduler goes out of scope. // Handle input here. // ... if (shouldQuitProgram) { done = true; } } }
Realm 새로고침
스케줄러나 이벤트 루프에 의해 제어되는 모든 스레드에서 Realm은 이벤트 루프 반복이 시작될 때마다 객체를 자동으로 새로 고칩니다. 이벤트 루프를 반복하는 사이에 스냅샷으로 작업하게 되므로 개별 메서드는 항상 일관된 뷰를 볼 수 있으며 다른 스레드에서 어떤 일이 발생할지 걱정할 필요가 없습니다.
스레드에서 Realm을 처음 열면 해당 상태는 가장 최근에 성공한 쓰기 커밋이 되며 새로 고침할 때까지 해당 버전으로 유지됩니다. 스레드가 런 루프에 의해 제어되지 않으면 realm.refresh() 트랜잭션을 가장 최근 상태로 진행하려면 메서드를 수동으로 호출해야 합니다.
realm.refresh();
참고
정기적으로 Realm을 새로 고치지 못하면 일부 트랜잭션 버전이 '고정'되어 Realm이 해당 버전에서 사용하는 디스크 공간을 재사용하지 못하고 파일 크기가 커질 수 있습니다.
Realm의 스레딩 모델 심층 분석
Realm 은 MVCC(Multiversion Concurrency Control) 를 통해 스레드 전반에 걸쳐 안전하고 빠르며 잠금 없는 동시 액세스 를 제공합니다. 아키텍처.
Git과 비교 및 대조
Git 과 같은 분산된 버전 관리 시스템에 익숙한 경우 를 사용하면 이미 MVCC를 직관적으로 이해하고 있을 수 있습니다. Git의 두 가지 기본 요소는 다음과 같습니다.
커밋, 즉 원자성 쓰기입니다.
브랜치, 즉 커밋 기록의 다른 버전입니다.
마찬가지로 Realm에는 트랜잭션 형태의 원자 단위로 커밋된 쓰기가 있습니다. 또한 Realm은 브랜치처럼 주어진 시간에 다양한 버전의 히스토리를 갖고 있습니다.
포크를 통한 배포와 분산을 적극적으로 지원하는 Git과 달리 영역에는 항상 하나의 최신 버전만 있고 항상 최신 버전의 헤드에 데이터를 기록합니다. 영역은 이전 버전에 쓸 수 없습니다. 즉, 데이터가 하나의 최신 버전으로 수렴된다는 의미입니다.
내부 구조
영역은 B-트리 데이터 구조를 사용하여 구현됩니다. 최상위 노드는 영역의 버전을 나타냅니다. 하위 노드는 해당 버전의 영역에 있는 객체입니다. 영역은 Git이 HEAD 커밋에 대한 포인터를 갖는 것과 마찬가지로 최신 버전에 대한 포인터를 가지고 있습니다.
Realm은 쓰기 중 복사(copy-on-write) 기술을 사용하여 격리 를 보장합니다. 및 내구성 . 변경을 수행하면 Realm은 트리의 관련 부분을 복사하여 기록합니다. 그런 다음 Realm은 두 단계로 변경 사항을 커밋합니다.
Realm은 변경 사항을 디스크에 기록하고 성공 여부를 확인합니다.
그런 다음 Realm은 최신 버전 포인터가 새로 작성된 버전을 가리키도록 설정합니다.
2단계 커밋 프로세스는 쓰기가 도중에 실패하더라도 트리의 관련 부분 복사본이 변경되었기 때문에 원래 버전이 어떤 식으로든 손상되지 않도록 보장합니다. 마찬가지로 영역의 루트 포인터는 새 버전이 유효하다는 것이 보장될 때까지 원본 버전을 가리킵니다.
예시
다음 다이어그램은 커밋 프로세스를 보여줍니다.
영역은 트리 구조로 되어 있습니다. 영역에는 최신 버전인 V1에 대한 포인터가 있습니다.
기록 시 Realm은 V1을 기반으로 V2 버전을 새로 만듭니다. Realm은 수정을 위해 객체의 복사본을 만들고(A 1, C 1) 수정되지 않은 객체에 대한 링크는 계속해서 원본 버전(B, D)을 가리킵니다.
커밋을 확인한 후 Realm은 포인터를 새로운 최신 버전인 V2로 업데이트합니다. 그런 다음 Realm은 더 이상 트리에 연결되지 않은 오래된 노드를 버립니다.
Realm은 메모리 매핑과 같은 제로 카피 기술을 사용하여 데이터를 처리합니다. 영역에서 값을 읽으면 복사본이 아닌 사실상 실제 디스크 값을 보는 것입니다. 이는 라이브 객체의 기본이며 디스크 쓰기가 검증된 후 영역 헤드 포인터가 새 버전을 가리키도록 설정할 수 있는 이유이기도 합니다.