gobject: new API with destroy notify for thread-safe use of g_object_weak_ref()

Add API g_object_weak_ref_full() and g_object_weak_unref_full().

The documentation elaborates how g_object_weak_ref() cannot be used
(naively) in a thread-safe way, when another thread is invoking dispose
(e.g. by running g_object_run_dispose() or by unrefing the last
reference). The suggestion is to combine that with GWeakRef.

Note that usually you cannot use GWeakRef alone, since you get no
notification when the reference count drop to zero. So a complete
solution uses g_object_weak_ref() and GWeakRef together.

The problem is that the user must not call g_object_weak_unref() when
another thread might just dispose() the instance. Presumably, they use
GWeakRef to first obtain a strong reference. That ensures that no other
thread can trigger dispose via a g_object_unref(). That however does not
help against another thread calling g_object_run_dispose().

In fact, there is no way to safely call g_object_weak_unref() when
another thread might call g_object_run_dispose() (unless the user
coordinates/synchronizes that at a higher level). The new API can avoid
that problem.

Note that calling g_object_weak_unref() asserts that it finds the
notification. Which might be violated by another thread calling
g_object_run_dispose() at the wrong time. But the assertion is not the
only problem. Often, the weak notification might hold resources (e.g.
an allocated data) that needs to be released -- either by the weak
notification callback or during g_object_weak_unref().

For example, see "gobject/gbinding.c". It calls `g_object_weak_unref
(source, weak_unbind, context);` while holding a strong reference on
source. But if another thread runs g_object_run_dispose() at the wrong
moment, we will call binding_context_unref() twice (and crash).

You might think, we cannot possibly support that another thread calls
g_object_run_dispose() while we still want to do something on an object.
Maybe in many cases that is not supportable. However, the user could
have a thread-safe GObject that handles that correctly. Also, see for
example GBinding, which operates on the basics of GObject (weak
notifications, GWeakRef, property notification subscription). Those
parts all all supposed to be thred-safe, and a low-level object like
GBinding should also work with objects that can be disposed by other
threads (including via g_object_run_dispose()). In general, while it's
often difficult to support multi threadded g_object_run_dispose(), the
basic g_object_weak_ref() should not fail to support such pattern.

Add new API that solves this. g_object_weak_ref_full() takes a
GDestroyNotify, which is guaranteed to be called exactly once -- unless
the data is stolen during g_object_weak_unref_full(). This can be
convenient for cleanup. But more importantly, doing the cleanup once is
also a place to synchronize and ensure we do something on the object,
while it is still alive.

We may combine g_object_weak_ref_full() with GWeakRef for a thread-safe
solution, like GBinding does. But you can now also have a fully
thread-safe solution without using GWeakRef. The benefit of that is that
you never need to acquire a strong reference on the object and never
emit a toggle notification, while still knowing the object is alive. The
unit test shows how that can be done.

The downside is that the size of WeakRefTuple increases by one pointer
size. Optimizing that would be cumbersome and is not done.
This commit is contained in:
Thomas Haller
2025-04-14 09:40:30 +02:00
parent 9b303ee5b3
commit 5cc532765d
3 changed files with 334 additions and 17 deletions

View File

