putil: Add new primitives for async callbacks/futures
These are a lot lighter than AsyncFuture and safer to use in C++ due to the reliance on RAII. An async method can now take a CompletionToken, which will implicitly accept an arbitrary callable or an AsyncFuture pointer.
This commit is contained in:
parent
67723ca06a
commit
2ea02ef0c8
|
|
@ -389,6 +389,17 @@ wake_task(AsyncTask *task) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal callback called when a CompletionToken created from this future
|
||||
* completes.
|
||||
*/
|
||||
void AsyncFuture::
|
||||
token_callback(Completable::Data *data, bool success) {
|
||||
AsyncFuture *future = (AsyncFuture *)data;
|
||||
future->set_result(EventParameter(success));
|
||||
unref_delete(future);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see AsyncFuture::gather
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
#include "eventParameter.h"
|
||||
#include "patomic.h"
|
||||
#include "small_vector.h"
|
||||
#include "completionToken.h"
|
||||
|
||||
class AsyncTaskManager;
|
||||
class AsyncTask;
|
||||
|
|
@ -58,7 +59,7 @@ class AsyncTask;
|
|||
*
|
||||
* @since 1.10.0
|
||||
*/
|
||||
class EXPCL_PANDA_EVENT AsyncFuture : public TypedReferenceCount {
|
||||
class EXPCL_PANDA_EVENT AsyncFuture : public TypedReferenceCount, protected Completable::Data {
|
||||
PUBLISHED:
|
||||
INLINE AsyncFuture();
|
||||
virtual ~AsyncFuture();
|
||||
|
|
@ -109,6 +110,8 @@ public:
|
|||
private:
|
||||
void wake_task(AsyncTask *task);
|
||||
|
||||
static void token_callback(Completable::Data *, bool success);
|
||||
|
||||
protected:
|
||||
enum FutureState : patomic_unsigned_lock_free::value_type {
|
||||
// Pending states
|
||||
|
|
@ -136,6 +139,7 @@ protected:
|
|||
|
||||
friend class AsyncGatheringFuture;
|
||||
friend class AsyncTaskChain;
|
||||
friend class CompletionToken;
|
||||
friend class PythonTask;
|
||||
|
||||
public:
|
||||
|
|
@ -199,6 +203,33 @@ private:
|
|||
static TypeHandle _type_handle;
|
||||
};
|
||||
|
||||
#ifndef CPPPARSER
|
||||
// Allow passing a future into a method accepting a CompletionToken.
|
||||
template<>
|
||||
INLINE CompletionToken::
|
||||
CompletionToken(AsyncFuture *future) {
|
||||
if (future != nullptr) {
|
||||
future->ref();
|
||||
_callback._data = future;
|
||||
if (_callback._data->_function == nullptr) {
|
||||
_callback._data->_function = &AsyncFuture::token_callback;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
template<>
|
||||
INLINE CompletionToken::
|
||||
CompletionToken(PT(AsyncFuture) future) {
|
||||
if (future != nullptr) {
|
||||
_callback._data = future;
|
||||
if (_callback._data->_function == nullptr) {
|
||||
_callback._data->_function = &AsyncFuture::token_callback;
|
||||
}
|
||||
future.cheat() = nullptr;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#include "asyncFuture.I"
|
||||
|
||||
#endif // !ASYNCFUTURE_H
|
||||
|
|
|
|||
|
|
@ -20,6 +20,9 @@ set(P3PUTIL_HEADERS
|
|||
clockObject.h clockObject.I
|
||||
collideMask.h
|
||||
colorSpace.h
|
||||
completable.I completable.h
|
||||
completionCounter.I completionCounter.h
|
||||
completionToken.I completionToken.h
|
||||
copyOnWriteObject.h copyOnWriteObject.I
|
||||
copyOnWritePointer.h copyOnWritePointer.I
|
||||
compareTo.I compareTo.h
|
||||
|
|
@ -86,6 +89,7 @@ set(P3PUTIL_SOURCES
|
|||
callbackObject.cxx
|
||||
clockObject.cxx
|
||||
colorSpace.cxx
|
||||
completionCounter.cxx
|
||||
copyOnWriteObject.cxx
|
||||
copyOnWritePointer.cxx
|
||||
config_putil.cxx configurable.cxx
|
||||
|
|
|
|||
|
|
@ -0,0 +1,61 @@
|
|||
/**
|
||||
* PANDA 3D SOFTWARE
|
||||
* Copyright (c) Carnegie Mellon University. All rights reserved.
|
||||
*
|
||||
* All use of this software is subject to the terms of the revised BSD
|
||||
* license. You should have received a copy of this license along
|
||||
* with this source code in a file named "LICENSE."
|
||||
*
|
||||
* @file completable.I
|
||||
* @author rdb
|
||||
* @date 2025-01-22
|
||||
*/
|
||||
|
||||
#ifndef CPPPARSER
|
||||
/**
|
||||
*
|
||||
*/
|
||||
template<class Callable>
|
||||
INLINE Completable::
|
||||
Completable(Callable callback) :
|
||||
_data(new LambdaData<Callable>(std::move(callback), [](Data *data, bool do_run) {
|
||||
LambdaData<Callable> *self = (LambdaData<Callable> *)data;
|
||||
if (do_run) {
|
||||
std::move(self->_lambda)();
|
||||
}
|
||||
delete self;
|
||||
})) {
|
||||
}
|
||||
#endif
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
INLINE Completable::
|
||||
Completable(Completable &&from) noexcept :
|
||||
_data(from._data) {
|
||||
from._data = nullptr;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
INLINE Completable::
|
||||
~Completable() {
|
||||
Data *data = _data;
|
||||
if (data != nullptr) {
|
||||
data->_function.load(std::memory_order_relaxed)(data, false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
INLINE void Completable::
|
||||
operator ()() {
|
||||
Data *data = _data;
|
||||
_data = nullptr;
|
||||
if (data != nullptr) {
|
||||
data->_function.load(std::memory_order_relaxed)(data, true);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
/**
|
||||
* PANDA 3D SOFTWARE
|
||||
* Copyright (c) Carnegie Mellon University. All rights reserved.
|
||||
*
|
||||
* All use of this software is subject to the terms of the revised BSD
|
||||
* license. You should have received a copy of this license along
|
||||
* with this source code in a file named "LICENSE."
|
||||
*
|
||||
* @file completable.h
|
||||
* @author rdb
|
||||
* @date 2025-01-22
|
||||
*/
|
||||
|
||||
#ifndef COMPLETABLE_H
|
||||
#define COMPLETABLE_H
|
||||
|
||||
#include "pandabase.h"
|
||||
#include "patomic.h"
|
||||
|
||||
/**
|
||||
* Stores a type-erased callable that is move-only. May only be called once.
|
||||
*/
|
||||
class EXPCL_PANDA_PUTIL Completable {
|
||||
public:
|
||||
constexpr Completable() = default;
|
||||
|
||||
#ifndef CPPPARSER
|
||||
template<class Callable>
|
||||
INLINE Completable(Callable callback);
|
||||
#endif
|
||||
|
||||
INLINE Completable(const Completable ©) = delete;
|
||||
INLINE Completable(Completable &&from) noexcept;
|
||||
|
||||
INLINE void operator ()();
|
||||
|
||||
INLINE ~Completable();
|
||||
|
||||
protected:
|
||||
// There are several design approaches here:
|
||||
// 1. Optimize for no data block: do not require dynamic allocation of a data
|
||||
// block in the simple case where the callback data is only the size of a
|
||||
// single pointer. Store two pointers, one function pointer and a data
|
||||
// pointer(-sized storage), directly on the class here.
|
||||
// 2. Optimize for a data block: store the function pointer on the data block,
|
||||
// always requiring dynamic allocation.
|
||||
//
|
||||
// Right now I have opted for 2 because it allows the function pointer to be
|
||||
// dynamically swapped (used in CompletionCounter), but this decision may
|
||||
// change in the future.
|
||||
|
||||
struct Data;
|
||||
typedef void CallbackFunction(Data *, bool);
|
||||
|
||||
struct Data {
|
||||
patomic<CallbackFunction *> _function { nullptr };
|
||||
};
|
||||
|
||||
template<typename Lambda>
|
||||
struct LambdaData : public Data {
|
||||
// Must unfortunately be defined inline, since this struct is protected.
|
||||
LambdaData(Lambda lambda, CallbackFunction *function) :
|
||||
_lambda(std::move(lambda)) {
|
||||
_function = function;
|
||||
}
|
||||
|
||||
Lambda _lambda;
|
||||
};
|
||||
|
||||
Data *_data = nullptr;
|
||||
|
||||
friend class AsyncFuture;
|
||||
friend class CompletionCounter;
|
||||
friend class CompletionToken;
|
||||
};
|
||||
|
||||
#include "completable.I"
|
||||
|
||||
#endif
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
/**
|
||||
* PANDA 3D SOFTWARE
|
||||
* Copyright (c) Carnegie Mellon University. All rights reserved.
|
||||
*
|
||||
* All use of this software is subject to the terms of the revised BSD
|
||||
* license. You should have received a copy of this license along
|
||||
* with this source code in a file named "LICENSE."
|
||||
*
|
||||
* @file completionCounter.I
|
||||
* @author rdb
|
||||
* @date 2025-01-22
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
INLINE CompletionCounter::
|
||||
~CompletionCounter() {
|
||||
CounterData *data = _data;
|
||||
if (data != nullptr) {
|
||||
// then() is not called; we still need something that destructs the data
|
||||
// when done.
|
||||
auto prev_function = data->_function.exchange(&abandon_callback, std::memory_order_relaxed);
|
||||
if (prev_function == nullptr) {
|
||||
// Was already done.
|
||||
delete data;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new token. May not be called after then().
|
||||
*/
|
||||
INLINE CompletionToken CompletionCounter::
|
||||
make_token() {
|
||||
CompletionToken token;
|
||||
if (_data == nullptr) {
|
||||
_data = new CounterData;
|
||||
_data->_function = &initial_callback;
|
||||
}
|
||||
auto old_value = _data->_counter.fetch_add(1);
|
||||
nassertr(old_value >= 0, token);
|
||||
token._callback._data = _data;
|
||||
return token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the given callback immediately upon completion. If the counter is
|
||||
* already done, runs it immediately. This requires an rvalue because it
|
||||
* consumes the counter, use std::move() if you don't have an rvalue.
|
||||
*
|
||||
* The callback will either be called immediately or directly when the last
|
||||
* token calls complete(), however, it may also be called if a token is
|
||||
* destroyed. This may happen at unexpected times, such as when the lambda
|
||||
* holding the token is destroyed prematurely. In this case, however, the
|
||||
* passed success argument will always be false.
|
||||
*/
|
||||
template<class Callable>
|
||||
INLINE void CompletionCounter::
|
||||
then(Callable callable) && {
|
||||
// Replace the callback pointer with something that calls the given callable
|
||||
// once the count reaches 0.
|
||||
CounterData *data = _data;
|
||||
nassertv(data != nullptr);
|
||||
_data = nullptr;
|
||||
if (data->_function.load(std::memory_order_acquire) == nullptr) {
|
||||
// Already done.
|
||||
callable((data->_counter.load(std::memory_order_relaxed) & ~0xffff) == 0);
|
||||
delete data;
|
||||
return;
|
||||
}
|
||||
|
||||
static_assert(sizeof(Callable) <= sizeof(data->_storage),
|
||||
"raise storage size in completionCounter.h or reduce lambda captures");
|
||||
|
||||
new (data->_storage) Callable(std::move(callable));
|
||||
|
||||
Completable::CallbackFunction *new_function =
|
||||
[] (Completable::Data *data_ptr, bool success) {
|
||||
CounterData *data = (CounterData *)data_ptr;
|
||||
auto prev_count = data->_counter.fetch_add((success ? 0 : 0x10000) - 1, std::memory_order_release);
|
||||
if ((short)(prev_count & 0xffff) > 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
Callable *callable = (Callable *)data->_storage;
|
||||
std::move(*callable)(success && (prev_count & ~0xffff) == 0);
|
||||
callable->~Callable();
|
||||
delete data;
|
||||
};
|
||||
|
||||
auto prev_function = data->_function.exchange(new_function, std::memory_order_acq_rel);
|
||||
if (UNLIKELY(prev_function == nullptr)) {
|
||||
// Last token finished in the meantime.
|
||||
new_function(data, (data->_counter.load(std::memory_order_relaxed) & ~0xffff) == 0);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
/**
|
||||
* PANDA 3D SOFTWARE
|
||||
* Copyright (c) Carnegie Mellon University. All rights reserved.
|
||||
*
|
||||
* All use of this software is subject to the terms of the revised BSD
|
||||
* license. You should have received a copy of this license along
|
||||
* with this source code in a file named "LICENSE."
|
||||
*
|
||||
* @file completionCounter.cxx
|
||||
* @author rdb
|
||||
* @date 2025-01-24
|
||||
*/
|
||||
|
||||
#include "completionCounter.h"
|
||||
|
||||
/**
|
||||
* Called when a token is completed before then() is called.
|
||||
*/
|
||||
void CompletionCounter::
|
||||
initial_callback(Completable::Data *data_ptr, bool success) {
|
||||
CounterData &data = *(CounterData *)data_ptr;
|
||||
auto prev_count = data._counter.fetch_add((success ? 0 : 0x10000) - 1, std::memory_order_release);
|
||||
if ((prev_count & 0xffff) == 1) {
|
||||
// We're done early.
|
||||
auto prev_callback = data._function.exchange(nullptr, std::memory_order_acq_rel);
|
||||
nassertv(prev_callback != nullptr);
|
||||
|
||||
// Someone called then() in the meantime. Call the new callback. The
|
||||
// refcount will drop below 0 when that's called but they are designed to
|
||||
// handle that.
|
||||
if (prev_callback != &initial_callback) {
|
||||
prev_callback(data_ptr, success && (prev_count & ~0xffff) == 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a token is completed after this object is destroyed without
|
||||
* then() being called.
|
||||
*/
|
||||
void CompletionCounter::
|
||||
abandon_callback(Completable::Data *data_ptr, bool success) {
|
||||
CounterData &data = *(CounterData *)data_ptr;
|
||||
auto prev_count = data._counter.fetch_sub(1, std::memory_order_relaxed);
|
||||
if ((prev_count & 0xffff) <= 1) {
|
||||
// Done.
|
||||
auto prev_callback = data._function.exchange(nullptr, std::memory_order_relaxed);
|
||||
nassertv(prev_callback != nullptr);
|
||||
nassertv(prev_callback == &abandon_callback);
|
||||
delete &data;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
/**
|
||||
* PANDA 3D SOFTWARE
|
||||
* Copyright (c) Carnegie Mellon University. All rights reserved.
|
||||
*
|
||||
* All use of this software is subject to the terms of the revised BSD
|
||||
* license. You should have received a copy of this license along
|
||||
* with this source code in a file named "LICENSE."
|
||||
*
|
||||
* @file completionCounter.h
|
||||
* @author rdb
|
||||
* @date 2025-01-22
|
||||
*/
|
||||
|
||||
#ifndef COMPLETIONCOUNTER_H
|
||||
#define COMPLETIONCOUNTER_H
|
||||
|
||||
#include "pandabase.h"
|
||||
#include "completionToken.h"
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
/**
|
||||
* Shared counter that generates "completion tokens" incrementing a counter,
|
||||
* which will decrement the counter once they are finished. After the tokens
|
||||
* are handed out, a callback may be registered using then(), which will be
|
||||
* called as soon as the last token is done.
|
||||
*/
|
||||
class EXPCL_PANDA_PUTIL CompletionCounter {
|
||||
public:
|
||||
constexpr CompletionCounter() = default;
|
||||
CompletionCounter(const CompletionCounter ©) = delete;
|
||||
|
||||
INLINE ~CompletionCounter();
|
||||
|
||||
INLINE CompletionToken make_token();
|
||||
|
||||
template<class Callable>
|
||||
INLINE void then(Callable callable) &&;
|
||||
|
||||
private:
|
||||
static void initial_callback(Completable::Data *data, bool success);
|
||||
static void abandon_callback(Completable::Data *data, bool success);
|
||||
|
||||
protected:
|
||||
struct CounterData : public Completable::Data {
|
||||
// Least significant half is counter, most significant half is error count
|
||||
patomic_signed_lock_free _counter { 0 };
|
||||
|
||||
// Just raise this if the static_assert fires (or limit the size of your
|
||||
// lambda captures).
|
||||
alignas(std::max_align_t) unsigned char _storage[64];
|
||||
};
|
||||
CounterData *_data = nullptr;
|
||||
};
|
||||
|
||||
#include "completionCounter.I"
|
||||
|
||||
#endif
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
/**
|
||||
* PANDA 3D SOFTWARE
|
||||
* Copyright (c) Carnegie Mellon University. All rights reserved.
|
||||
*
|
||||
* All use of this software is subject to the terms of the revised BSD
|
||||
* license. You should have received a copy of this license along
|
||||
* with this source code in a file named "LICENSE."
|
||||
*
|
||||
* @file completionToken.I
|
||||
* @author rdb
|
||||
* @date 2025-01-22
|
||||
*/
|
||||
|
||||
#ifndef CPPPARSER
|
||||
/**
|
||||
* Creates a token that calls the given callback when it's done, passing it
|
||||
* true on success and false on failure or abandonment.
|
||||
*/
|
||||
template<class Callable>
|
||||
INLINE CompletionToken::
|
||||
CompletionToken(Callable callback) {
|
||||
// Main difference over a Completable is that this will always call the
|
||||
// callback, even on failure, so that cleanup can be done.
|
||||
_callback._data = new Completable::LambdaData<Callable>(std::move(callback), [](Completable::Data *data, bool success) {
|
||||
Completable::LambdaData<Callable> *self = (Completable::LambdaData<Callable> *)data;
|
||||
std::move(self->_lambda)(success);
|
||||
delete self;
|
||||
});
|
||||
}
|
||||
#endif
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
INLINE void CompletionToken::
|
||||
complete(bool success) {
|
||||
Completable::Data *data = _callback._data;
|
||||
if (data != nullptr) {
|
||||
_callback._data = nullptr;
|
||||
data->_function.load(std::memory_order_relaxed)(data, success);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
/**
|
||||
* PANDA 3D SOFTWARE
|
||||
* Copyright (c) Carnegie Mellon University. All rights reserved.
|
||||
*
|
||||
* All use of this software is subject to the terms of the revised BSD
|
||||
* license. You should have received a copy of this license along
|
||||
* with this source code in a file named "LICENSE."
|
||||
*
|
||||
* @file completionToken.h
|
||||
* @author rdb
|
||||
* @date 2025-01-22
|
||||
*/
|
||||
|
||||
#ifndef COMPLETIONTOKEN_H
|
||||
#define COMPLETIONTOKEN_H
|
||||
|
||||
#include "pandabase.h"
|
||||
#include "pnotify.h"
|
||||
#include "completable.h"
|
||||
|
||||
/**
|
||||
* A completion token can be created from a callback, future or
|
||||
* CompletionCounter and can be passed into an asynchronous operation in order
|
||||
* to receive a signal when it is done.
|
||||
*
|
||||
* The asynchronous operation should call complete() on it when it is done,
|
||||
* with a boolean value indicating success or failure. If the token is
|
||||
* destroyed prematurely, it is treated as if it called complete(false).
|
||||
*
|
||||
* This should be preferred over passing an AsyncFuture into a method since
|
||||
* a CompletionToken provides both more flexibility in use (due to accepting
|
||||
* an arbitrary callback) and more safety (since the RAII semantics guarantees
|
||||
* that the callback is never silently dropped).
|
||||
*
|
||||
* The token may only be moved, not copied.
|
||||
*/
|
||||
class EXPCL_PANDA_PUTIL CompletionToken {
|
||||
public:
|
||||
constexpr CompletionToken() = default;
|
||||
|
||||
#ifndef CPPPARSER
|
||||
template<class Callable>
|
||||
INLINE CompletionToken(Callable callback);
|
||||
#endif
|
||||
|
||||
void complete(bool success);
|
||||
|
||||
protected:
|
||||
Completable _callback;
|
||||
|
||||
friend class CompletionCounter;
|
||||
};
|
||||
|
||||
#include "completionToken.I"
|
||||
|
||||
#endif
|
||||
|
|
@ -17,6 +17,7 @@
|
|||
#include "callbackObject.cxx"
|
||||
#include "clockObject.cxx"
|
||||
#include "colorSpace.cxx"
|
||||
#include "completionCounter.cxx"
|
||||
#include "config_putil.cxx"
|
||||
#include "configurable.cxx"
|
||||
#include "copyOnWriteObject.cxx"
|
||||
|
|
|
|||
Loading…
Reference in New Issue