|
| 1 | +/* |
| 2 | + * Copyright 2025 The Android Open Source Project |
| 3 | + * |
| 4 | + * Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | + * you may not use this file except in compliance with the License. |
| 6 | + * You may obtain a copy of the License at |
| 7 | + * |
| 8 | + * http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | + * |
| 10 | + * Unless required by applicable law or agreed to in writing, software |
| 11 | + * distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | + * See the License for the specific language governing permissions and |
| 14 | + * limitations under the License. |
| 15 | + */ |
| 16 | + |
| 17 | +#pragma once |
| 18 | + |
| 19 | +#include <cstddef> |
| 20 | +#include <functional> |
| 21 | +#include <memory> |
| 22 | +#include <mutex> |
| 23 | +#include <optional> |
| 24 | +#include <type_traits> |
| 25 | +#include <utility> |
| 26 | + |
| 27 | +#include <android-base/thread_annotations.h> |
| 28 | +#include <ftl/finalizer.h> |
| 29 | +#include <ftl/function.h> |
| 30 | + |
| 31 | +namespace android::surfaceflinger::tests::end2end::test_framework::core { |
| 32 | + |
| 33 | +// Define a function wrapper class that has some special features to make it async safe. |
| 34 | +// |
| 35 | +// 1) The contained function is only called on one thread at a time. |
| 36 | +// 2) The contained function can be safely replaced at any time. |
| 37 | +// 3) This wrapper helps ensure that after replacement, all calls to the replaced function are |
| 38 | +// complete by a well-defined synchronization point, which can be deferred to happen outside of |
| 39 | +// mutex locks that might otherwise cause a deadlock. |
| 40 | +// |
| 41 | +// To achieve the last feature, the `set` function to perform replacement returns a special |
| 42 | +// `Finalizer` instance which is either automatically invoked on destruction, or on demand via |
| 43 | +// `operator()` with no arguments (and returning no value). When invoked, the finalizer waits for |
| 44 | +// any calls to the replaced function to complete, and as the finalizer can be moved if needed, this |
| 45 | +// wait can be done without other mutexes being held that might cause a deadlock in the replaced |
| 46 | +// function. |
| 47 | +// |
| 48 | +// Once the finalizer completes, any resources needed only by the previous function can be |
| 49 | +// safely destroyed. The finalizer also destroys any captured state that was part of the |
| 50 | +// previous function. |
| 51 | +// |
| 52 | +// Note that the target function that is called by this wrapper is allowed to replace the function |
| 53 | +// this wrapper contains. When this happens there is no synchronization point, as waiting for the |
| 54 | +// replaced function to complete as part of what is executed while it is invoked would be a |
| 55 | +// deadlock. For this case, the returned finalizer instance is a no-op if invoked. Instead the |
| 56 | +// capture for the prior function will be destroyed when the control returns back to the wrapper, |
| 57 | +// before control returns to the code that invoked the wrapper. |
| 58 | +// |
| 59 | +// Instances of this class can be default constructed, and invoking the contained function in this |
| 60 | +// state does nothing, and also requires no synchronization point when replaced by an actual target. |
| 61 | +// After being set, this state can be entered again by using the `clear()` member function. |
| 62 | +// |
| 63 | +// If the contained function has a return type T other than void, the return type of the wrapper |
| 64 | +// will be std::optional<T>. If there is no target set, invoking the wrapper will return a |
| 65 | +// std::nullopt value, otherwise it will return an optional with the value set to the value returned |
| 66 | +// by the contained function. |
| 67 | +// |
| 68 | +// Usage: |
| 69 | +// |
| 70 | +// AsyncFunctionStd<void()> function = [this](){ |
| 71 | +// std_lock_guard lock(mutex); |
| 72 | +// someMemberFunction(); |
| 73 | +// }; |
| 74 | +// |
| 75 | +// function(); // Invokes someMemberFunction(); |
| 76 | +// |
| 77 | +// // May invoke someMemberFunction or otherMemberFunction (set below). |
| 78 | +// std::async(std::launch::async, function); |
| 79 | +// |
| 80 | +// ftl::AsyncFunctionStd<void()>::Finalizer finalizer; |
| 81 | +// { |
| 82 | +// std_lock_guard lock(mutex); |
| 83 | +// finalizer = function.set([this](){ |
| 84 | +// std_lock_guard lock(mutex); |
| 85 | +// otherMemberFunction(); |
| 86 | +// }); |
| 87 | +// // do not invoke the finalizer with locks held, unless there is no chance of a deadlock. |
| 88 | +// } |
| 89 | +// function() // Invokes instance2->otherMemberFunction(); |
| 90 | +// finalizer(); // Waits for calls to someMemberFunction to complete |
| 91 | +// // It is now safe to destroy resources that are used by someMemberFunction. |
| 92 | +// |
| 93 | +// std::ignore = function.clear(); // Clear the function and implicitly invoke the returned |
| 94 | +// finalizer. |
| 95 | +// // It is now safe to destroy resource that used used by otherMemberFunction, including |
| 96 | +// // 'this' if desired. |
| 97 | +// |
| 98 | +template <typename Function> |
| 99 | +class AsyncFunction final { |
| 100 | + struct SharedFunction; |
| 101 | + |
| 102 | + // Turns some return type `T` into `std::optional<T>`, unless `T` is `void`. |
| 103 | + template <typename T> |
| 104 | + using AddOptionalUnlessVoid = std::conditional_t<std::is_void_v<T>, T, std::optional<T>>; |
| 105 | + |
| 106 | + public: |
| 107 | + using Finalizer = ftl::FinalizerStd; |
| 108 | + |
| 109 | + // The return type from `operator()`. |
| 110 | + using result_type = AddOptionalUnlessVoid<typename Function::result_type>; |
| 111 | + |
| 112 | + // Default construct an empty state. |
| 113 | + AsyncFunction() = default; |
| 114 | + |
| 115 | + ~AsyncFunction() { |
| 116 | + // Ensure any outstanding calls complete before teardown by clearing the shared_ptr. Note |
| 117 | + // however if there are any, there would likely be other problems since the owning class is |
| 118 | + // in the process of being destroyed. |
| 119 | + setInternal(nullptr)(); |
| 120 | + } |
| 121 | + |
| 122 | + // For simplicity, copying and moving are not possible. |
| 123 | + AsyncFunction(const AsyncFunction&) = delete; |
| 124 | + auto operator=(const AsyncFunction&) = delete; |
| 125 | + AsyncFunction(AsyncFunction&&) = delete; |
| 126 | + auto operator=(AsyncFunction&&) = delete; |
| 127 | + |
| 128 | + // Constructs an AsyncFunction from the function type. |
| 129 | + template <typename NewFunction> |
| 130 | + requires(!std::is_same_v<std::remove_cvref_t<NewFunction>, AsyncFunction> && |
| 131 | + std::is_constructible_v<Function, NewFunction>) |
| 132 | + // NOLINTNEXTLINE(google-explicit-constructor) |
| 133 | + explicit(false) AsyncFunction(NewFunction&& function) |
| 134 | + : mShared(std::make_shared<SharedFunction>(std::forward<NewFunction>(function))) {} |
| 135 | + |
| 136 | + // Replaces the contained function value with a new one. |
| 137 | + // |
| 138 | + // Returns a finalizer which when invoked waits for calls to the old function value |
| 139 | + // complete. This is done so that the caller can invoke the caller without locks held |
| 140 | + // that might block the call from completing, |
| 141 | + template <typename NewFunction> |
| 142 | + requires(std::is_constructible_v<Function, NewFunction>) |
| 143 | + [[nodiscard]] auto set(NewFunction&& function) -> Finalizer { |
| 144 | + return setInternal(std::make_shared<SharedFunction>(std::forward<NewFunction>(function))); |
| 145 | + } |
| 146 | + |
| 147 | + // Clears the contained function value. |
| 148 | + // |
| 149 | + // Returns a finalizer which when invoked waits for calls to the old function value |
| 150 | + // complete. This is done so that the caller can invoke the caller without locks held |
| 151 | + // that might block the call from completing, |
| 152 | + [[nodiscard]] auto clear() -> Finalizer { return setInternal(nullptr); } |
| 153 | + |
| 154 | + // Invoke the contained function, if set. |
| 155 | + template <typename... Args> |
| 156 | + requires(std::is_invocable_v<Function, Args...>) |
| 157 | + auto operator()(Args&&... args) const -> result_type { |
| 158 | + // We might need to retry the process to forward the call if we happen to obtain a zombie |
| 159 | + // shared_ptr. We try at least once. |
| 160 | + bool retry = true; |
| 161 | + while (std::exchange(retry, false)) { |
| 162 | + // To avoid deadlocks, the call to the contained function must be made on a copy of the |
| 163 | + // shared_ptr without the internal locks on the source shared_ptr member data. |
| 164 | + const auto shared = copy(); |
| 165 | + |
| 166 | + // Confirm we got a non-null pointer before continuing. The pointer can be null if no |
| 167 | + // target is set. |
| 168 | + if (shared == nullptr) { |
| 169 | + break; |
| 170 | + } |
| 171 | + |
| 172 | + // We must hold shared->callingMutex before accessing the other fields it contains. |
| 173 | + std::lock_guard lock(shared->callingMutex); |
| 174 | + |
| 175 | + // Now that the calling mutex is held, confirm it is valid to use. |
| 176 | + if (!shared->valid) [[unlikely]] { |
| 177 | + // If the pointer isn't valid, we must retry to get a new copy. |
| 178 | + // It indicates another thread set a new target by setting a new pointer after we |
| 179 | + // made our copy, but before we acquired `callingMutex`. Our pointer is effectively |
| 180 | + // a zombie, and must not be used. |
| 181 | + retry = true; |
| 182 | + continue; |
| 183 | + } |
| 184 | + |
| 185 | + // If `Function` can be (possibly explicitly) converted to bool, it is used as a check |
| 186 | + // at runtime that the function is safe to invoke. |
| 187 | + if constexpr (std::is_constructible_v<bool, Function>) { |
| 188 | + if (!shared->function) { |
| 189 | + break; |
| 190 | + } |
| 191 | + } |
| 192 | + |
| 193 | + // Forward the call. Note that callingMutex must be held for the duration of the call. |
| 194 | + return std::invoke(shared->function, std::forward<Args>(args)...); |
| 195 | + } |
| 196 | + |
| 197 | + // If we reached this point, we had no target to invoke. |
| 198 | + if constexpr (!std::is_void_v<std::invoke_result_t<Function, Args...>>) { |
| 199 | + // If the function was supposed to return a value, we explicitly return `std::nullopt` |
| 200 | + // rather than manufacturing a value of some arbitrary type (perhaps by default |
| 201 | + // construction). |
| 202 | + return std::nullopt; |
| 203 | + } |
| 204 | + } |
| 205 | + |
| 206 | + private: |
| 207 | + struct SharedFunction final { |
| 208 | + SharedFunction() = default; |
| 209 | + |
| 210 | + template <typename NewFunction> |
| 211 | + requires(!std::is_same_v<NewFunction, SharedFunction>) |
| 212 | + explicit SharedFunction(NewFunction&& newFunction) |
| 213 | + : function(std::forward<NewFunction>(newFunction)) {} |
| 214 | + |
| 215 | + // NOLINTBEGIN(misc-non-private-member-variables-in-classes) |
| 216 | + |
| 217 | + // `callingMutex` must be held for the duration that `function` is invoked. |
| 218 | + mutable std::recursive_mutex callingMutex; |
| 219 | + const Function function; // NOLINT(cppcoreguidelines-avoid-const-or-ref-data-members) |
| 220 | + // If valid is false, it means the shared pointer was exchanged to point at a new value, and |
| 221 | + // the function here is no longer safe to invoke. |
| 222 | + bool valid = true; // GUARDED_BY(callingMutex) |
| 223 | + |
| 224 | + // NOLINTEND(misc-non-private-member-variables-in-classes) |
| 225 | + }; |
| 226 | + |
| 227 | + [[nodiscard]] auto setInternal(std::shared_ptr<SharedFunction> newShared) -> Finalizer { |
| 228 | + // To avoid deadlocks, the old instance MUST be destroyed outside of all locks, including |
| 229 | + // locks held by the caller. It is the caller's responsibility to invoke the returned |
| 230 | + // finalizer outside of any locks being holds. |
| 231 | + std::shared_ptr<SharedFunction> prior = exchange(std::move(newShared)); |
| 232 | + |
| 233 | + return Finalizer([prior = std::move(prior)]() { |
| 234 | + if (prior) { |
| 235 | + // Wait for any call to complete. |
| 236 | + // Note that callingMutex is a recursive_mutex so that we won't deadlock if the |
| 237 | + // current thread is already holding the same lock. |
| 238 | + std::lock_guard lock(prior->callingMutex); |
| 239 | + // Mark the function as no longer valid to call, on the off chance another thread |
| 240 | + // obtained a copy just before this thread did the exchange. |
| 241 | + prior->valid = false; |
| 242 | + } |
| 243 | + }); |
| 244 | + } |
| 245 | + |
| 246 | + [[nodiscard]] auto exchange(std::shared_ptr<SharedFunction>&& newShared) |
| 247 | + -> std::shared_ptr<SharedFunction> { |
| 248 | + std::lock_guard lock(mMutex); |
| 249 | + return std::exchange(mShared, std::move(newShared)); |
| 250 | + } |
| 251 | + |
| 252 | + [[nodiscard]] auto copy() const -> std::shared_ptr<SharedFunction> { |
| 253 | + std::lock_guard lock(mMutex); |
| 254 | + return mShared; |
| 255 | + } |
| 256 | + |
| 257 | + // In the future, maybe this can become `std::atomic<std::shared_ptr<Function>>`. |
| 258 | + mutable std::mutex mMutex; |
| 259 | + std::shared_ptr<SharedFunction> mShared GUARDED_BY(mMutex); |
| 260 | +}; |
| 261 | + |
| 262 | +template <typename Function> |
| 263 | +AsyncFunction(Function&&) -> AsyncFunction<std::decay_t<Function>>; |
| 264 | + |
| 265 | +template <typename Signature> |
| 266 | +using AsyncFunctionStd = AsyncFunction<std::function<Signature>>; |
| 267 | + |
| 268 | +template <typename Signature> |
| 269 | +using AsyncFunctionFtl = AsyncFunction<ftl::Function<Signature>>; |
| 270 | + |
| 271 | +template <typename Signature> |
| 272 | +using AsyncFunctionFtl1 = AsyncFunction<ftl::Function<Signature, 1>>; |
| 273 | + |
| 274 | +template <typename Signature> |
| 275 | +using AsyncFunctionFtl2 = AsyncFunction<ftl::Function<Signature, 2>>; |
| 276 | + |
| 277 | +template <typename Signature> |
| 278 | +using AsyncFunctionFtl3 = AsyncFunction<ftl::Function<Signature, 3>>; |
| 279 | + |
| 280 | +} // namespace android::surfaceflinger::tests::end2end::test_framework::core |
0 commit comments