@@ -3712,6 +3712,7 @@ g_object_disconnect (gpointer _object,
typedef struct
{
GWeakNotify notify;
GDestroyNotify destroy;
gpointer data;
} WeakRefTuple;
@@ -3873,6 +3874,70 @@ g_object_weak_ref_cb (gpointer *data,
return NULL;
}
G_ALWAYS_INLINE static inline void
_g_object_weak_ref_full (GObject *object,
GWeakNotify notify,
gpointer data,
GDestroyNotify destroy)
{
_g_datalist_id_update_atomic (&object->qdata,
quark_weak_notifies,
g_object_weak_ref_cb,
&((WeakRefTuple){
.notify = notify,
.data = data,
.destroy = destroy,
}));
}
/**
* g_object_weak_unref_full: (skip)
* @object: #GObject to remove a weak reference from
* @notify: callback to search for
* @data: data to search for
* @destroy: an optional #GDestroyNotify.
*
* Adds a weak reference callback to an object. Weak references are
* used for notification when an object is disposed. They are called
* "weak references" because they allow you to safely hold a pointer
* to an object without calling g_object_ref() (g_object_ref() adds a
* strong reference, that is, forces the object to stay alive).
*
* The @notify callback is invoked on the thread that disposes the object,
* either by releasing the last reference or by calling g_object_run_dispose().
* It is guaranteed that the @notify callback is invoked zero or one time.
* When g_object_weak_unref_full() returns %TRUE, the @notify callback was
* successfully unregistered and never invoked. If it returns %FALSE, it
* was already or will still be invoked.
*
* You are guaranteed, that the @destroy callback is called exactly once,
* unless you pass successfully steal the data during
* g_object_weak_unref_full(). If it is invoked, it happens either right after
* the @notify callback or during the g_object_weak_unref() call.
*
* Without careful actions, weak notifications are not thread-safe if the
* dispose can happen on anther thread. You might combine weak notifications
* with #GWeakRef to solve that partly. However, obtaining a strong reference
* via #GWeakRef does not guard against another thread calling
* g_object_run_dispose(). A fully thread-safe solution can use a @destroy
* callback here, which is guaranteed to be executed once and act as a
* synchronization point.
*
* Since: 2.86
*/
void
g_object_weak_ref_full (GObject *object,
GWeakNotify notify,
gpointer data,
GDestroyNotify destroy)
{
g_return_if_fail (G_IS_OBJECT (object));
g_return_if_fail (notify != NULL);
g_return_if_fail (g_atomic_int_get (&object->ref_count) >= 1);
_g_object_weak_ref_full (object, notify, data, destroy);
}
/**
* g_object_weak_ref: (skip)
* @object: #GObject to reference weakly
@@ -3888,7 +3953,9 @@ g_object_weak_ref_cb (gpointer *data,
* Note that the weak references created by this method are not
* thread-safe: they cannot safely be used in one thread if the
* object's last g_object_unref() might happen in another thread.
* Use #GWeakRef if thread-safety is required.
*
* Use #GWeakRef or g_object_weak_ref_full() with suitable callbacks if
* thread-safety is required.
*/
void
g_object_weak_ref (GObject *object,
@@ -3899,13 +3966,7 @@ g_object_weak_ref (GObject *object,
g_return_if_fail (notify != NULL);
g_return_if_fail (g_atomic_int_get (&object->ref_count) >= 1);
_g_datalist_id_update_atomic (&object->qdata,
quark_weak_notifies,
g_object_weak_ref_cb,
&((WeakRefTuple){
.notify = notify,
.data = data,
}));
_g_object_weak_ref_full (object, notify, data, NULL);
}
static gpointer
@@ -3927,11 +3988,12 @@ g_object_weak_unref_cb (gpointer *data,
}
}
g_critical ("%s: couldn't find weak ref %p(%p)", G_STRFUNC, tuple->notify, tuple->data);
return NULL;
handle_weak_ref_found:
tuple->destroy = wstack->weak_refs[idx].destroy;
_weak_ref_stack_update_release_all_state (wstack, idx);
wstack->n_weak_refs -= 1;
@@ -3952,7 +4014,68 @@ handle_weak_ref_found:
_weak_ref_stack_maybe_shrink (&wstack, data);
}
return NULL;
return tuple;
}
G_ALWAYS_INLINE static inline gboolean
_g_object_weak_unref_full (GObject *object,
GWeakNotify notify,
gpointer data,
gboolean steal_data)
{
WeakRefTuple tuple = {
.notify = notify,
.data = data,
.destroy = NULL,
};
gpointer result;
result = _g_datalist_id_update_atomic (&object->qdata,
quark_weak_notifies,
g_object_weak_unref_cb,
&tuple);
if (!result)
return FALSE;
if (!steal_data && tuple.destroy)
tuple.destroy (data);
return TRUE;
}
/**
* g_object_weak_unref_full: (skip)
* @object: #GObject to remove a weak reference from
* @notify: callback to search for
* @data: data to search for
* @steal_data: whether to not invoke the #GDestroyNotify and steal the data
*
* Removes a weak reference callback to an object.
*
* Unlike g_object_weak_unref(), it is acceptable that there is nothing to
* unregister. That might be due to a race where the object is disposed on
* another thread. You can tell based on the return value whether that was the
* case.
*
* Depending on @steal_data and whether the weak notification is still
* registered, the destroy notification from g_object_weak_ref_full() will be
* called on the data.
*
* Since: 2.86
*
* Returns: %TRUE if a weak notification was found and unregistered without
* ever being invoked.
*/
gboolean
g_object_weak_unref_full (GObject *object,
GWeakNotify notify,
gpointer data,
gboolean steal_data)
{
g_return_val_if_fail (G_IS_OBJECT (object), FALSE);
g_return_val_if_fail (notify != NULL, FALSE);
return _g_object_weak_unref_full (object, notify, data, steal_data);
}
/**
@@ -3962,22 +4085,30 @@ handle_weak_ref_found:
* @data: data to search for
*
* Removes a weak reference callback to an object.
*
* Note that it is a bug to call GObjectClass.weak_unref() for a non-registered
* weak notification. If other threads can dispose the objects, this condition
* can be violated due to a race. Avoid that by using
* GObjectClass.weak_unref_full() which gracefully accepts that the
* notification may not be registered.
*
* GObjectClass.weak_unref() will destroy the data, if a #GDestroyNotify
* was provided during GObjectClass.weak_ref_full().
*/
void
g_object_weak_unref (GObject *object,
GWeakNotify notify,
gpointer data)
{
gboolean weak_ref_found;
g_return_if_fail (G_IS_OBJECT (object));
g_return_if_fail (notify != NULL);
_g_datalist_id_update_atomic (&object->qdata,
quark_weak_notifies,
g_object_weak_unref_cb,
&((WeakRefTuple){
.notify = notify,
.data = data,
}));
weak_ref_found = _g_object_weak_unref_full (object, notify, data, FALSE);
if (G_UNLIKELY (!weak_ref_found))
g_critical ("%s: couldn't find weak ref %p(%p)", G_STRFUNC, notify, data);
}
typedef struct
@@ -4110,6 +4241,9 @@ g_object_weak_release_all (GObject *object, gboolean release_all)
wdata.tuple.notify (wdata.tuple.data, object);
if (wdata.tuple.destroy)
wdata.tuple.destroy (wdata.tuple.data);
if (wdata.release_all_done)
break;
}

