-
Notifications
You must be signed in to change notification settings - Fork 170
/
Copy pathconnection_evaluator.h
156 lines (131 loc) · 6.55 KB
/
connection_evaluator.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
/*
This file is part of KDBindings.
SPDX-FileCopyrightText: 2023 Klarälvdalens Datakonsult AB, a KDAB Group company <[email protected]>
Author: Shivam Kunwar <[email protected]>
SPDX-License-Identifier: MIT
Contact KDAB at <[email protected]> for commercial licensing options.
*/
#pragma once
#include <algorithm>
#include <functional>
#include <mutex>
#include <kdbindings/connection_handle.h>
namespace KDBindings {
/**
* @brief Manages and evaluates deferred Signal connections.
*
* @warning Deferred connections are experimental and may be removed or changed in the future.
*
* The ConnectionEvaluator class is responsible for managing and evaluating connections
* to Signals. It provides mechanisms to delay and control the evaluation of connections.
* It therefore allows controlling when and on which thread slots connected to a Signal are executed.
*
* @see Signal::connectDeferred()
*/
class ConnectionEvaluator
{
public:
/** ConnectionEvaluators are default constructible */
ConnectionEvaluator() = default;
/** Connectionevaluators are not copyable */
// As it is designed to manage connections,
// and copying it could lead to unexpected behavior, including duplication of connections and issues
// related to connection lifetimes. Therefore, it is intentionally made non-copyable.
ConnectionEvaluator(const ConnectionEvaluator &) noexcept = delete;
ConnectionEvaluator &operator=(const ConnectionEvaluator &) noexcept = delete;
/** ConnectionEvaluators are not moveable */
// As they are captures by-reference
// by the Signal, so moving them would lead to a dangling reference.
ConnectionEvaluator(ConnectionEvaluator &&other) noexcept = delete;
ConnectionEvaluator &operator=(ConnectionEvaluator &&other) noexcept = delete;
virtual ~ConnectionEvaluator() = default;
/**
* @brief Evaluate the deferred connections.
*
* This function is responsible for evaluating and executing deferred connections.
* This function is thread safe.
*
* @warning Evaluating slots that throw an exception is currently undefined behavior.
*/
void evaluateDeferredConnections()
{
std::lock_guard<std::recursive_mutex> lock(m_slotInvocationMutex);
if (m_isEvaluating) {
// We're already evaluating, so we don't want to re-enter this function.
return;
}
m_isEvaluating = true;
// Current best-effort error handling will remove any further invocations that were queued.
// We could use a queue and use a `while(!empty) { pop_front() }` loop instead to avoid this.
// However, we would then ideally use a ring-buffer to avoid excessive allocations, which isn't in the STL.
try {
for (auto &pair : m_deferredSlotInvocations) {
pair.second();
}
} catch (...) {
// Best-effort: Reset the ConnectionEvaluator so that it at least doesn't execute the same erroneous slot multiple times.
m_deferredSlotInvocations.clear();
m_isEvaluating = false;
throw;
}
m_deferredSlotInvocations.clear();
m_isEvaluating = false;
}
protected:
/**
* @brief Called when a new slot invocation is added.
*
* This function can be overwritten by subclasses to get notified whenever a new invocation is added to this evaluator.
* The default implementation does nothing and does not have to be called by subclasses when overriding.
*
* ⚠️ *Note that this function will be executed on the thread that enqueued the slot invocation (i.e. the thread that called .emit() on the signal),
* which is usually not the thread that is responsible for evaluating the connections!
* Therefore it is usually not correct to call evaluateDeferredConnections() within this function!
* User code is responsible for ensuring that the threads are synchronized correctly.*
*
* For example, if you plan to evaluate (execute) the slot invocations in some "main" thread A
* and a signal is emitted in thread B, than this method will be called on thread B.
* It is a good place to "wake up" the event loop of thread A so that thread A can call `evaluateDeferredConnections()`.
*/
virtual void onInvocationAdded() { }
private:
template<typename...>
friend class Signal;
void enqueueSlotInvocation(const ConnectionHandle &handle, const std::function<void()> &slotInvocation)
{
{
std::lock_guard<std::recursive_mutex> lock(m_slotInvocationMutex);
m_deferredSlotInvocations.push_back({ handle, std::move(slotInvocation) });
}
onInvocationAdded();
}
// Note: This function is marked with noexcept but may theoretically encounter an exception and terminate the program if locking the mutex fails.
// If this does happen though, there's likely something very wrong, so std::terminate is actually a reasonable way to handle this.
//
// In addition, we do need to use a recursive_mutex, as otherwise a slot from `enqueueSlotInvocation` may theoretically call this function and cause undefined behavior.
void dequeueSlotInvocation(const ConnectionHandle &handle) noexcept
{
std::lock_guard<std::recursive_mutex> lock(m_slotInvocationMutex);
if (m_isEvaluating) {
// It's too late, we're already evaluating the deferred connections.
// We can't remove the invocation now, as it might be currently evaluated.
// And removing any invocations would be undefined behavior as we would invalidate
// the loop indices in `evaluateDeferredConnections`.
return;
}
auto handleMatches = [&handle](const auto &invocationPair) {
return invocationPair.first == handle;
};
// Remove all invocations that match the handle
m_deferredSlotInvocations.erase(
std::remove_if(m_deferredSlotInvocations.begin(), m_deferredSlotInvocations.end(), handleMatches),
m_deferredSlotInvocations.end());
}
std::vector<std::pair<ConnectionHandle, std::function<void()>>> m_deferredSlotInvocations;
// We need to use a recursive mutex here, as `evaluateDeferredConnections` executes arbitrary user code.
// This may end up in a call to dequeueSlotInvocation, which locks the same mutex.
// We'll also need to add a flag to make sure we don't actually dequeue invocations while we're evaluating them.
std::recursive_mutex m_slotInvocationMutex;
bool m_isEvaluating = false;
};
} // namespace KDBindings