Skip to content

Remove AsyncPdmObjectVectorDeleter and delete PDM objects synchronously - #14492

Merged
magnesj merged 2 commits into
OPM:devfrom
magnesj:remove-async-pdm-object-deleter
Aug 10, 2026
Merged

Remove AsyncPdmObjectVectorDeleter and delete PDM objects synchronously#14492
magnesj merged 2 commits into
OPM:devfrom
magnesj:remove-async-pdm-object-deleter

Conversation

@magnesj

@magnesj magnesj commented Aug 8, 2026

Copy link
Copy Markdown
Member

Removes caf::AsyncPdmObjectVectorDeleter and PdmChildArrayField::deleteChildrenAsync(), and deletes PDM objects synchronously instead. Closes #14491.

Problem

Deleting a PDM object is not a local operation. PdmObjectHandle::prepareForDelete() mutates state owned by other objects:

for ( it = m_pointersReferencingMe.begin(); it != m_pointersReferencingMe.end(); ++it )
    ( **it ) = nullptr;                     // writes into PdmPointer members owned by other objects
m_pointersReferencingMe.clear();            // set mutated by every PdmPointer ctor/dtor, anywhere

m_pointersReferencingMe is an unsynchronised std::set that PdmPointerImpl::addReference() and removeReference() touch on every PdmPointer construction, copy, assignment and destruction. Destroying an object on a worker thread therefore races with the main thread on the shared object graph.

Async deletion also nested: ~RimSummaryCaseMainCollection called m_ensembles.deleteChildrenAsync(), the spawned thread ran ~RimSummaryEnsemble, which called m_cases.deleteChildrenAsync() again. Crash reports from release 2026.06.1:

[14] AsyncPdmObjectVectorDeleter<RimSummaryEnsemble>::start()::{lambda}   worker thread
[12] RimSummaryEnsemble::~RimSummaryEnsemble()
[11] PdmChildArrayField<RimSummaryCase*>::deleteChildrenAsync()           spawns a second thread
[10] AsyncPdmObjectVectorDeleter<RimSummaryCase>::AsyncPdmObjectVectorDeleter()
[9]  std::_Rb_tree<caf::PdmObjectHandle**>::_M_erase                      m_pointersReferencingMe
[8]  std::__new_allocator::deallocate                                     SIGSEGV

Why removal rather than hardening

The mechanism does not pay for itself. Measured teardown of already loaded summary readers, Windows, MSVC, system allocator:

Model Cases Sequential Parallel Speedup PDM share of teardown
Drogon 100 0.064 s 0.048 s 1.34 x 1.8 %
Drogon 397 0.242 s 0.237 s 1.02 x 2.8 %
Heidrun, 44 MB UNSMRY 10 0.034 s 0.050 s 0.68 x 0.4 %

free() is serialised inside the allocator, so at ensemble scale the speedup disappears, and for large payloads parallel release is slower than sequential. Teardown is cheap in absolute terms either way, and the PDM bookkeeping the race is about accounts for 0.4 to 2.8 percent of it.

Caveat on the numbers: the readers were loaded on one thread, while loadFileSummaryCaseData() loads them with OpenMP. Under an allocator with per-thread arenas, glibc for instance, that changes which arena the memory is freed to and could change the parallel column. The measurement is from Windows, the crash reports are from Linux builds.

Changes

  • Delete cafAsyncObjectDeleter.h and .inl, and PdmChildArrayField::deleteChildrenAsync().
  • Add caf::PdmObjectHandleTools::deleteObjects() for deleting a vector of objects that is no longer owned by a child array field. Used by RicCloseSummaryCaseFeature.
  • RimSummaryCaseMainCollection::~, RimSummaryEnsemble::~, RimSummaryEnsemble::replaceCases() and RimSummaryFileSetEnsemble::createSummaryCasesFromEnsembleFileSet() used clearWithoutDelete() plus a manual delete loop to work around the race. They now call deleteChildren() directly.

Note on issue 12262

The observer disconnection does not depend on clearWithoutDelete(). ~Signal() calls removeObservedSignal() on every observer, so a deleted child detaches itself:

virtual ~Signal()
{
    for ( auto observerCallbackPair : m_observerCallbacks )
        observerCallbackPair.first->removeObservedSignal( this );
}

Verified by asserting the observed signal count with and without clearWithoutDelete() in deleteChildren(), it reaches zero either way. #12262 was a race in the async path, where ~Signal() mutated the observer list from a worker thread concurrently with the main thread. With deletion synchronous the ordering is deterministic, so deleteChildren() keeps its original body.

The unit test is renamed to DeletedChildrenDisconnectFromObserver and now asserts the observed signal count directly, 1000 before deletion and 0 after, instead of relying on a crash that only reproduced in a Debug build.

Verification

ResInsight and ResInsight-tests build and link, all 90 cafPdmCore_UnitTests pass.

caf::AsyncWorkerManager has no callers left after this change. It is a separate general purpose utility, so it is left in place.

magnesj added 2 commits August 8, 2026 10:51
Deleting PDM objects on a worker thread is not safe. PdmObjectHandle::prepareForDelete()
mutates state owned by other objects, it nulls the guarded pointers held by other objects
and clears m_pointersReferencingMe, an unsynchronised std::set that every PdmPointer
construction and destruction touches. Destroying an object off the main thread therefore
races with the main thread on the shared object graph. See issue 14491.

Profiling a summary ensemble teardown shows the mechanism does not pay for itself. Releasing
397 Drogon realizations takes 0.24 s sequentially and 0.24 s in parallel, and for a heavy
case the parallel release is slower than the sequential one, because free() is serialised
inside the allocator. The PDM bookkeeping itself is 0.4 to 2.8 percent of the teardown.

Remove the class and deleteChildrenAsync(), and delete synchronously instead. The call sites
that used clearWithoutDelete() and a manual delete loop to work around the race can now call
deleteChildren() directly. Add caf::PdmObjectHandleTools::deleteObjects() for the case where
the objects are no longer owned by a child array field.

The observer disconnection from issue 12262 does not depend on clearWithoutDelete(). ~Signal()
unregisters itself from every observer, so a deleted child detaches itself. That fix addressed
the async race, where ~Signal() mutated the observer list from a worker thread. The unit test
is updated to assert the observed signal count directly instead of relying on a crash.
The include came transitively from cafAsyncObjectDeleter.inl through
cafPdmChildArrayField.inl. Removing the async deleter broke the build on Linux,
where libstdc++ and libc++ do not pull in <thread> the way the MSVC STL does.
@magnesj magnesj self-assigned this Aug 10, 2026
@magnesj
magnesj requested a review from kriben August 10, 2026 05:54
@magnesj
magnesj marked this pull request as ready for review August 10, 2026 05:54
@magnesj
magnesj merged commit 94da54c into OPM:dev Aug 10, 2026
10 checks passed
@magnesj
magnesj deleted the remove-async-pdm-object-deleter branch August 10, 2026 07:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

PDM: async object deletion races with the main thread on the shared object graph

2 participants