View File

@@ -517,10 +517,20 @@ GOBJECT_AVAILABLE_IN_ALL
void g_object_weak_ref (GObject *object,
GWeakNotify notify,
gpointer data);
GOBJECT_AVAILABLE_IN_2_86
void g_object_weak_ref_full (GObject *object,
GWeakNotify notify,
gpointer data,
GDestroyNotify destroy);
GOBJECT_AVAILABLE_IN_ALL
void g_object_weak_unref (GObject *object,
GWeakNotify notify,
gpointer data);
GOBJECT_AVAILABLE_IN_2_86
gboolean g_object_weak_unref_full (GObject *object,
GWeakNotify notify,
gpointer data,
gboolean steal_data);
GOBJECT_AVAILABLE_IN_ALL
void g_object_add_weak_pointer (GObject *object,
gpointer *weak_pointer_location);

View File

@@ -549,6 +549,178 @@ test_references_run_dispose (void)
/*****************************************************************************/
static gint _ref_thread_safe_weak_was_invoked = 0;
typedef struct
{
gint ref_count;
GMutex mutex;
/* This represents the action that is taken by either the weak notification
* xor the thread that calls g_object_weak_unref_full().
*
* The point is that there is a guarantee that exactly one of the two parties
* performs this action. This is what g_object_weak_ref_full() allows, to
* have the action thread safe.
*
* Obviously, the action happens on two different threads, so you will need
* some form of synchronization around it (e.g. a mutex). */
const char *synchronized_action;
} RefThreadSafeData;
static gpointer
_ref_thread_safe_thread_fcn (gpointer thread_data)
{
g_object_unref (thread_data);
return NULL;
}
static void
_ref_thread_safe_weak_cb (gpointer data,
GObject *where_the_object_was)
{
RefThreadSafeData *wdata = data;
if (!g_atomic_int_compare_and_exchange (&_ref_thread_safe_weak_was_invoked, 0, 1))
g_assert_not_reached ();
g_mutex_lock (&wdata->mutex);
if (!wdata->synchronized_action)
wdata->synchronized_action = "_ref_thread_safe_weak_cb";
g_mutex_unlock (&wdata->mutex);
g_usleep (100);
if (!g_atomic_int_compare_and_exchange (&_ref_thread_safe_weak_was_invoked, 1, 2))
g_assert_not_reached ();
}
static RefThreadSafeData *
_ref_thread_safe_ref (gpointer data)
{
RefThreadSafeData *wdata = data;
g_atomic_int_inc (&wdata->ref_count);
return data;
}
static void
_ref_thread_safe_unref (gpointer data)
{
RefThreadSafeData *wdata = data;
if (g_atomic_int_dec_and_test (&wdata->ref_count))
{
g_mutex_clear (&wdata->mutex);
g_free (wdata);
}
}
static void
test_references_thread_safe (void)
{
GThread *thread;
GObject *obj;
const char *called_synchronized_action;
RefThreadSafeData *wdata;
gint usec_sleep;
gboolean should_steal_data = g_random_boolean ();
gboolean did_steal_data = FALSE;
gboolean did_unref = FALSE;
int was_invoked;
obj = g_object_new (G_TYPE_OBJECT, NULL);
wdata = g_new (RefThreadSafeData, 1);
*wdata = (RefThreadSafeData){
.ref_count = 1,
.synchronized_action = NULL,
};
g_mutex_init (&wdata->mutex);
/* To test and use g_object_weak_ref_full() in a thread safe way we pass on
* an (atomic) reference to our RefThreadSafeData. In this case, the whole
* setup is elaborate and cumbersome.
*
* Note that a real user who subscribes to a weak notification does so for a
* reason. They must already track state about what they doing (only so that
* they can call g_object_weak_unref_full() later. At that point, they will
* already have part of this in place. It will be easier for such a real
* world usage to extend their code to use g_object_weak_ref_full() in a
* thread safe way, than it is for our test here.
*/
g_object_weak_ref_full (obj, _ref_thread_safe_weak_cb, _ref_thread_safe_ref (wdata), _ref_thread_safe_unref);
thread = g_thread_new ("run-dispose", _ref_thread_safe_thread_fcn, obj);
usec_sleep = g_random_int_range (-1, 20);
if (usec_sleep != -1)
g_usleep ((gulong) usec_sleep);
g_mutex_lock (&wdata->mutex);
if (!wdata->synchronized_action)
{
/* We want to call g_object_weak_unref_full(), for that that we must be
* sure that the object is still alive.
*
* We could use a GWeakRef (in addition) to that to get a strong
* reference first.
*
* Instead, we can synchronize with the weak notification. If the weak
* notification at this point (while holding the mutex), did not yet run,
* we know that the object is still alive. We know this even without
* acquiring a strong reference(!) and thus without emitting a toggle
* notification.
*/
did_unref = g_object_weak_unref_full (obj, _ref_thread_safe_weak_cb, wdata, should_steal_data);
if (did_unref)
did_steal_data = should_steal_data;
wdata->synchronized_action = "main_thread";
}
called_synchronized_action = wdata->synchronized_action;
g_mutex_unlock (&wdata->mutex);
/* At this point, exactly one thread ran the action ("main_thread" or
* "_ref_thread_safe_weak_cb") while under a mutex. We also safely
* called g_object_weak_unref_full().
*
* Note that if g_object_weak_unref_full() returned FALSE, then in parallel
* _ref_thread_safe_weak_cb() callback might still be running and access
* wdata. But the weak notification callback will also obtain the mutex, and
* see that it should do nothing further. */
g_assert_cmpstr (called_synchronized_action, !=, NULL);
was_invoked = g_atomic_int_get (&_ref_thread_safe_weak_was_invoked);
if (did_unref)
{
/* If g_object_weak_unref_full() did unregister the notification, the
* callback is not running. */
g_assert_cmpint (was_invoked, ==, 0);
}
if (g_str_equal (called_synchronized_action, "_ref_thread_safe_weak_cb"))
{
/* was_invoked must be at least 1 (maybe not yet 2). */
g_assert_cmpint (was_invoked, >=, 1);
}
if (did_steal_data)
{
/* We successfully stole the data. Must do the additional unref here. */
_ref_thread_safe_unref (wdata);
}
_ref_thread_safe_unref (wdata);
g_thread_join (thread);
/* _ref_thread_safe_weak_cb() ran to completion iff we did not successfully
* g_object_weak_unref_full(). */
g_assert_cmpint (g_atomic_int_get (&_ref_thread_safe_weak_was_invoked), ==, (did_unref ? 0 : 2));
}
/*****************************************************************************/
int
main (int argc,
char *argv[])
@@ -563,6 +735,7 @@ main (int argc,
g_test_add_func ("/gobject/references-many", test_references_many);
g_test_add_func ("/gobject/references_two", test_references_two);
g_test_add_func ("/gobject/references_run_dispose", test_references_run_dispose);
g_test_add_func ("/gobject/references_thread_safe", test_references_thread_safe);
return g_test_run ();
}