Remove AsyncPdmObjectVectorDeleter and delete PDM objects synchronously

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.
This commit is contained in:
Magne Sjaastad authored and GitHub committed 2026-08-10 09:40:33 +02:00
1 parent 2322d9e97f
commit 94da54c4f5
13 files changed
+37 -189

No files matched your search

@@ -41,6 +41,7 @@
#include <algorithm>
#include <random>
#include <sstream>
#include <thread>
//--------------------------------------------------------------------------------------------------
///
@@ -37,12 +37,9 @@
#include "RiuPlotMainWindow.h"
#include "cafAsyncObjectDeleter.h"
#include "cafPdmObjectHandleTools.h"
#include "cafSelectionManager.h"
#include "cafAssert.h"
#include <QAction>
CAF_CMD_SOURCE_INIT( RicCloseSummaryCaseFeature, "RicCloseSummaryCaseFeature" );
@@ -96,8 +93,7 @@ void RicCloseSummaryCaseFeature::deleteSummaryCases( std::vector<RimSummaryCase*
RiuPlotMainWindow* mainPlotWindow = RiaGuiApplication::instance()->mainPlotWindow();
mainPlotWindow->updateMultiPlotToolBar();
caf::AsyncPdmObjectVectorDeleter<RimSummaryCase> summaryCaseDeleter( cases );
CAF_ASSERT( cases.empty() ); // vector should be empty immediately.
caf::PdmObjectHandleTools::deleteObjects( cases );
}
//--------------------------------------------------------------------------------------------------
@@ -152,14 +152,7 @@ void RimSummaryFileSetEnsemble::onFileSetNameChanged( const caf::SignalEmitter*
//--------------------------------------------------------------------------------------------------
void RimSummaryFileSetEnsemble::createSummaryCasesFromEnsembleFileSet( bool notifyChange )
{
// Delete old cases synchronously to avoid a race between the std::thread spawned by
// deleteChildrenAsync() and the OpenMP parallel loop in loadFileSummaryCaseData().
// #pragma omp critical does not protect against std::thread races.
// clearWithoutDelete() disconnects observer signals first (preserves the fix for #12262).
auto casesToDelete = m_cases.childrenByType();
m_cases.clearWithoutDelete();
for ( auto* c : casesToDelete )
delete c;
m_cases.deleteChildren();
if ( m_ensembleFileSet() )
{
@@ -143,8 +143,8 @@ RimSummaryCaseMainCollection::RimSummaryCaseMainCollection()
//--------------------------------------------------------------------------------------------------
RimSummaryCaseMainCollection::~RimSummaryCaseMainCollection()
{
m_cases.deleteChildrenAsync();
m_ensembles.deleteChildrenAsync();
m_cases.deleteChildren();
m_ensembles.deleteChildren();
}
//--------------------------------------------------------------------------------------------------
@@ -116,7 +116,8 @@ RimSummaryEnsemble::RimSummaryEnsemble()
//--------------------------------------------------------------------------------------------------
RimSummaryEnsemble::~RimSummaryEnsemble()
{
m_cases.deleteChildrenAsync();
m_cases.deleteChildren();
updateReferringCurveSets();
}
@@ -204,13 +205,7 @@ RimSummaryCase* RimSummaryEnsemble::firstSummaryCase() const
//--------------------------------------------------------------------------------------------------
void RimSummaryEnsemble::replaceCases( const std::vector<RimSummaryCase*>& summaryCases, bool notifyChange )
{
// Delete synchronously to prevent a race between the std::thread spawned by
// deleteChildrenAsync() and any active OpenMP parallel loop in loadFileSummaryCaseData().
// clearWithoutDelete() disconnects observer signals first (preserves the fix for #12262).
auto casesToDelete = m_cases.childrenByType();
m_cases.clearWithoutDelete();
for ( auto* c : casesToDelete )
delete c;
m_cases.deleteChildren();
if ( summaryCases.empty() ) return;
@@ -56,8 +56,6 @@ set(PROJECT_FILES
cafTristate.h
cafFilePath.cpp
cafFilePath.h
cafAsyncObjectDeleter.h
cafAsyncObjectDeleter.inl
cafAsyncWorkerManager.h
cafAsyncWorkerManager.cpp
cafPdmObjectHandleTools.h
@@ -1,55 +0,0 @@
//##################################################################################################
//
// Custom Visualization Core library
// Copyright (C) 2011-2013 Ceetron AS
//
// This library may be used under the terms of either the GNU General Public License or
// the GNU Lesser General Public License as follows:
//
// GNU General Public License Usage
// This library is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE.
//
// See the GNU General Public License at <<http://www.gnu.org/licenses/gpl.html>>
// for more details.
//
// GNU Lesser General Public License Usage
// This library is free software; you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE.
//
// See the GNU Lesser General Public License at <<http://www.gnu.org/licenses/lgpl-2.1.html>>
// for more details.
//
//##################################################################################################
#pragma once
namespace caf
{
template <typename PdmObjectType>
class AsyncPdmObjectVectorDeleter
{
public:
AsyncPdmObjectVectorDeleter( std::vector<PdmObjectType*>& pointerVector );
AsyncPdmObjectVectorDeleter( std::vector<PdmPointer<PdmObjectType>>& pdmPointerVector );
~AsyncPdmObjectVectorDeleter();
void start();
private:
std::vector<PdmObjectHandle*> m_pointersToDelete;
};
} // namespace caf
#include "cafAsyncObjectDeleter.inl"
@@ -1,76 +0,0 @@
#include <thread>
#include "cafAsyncWorkerManager.h"
#include "cafPdmObjectHandle.h"
namespace caf
{
//--------------------------------------------------------------------------------------------------
/// Constructor that takes ownership of the data in the provided vector
//--------------------------------------------------------------------------------------------------
template <typename PdmObjectType>
AsyncPdmObjectVectorDeleter<PdmObjectType>::AsyncPdmObjectVectorDeleter( std::vector<PdmObjectType*>& pointerVector )
{
m_pointersToDelete.reserve( pointerVector.size() );
for ( PdmObjectType* rawPointer : pointerVector )
{
if ( rawPointer )
{
PdmObjectHandle* objectHandle = static_cast<PdmObjectHandle*>( rawPointer );
objectHandle->prepareForDelete();
m_pointersToDelete.push_back( objectHandle );
}
}
pointerVector.clear();
}
//--------------------------------------------------------------------------------------------------
/// Constructor that takes ownership of the data in the provided vector
//--------------------------------------------------------------------------------------------------
template <typename PdmObjectType>
AsyncPdmObjectVectorDeleter<PdmObjectType>::AsyncPdmObjectVectorDeleter( std::vector<PdmPointer<PdmObjectType>>& pdmPointerVector )
{
m_pointersToDelete.reserve( pdmPointerVector.size() );
for ( PdmPointer<PdmObjectType>& pdmPointer : pdmPointerVector )
{
if ( pdmPointer.notNull() )
{
PdmObjectHandle* objectHandle = pdmPointer.rawPtr();
objectHandle->prepareForDelete();
m_pointersToDelete.push_back( objectHandle );
}
}
pdmPointerVector.clear();
}
//--------------------------------------------------------------------------------------------------
/// Destructor will launch the asynchronous deletion if start() hasn't already been run.
//--------------------------------------------------------------------------------------------------
template <typename PdmObjectType>
AsyncPdmObjectVectorDeleter<PdmObjectType>::~AsyncPdmObjectVectorDeleter()
{
if ( !m_pointersToDelete.empty() )
{
start();
}
}
//--------------------------------------------------------------------------------------------------
/// Perform deletion of the pointers in a separate thread.
//--------------------------------------------------------------------------------------------------
template <typename PdmObjectType>
void AsyncPdmObjectVectorDeleter<PdmObjectType>::start()
{
std::thread thread(
[]( std::vector<PdmObjectHandle*>&& pointerVector )
{
for ( PdmObjectHandle* pointerToDelete : pointerVector )
{
delete pointerToDelete;
}
},
std::move( m_pointersToDelete ) );
AsyncWorkerManager::instance().takeThreadOwnership( thread, false );
}
} // namespace caf
@@ -71,8 +71,6 @@ public:
void setValue( const std::vector<DataType*>& objects );
size_t indexOf( const PdmObjectHandle* obj ) const override;
virtual void deleteChildrenAsync();
// std::vector-like access
DataType* operator[]( size_t index ) const;
@@ -1,4 +1,3 @@
#include "cafAsyncObjectDeleter.h"
#include "cafClassTypeName.h"
#include "cafPdmObjectHandle.h"
@@ -184,26 +183,6 @@ void PdmChildArrayField<DataType*>::deleteChildren()
m_pointers.clear();
}
//--------------------------------------------------------------------------------------------------
/// Transfers ownership of the objects pointed to a separate thread.
/// Then clears the container and lets the thread delete the objects.
//--------------------------------------------------------------------------------------------------
template <typename DataType>
void PdmChildArrayField<DataType*>::deleteChildrenAsync()
{
CAF_ASSERT( isInitializedByInitFieldMacro() );
auto objectsToDelete = m_pointers;
// Disconnect the connection to observers before deleting the objects
// https://github.com/OPM/ResInsight/issues/12262
//
// See test in \Fwk\AppFwk\cafProjectDataModel\cafPdmCore\cafPdmCore_UnitTests\cafPdmChildArrayFieldHandleTest.cpp
clearWithoutDelete();
AsyncPdmObjectVectorDeleter<DataType> pointerDeleter( objectsToDelete );
}
//--------------------------------------------------------------------------------------------------
/// Removes the pointer at index from the container. Does not delete the object pointed to.
//--------------------------------------------------------------------------------------------------
@@ -160,15 +160,14 @@ TEST( ChildArrayFieldHandle, DerivedOtherObjects )
delete containerObj;
}
TEST( ChildArrayFieldHandle, AsyncDeleteOfMultipleChildren )
TEST( ChildArrayFieldHandle, DeletedChildrenDisconnectFromObserver )
{
// https://github.com/OPM/ResInsight/issues/12262
// A deleted child unregisters itself from its observers. ~Signal() calls removeObservedSignal()
// on every observer, so the container is not left holding signals owned by destroyed objects.
//
// To trigger crash, remove the line
// clearWithoutDelete();
// from PdmChildArrayField<DataType*>::deleteChildrenAsync()
//
// Crash was reproduced in Debug build in VS2022 17.13.2
// https://github.com/OPM/ResInsight/issues/12262 was a race in the async delete path, which no
// longer exists. ~Signal() ran on a worker thread and mutated the observer list concurrently
// with the main thread. Deletion is synchronous now, so this ordering is deterministic.
ContainerObj containerObj;
const int numObjects = 1000;
@@ -177,8 +176,9 @@ TEST( ChildArrayFieldHandle, AsyncDeleteOfMultipleChildren )
containerObj.createAndAppendObject();
}
containerObj.derivedObjs.deleteChildrenAsync();
EXPECT_EQ( size_t( numObjects ), containerObj.observedSignals().size() );
// Wait for async delete to complete
std::this_thread::sleep_for( std::chrono::milliseconds( 100 ) );
containerObj.derivedObjs.deleteChildren();
EXPECT_EQ( size_t( 0 ), containerObj.observedSignals().size() );
}
@@ -33,5 +33,22 @@ namespace PdmObjectHandleTools
return destinationObjects;
}
// Delete all objects in the vector, then clear it.
//
// Use this instead of a hand written delete loop. Objects that are still owned by a
// PdmChildArrayField must not be deleted this way, call deleteChildren() on the field instead so
// the observer signals are disconnected first.
//
template <typename ObjectType>
void deleteObjects( std::vector<ObjectType*>& objects )
{
for ( auto* object : objects )
{
delete object;
}
objects.clear();
}
} //namespace PdmObjectHandleTools
} //namespace caf
+2
View File
@@ -38,6 +38,8 @@
#include <QTcpServer>
#include <thread>
using grpc::CompletionQueue;
using grpc::Server;
using grpc::ServerAsyncResponseWriter;