BehaviorTree
Core Library to create and execute Behavior Trees
Loading...
Searching...
No Matches
timer_queue.h
1#pragma once
2
3#include <atomic>
4#include <chrono>
5#include <condition_variable>
6#include <functional>
7#include <mutex>
8#include <queue>
9#include <thread>
10
11#include <assert.h>
12
13namespace BT
14{
15// http://www.crazygaze.com/blog/2016/03/24/portable-c-timer-queue/
16
17namespace details
18{
19class Semaphore
20{
21public:
22 Semaphore(unsigned int count = 0) : m_count(count)
23 {}
24
25 void notify()
26 {
27 m_count.fetch_add(1);
28 m_cv.notify_one();
29 }
30
31 template <class Clock, class Duration>
32 bool waitUntil(const std::chrono::time_point<Clock, Duration>& point)
33 {
34 std::unique_lock<std::mutex> lock(m_mtx);
35 if(!m_cv.wait_until(lock, point, [this]() { return m_count > 0 || m_unlock; }))
36 {
37 return false;
38 }
39 // Only decrement if there is a real count. If we woke because of manualUnlock,
40 // m_count may be zero and we must not decrement it.
41 if(m_count > 0)
42 {
43 m_count.fetch_sub(1);
44 }
45 // Clear the manual unlock flag
46 m_unlock = false;
47
48 return true;
49 }
50
51 void manualUnlock()
52 {
53 m_unlock = true;
54 m_cv.notify_one();
55 }
56
57private:
58 std::mutex m_mtx;
59 std::condition_variable m_cv;
60 std::atomic_uint m_count = 0;
61 std::atomic_bool m_unlock = false;
62};
63} // namespace details
64
65// Timer Queue
66//
67// Allows execution of handlers at a specified time in the future
68// Guarantees:
69// - All handlers are executed ONCE, even if canceled (aborted parameter will
70//be set to true)
71// - If TimerQueue is destroyed, it will cancel all handlers.
72// - Handlers are ALWAYS executed in the Timer Queue worker thread.
73// - Handlers execution order is NOT guaranteed
74//
75template <typename ClockT = std::chrono::steady_clock,
76 typename DurationT = std::chrono::steady_clock::duration>
77class TimerQueue
78{
79public:
80 TimerQueue()
81 {
82 m_finish.store(false);
83 m_thread = std::thread([this]() { run(); });
84 }
85
86 ~TimerQueue()
87 {
88 // Cancel before raising m_finish: once the worker sees the flag, every
89 // item is already due, so its final checkWork() drains the whole queue.
90 cancelAll();
91 m_finish.store(true);
92 m_checkWork.notify();
93 if(m_thread.joinable())
94 {
95 m_thread.join();
96 }
97 }
98
99 //! Adds a new timer
100 // \return
101 // Returns the ID of the new timer. You can use this ID to cancel the
102 // timer
103 uint64_t add(std::chrono::milliseconds milliseconds, std::function<void(bool)> handler)
104 {
105 WorkItem item;
106 item.end = ClockT::now() + milliseconds;
107 item.handler = std::move(handler);
108
109 std::unique_lock<std::mutex> lk(m_mtx);
110 uint64_t id = ++m_idcounter;
111 item.id = id;
112 m_items.push(std::move(item));
113 lk.unlock();
114
115 // Something changed, so wake up timer thread
116 m_checkWork.notify();
117 return id;
118 }
119
120 //! Cancels the specified timer
121 // \return
122 // 1 if the timer was cancelled.
123 // 0 if you were too late to cancel (or the timer ID was never valid to
124 // start with)
126 {
127 // Instead of removing the item from the container (thus breaking the
128 // heap integrity), we set the item as having no handler, and put
129 // that handler on a new item at the top for immediate execution
130 // The timer thread will then ignore the original item, since it has no
131 // handler.
132 std::unique_lock<std::mutex> lk(m_mtx);
133 for(auto&& item : m_items.getContainer())
134 {
135 if(item.id == id && item.handler)
136 {
137 WorkItem newItem;
138 // Zero time, so it stays at the top for immediate execution
139 newItem.end = std::chrono::time_point<ClockT, DurationT>();
140 newItem.id = 0; // Means it is a canceled item
141 // Move the handler from item to newItem.
142 // Also, we need to manually set the handler to nullptr, since
143 // the standard does not guarantee moving an std::function will
144 // empty it. Some STL implementation will empty it, others will
145 // not.
146 newItem.handler = std::move(item.handler);
147 item.handler = nullptr;
148 m_items.push(std::move(newItem));
149
150 lk.unlock();
151 // Something changed, so wake up timer thread
152 m_checkWork.notify();
153 return 1;
154 }
155 }
156 return 0;
157 }
158
159 //! Cancels all timers
160 // \return
161 // The number of timers cancelled
163 {
164 // Setting all "end" to 0 (for immediate execution) is ok,
165 // since it maintains the heap integrity
166 std::unique_lock<std::mutex> lk(m_mtx);
167 for(auto&& item : m_items.getContainer())
168 {
169 if(item.id)
170 {
171 item.end = std::chrono::time_point<ClockT, DurationT>();
172 item.id = 0;
173 }
174 }
175 auto ret = m_items.size();
176
177 lk.unlock();
178 m_checkWork.notify();
179 return ret;
180 }
181
182 TimerQueue(const TimerQueue&) = delete;
183 TimerQueue& operator=(const TimerQueue&) = delete;
184 TimerQueue(TimerQueue&&) = delete;
185 TimerQueue& operator=(TimerQueue&&) = delete;
186
187private:
188 void run()
189 {
190 while(!m_finish.load())
191 {
192 auto end = calcWaitTime();
193 if(end.first)
194 {
195 // Timers found, so wait until it expires (or something else
196 // changes)
197 m_checkWork.waitUntil(end.second);
198 }
199 else
200 {
201 // No timers exist, so wait an arbitrary amount of time
202 m_checkWork.waitUntil(ClockT::now() + std::chrono::milliseconds(10));
203 }
204
205 // Check and execute as much work as possible, such as, all expired
206 // timers
207 checkWork();
208 }
209
210 // The loop may exit without a last pass (e.g. woken up by cancel() right
211 // before the shutdown), leaving cancelled items in the queue.
212 checkWork();
213
214 // If we are shutting down, we should not have any items left,
215 // since the shutdown cancels all items
216 assert(m_items.size() == 0);
217 }
218
219 std::pair<bool, std::chrono::time_point<ClockT, DurationT>> calcWaitTime()
220 {
221 std::lock_guard<std::mutex> lk(m_mtx);
222 while(m_items.size())
223 {
224 if(m_items.top().handler)
225 {
226 // Item present, so return the new wait time
227 return std::make_pair(true, m_items.top().end);
228 }
229 else
230 {
231 // Discard empty handlers (they were cancelled)
232 m_items.pop();
233 }
234 }
235
236 // No items found, so return no wait time (causes the thread to wait
237 // indefinitely)
238 return std::make_pair(false, std::chrono::time_point<ClockT, DurationT>());
239 }
240
241 void checkWork()
242 {
243 std::unique_lock<std::mutex> lk(m_mtx);
244 while(m_items.size() && m_items.top().end <= ClockT::now())
245 {
246 WorkItem item(std::move(m_items.top()));
247 m_items.pop();
248
249 lk.unlock();
250 if(item.handler)
251 {
252 item.handler(item.id == 0);
253 }
254 lk.lock();
255 }
256 }
257
258 details::Semaphore m_checkWork;
259 std::thread m_thread;
260 std::atomic_bool m_finish = false;
261 uint64_t m_idcounter = 0;
262
263 struct WorkItem
264 {
265 std::chrono::time_point<ClockT, DurationT> end;
266 uint64_t id = 0; // id==0 means it was cancelled
267 std::function<void(bool)> handler;
268 bool operator>(const WorkItem& other) const
269 {
270 return end > other.end;
271 }
272 };
273
274 std::mutex m_mtx;
275 // Inheriting from priority_queue, so we can access the internal container
276 class Queue
277 : public std::priority_queue<WorkItem, std::vector<WorkItem>, std::greater<WorkItem>>
278 {
279 public:
280 std::vector<WorkItem>& getContainer()
281 {
282 return this->c;
283 }
284 } m_items;
285};
286} // namespace BT
Definition: timer_queue.h:78
size_t cancelAll()
Cancels all timers.
Definition: timer_queue.h:162
size_t cancel(uint64_t id)
Cancels the specified timer.
Definition: timer_queue.h:125
uint64_t add(std::chrono::milliseconds milliseconds, std::function< void(bool)> handler)
Adds a new timer.
Definition: timer_queue.h:103
Definition: timer_queue.h:20
The SwitchNode is equivalent to a switch statement, where a certain branch (child) is executed accord...
Definition: basic_types.h:540
Definition: action_node.h:24