VLink  2.1.0
A high-performance communication middleware
graph_task.h
浏览该文件的文档.
1 /*
2  * Copyright (C) 2026 by Thun Lu. All rights reserved.
3  * Author: Thun Lu <thun.lu@zohomail.cn>
4  * Repo: https://github.com/thun-res/vlink
5  * _ __ __ _ __
6  * | | / / / / (_) ____ / /__
7  * | | / / / / / / / __ \ / //_/
8  * | |/ / / /___ / / / / / / / ,<
9  * |___/ /_____/ /_/ /_/ /_/ /_/|_|
10  *
11  * Licensed under the Apache License, Version 2.0 (the "License");
12  * you may not use this file except in compliance with the License.
13  * You may obtain a copy of the License at
14  *
15  * http://www.apache.org/licenses/LICENSE-2.0
16  *
17  * Unless required by applicable law or agreed to in writing, software
18  * distributed under the License is distributed on an "AS IS" BASIS,
19  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
20  * See the License for the specific language governing permissions and
21  * limitations under the License.
22  */
23 
24 /**
25  * @file graph_task.h
26  * @brief Directed acyclic task graph with condition branching, cycle guard and DOT export.
27  *
28  * @details
29  * @c GraphTask is VLink's data-flow primitive: every node carries a callback, a list of
30  * predecessors and a list of successors, and is submitted to any engine that exposes
31  * @c post_task / @c post_task_with_priority -- @c MessageLoop, @c MultiLoop or @c ThreadPool.
32  * Edges are declared with @c precede / @c succeed (Taskflow convention) and a process-wide
33  * topology mutex serialises all mutators across every graph instance.
34  *
35  * @par DAG diagram
36  *
37  * @verbatim
38  * +---+ precede +---+ precede +---+
39  * | A | ----------> | B | ----------> | C |
40  * +-+-+ +-+-+ +-+-+
41  * | condition | |
42  * v v v
43  * +-----+ +-----+ +-----+
44  * | D0 | | E | | F |
45  * +-----+ +-----+ +-----+
46  * ^
47  * | D0/D1/... condition task selects which successor branch fires
48  * +-----+
49  * | D1 |
50  * +-----+
51  * @endverbatim
52  *
53  * @par Task factories
54  *
55  * | Factory | Callback signature | Use case |
56  * | ----------------------------- | ------------------- | ------------------------------------- |
57  * | @c create(callback) | @c void() | Regular work node |
58  * | @c create_condition(callback) | @c int() | Branch selector (return value picks) |
59  *
60  * @par Execution policies
61  *
62  * | Policy | Behaviour |
63  * | ------------------ | ------------------------------------------------------------------ |
64  * | @c kPolicyOnce | Runs exactly once per @c execute pass (default) |
65  * | @c kPolicyMultiple | Runs multiple times within one @c execute pass |
66  * | @c kPolicyWaitAll | Waits for every predecessor before running |
67  *
68  * @par Example
69  * @code
70  * vlink::MultiLoop engine(4);
71  * engine.async_run();
72  *
73  * auto load = vlink::GraphTask::create("load", [] { load_data(); });
74  * auto proc = vlink::GraphTask::create("proc", [] { process(); });
75  * auto save = vlink::GraphTask::create("save", [] { save_data(); });
76  *
77  * // load -- > proc means load->precede(proc); produces execution order load, proc, save.
78  * load-- > proc-- > save;
79  *
80  * assert(!load->has_cycle());
81  * load->execute(&engine);
82  * @endcode
83  */
84 
85 #pragma once
86 
87 #include <memory>
88 #include <string>
89 #include <type_traits>
90 #include <unordered_set>
91 #include <utility>
92 #include <vector>
93 
94 #include "./functional.h"
95 #include "./macros.h"
96 #include "./traits.h"
97 
98 namespace vlink {
99 
100 /**
101  * @class GraphTask
102  * @brief DAG node carrying a callback, predecessor and successor links, and run-time state.
103  *
104  * @details
105  * Must be created through the @c create / @c create_condition factories so callbacks share
106  * @c std::enable_shared_from_this safely. @c execute traverses the reachable sub-graph and
107  * submits ready tasks to a user-supplied engine.
108  */
109 class VLINK_EXPORT GraphTask final : public std::enable_shared_from_this<GraphTask> {
110  private:
111  /**
112  * @brief Passkey used to restrict construction to the static factory functions.
113  *
114  * @details
115  * The token type and its default constructor are private. A factory creates the token and
116  * forwards it through @c std::allocate_shared; callers cannot name or create a token and
117  * therefore cannot invoke the token-gated constructors directly.
118  */
119  class PrivateToken final {
120  private:
121  PrivateToken() = default;
122 
123  friend class GraphTask;
124  };
125 
126  public:
127  /**
128  * @brief Run-time execution state of a node within an @c execute pass.
129  */
130  enum Status : uint8_t {
131  kStatusInActive = 0, ///< Not yet submitted or cancelled.
132  kStatusPending = 1, ///< Waiting for predecessors to complete.
133  kStatusRunning = 2, ///< Currently executing.
134  kStatusDone = 3, ///< Execution finished.
135  };
136 
137  /**
138  * @brief Policy controlling how many times the task may run within a pass.
139  */
140  enum Policy : uint8_t {
141  kPolicyOnce = 0, ///< Run exactly once per @c execute pass (default).
142  kPolicyMultiple = 1, ///< Allow multiple invocations within the pass.
143  kPolicyWaitAll = 2, ///< Wait for every predecessor before running.
144  };
145 
146  /**
147  * @brief Callback type for void-returning work nodes.
148  */
149  using Callback = MoveFunction<void()>;
150 
151  /**
152  * @brief Callback type for condition nodes; the return value selects an outgoing branch.
153  */
155 
156  /**
157  * @brief Status-change notification callback.
158  *
159  * @details
160  * Invoked with the task name and the new status whenever the node transitions.
161  */
162  using StatusCallback = MoveFunction<void(const std::string&, Status)>;
163 
164  /**
165  * @brief Creates a regular work node.
166  *
167  * @param callback Work function.
168  * @param condition_number Number of outgoing branches (@c 0 disables branching).
169  * @return Shared pointer to the new task.
170  */
171  [[nodiscard]] static std::shared_ptr<GraphTask> create(Callback&& callback, int condition_number = 0);
172 
173  /**
174  * @brief Creates a named regular work node.
175  *
176  * @param name Node name used in DOT output and status callbacks.
177  * @param callback Work function.
178  * @param condition_number Number of outgoing branches.
179  * @return Shared pointer to the new task.
180  */
181  [[nodiscard]] static std::shared_ptr<GraphTask> create(const std::string& name, Callback&& callback,
182  int condition_number = 0);
183 
184  /**
185  * @brief Creates a condition node whose return value selects a successor branch.
186  *
187  * @param callback Predicate returning the branch index.
188  * @param condition_number Number of branches accepted; out-of-range returns skip all successors.
189  * @return Shared pointer to the new condition task.
190  */
191  [[nodiscard]] static std::shared_ptr<GraphTask> create_condition(ConditionCallback&& callback,
192  int condition_number = 0);
193 
194  /**
195  * @brief Creates a named condition node.
196  *
197  * @param name Node name.
198  * @param callback Predicate returning the branch index.
199  * @param condition_number Branch count.
200  * @return Shared pointer to the new condition task.
201  */
202  [[nodiscard]] static std::shared_ptr<GraphTask> create_condition(const std::string& name,
203  ConditionCallback&& callback,
204  int condition_number = 0);
205 
206  /// @cond INTERNAL
207  /**
208  * @brief Factory-only private constructor for a regular task.
209  *
210  * @details This constructor is private at the API level but declared in the public section so
211  * Android libc++ can perform in-place construction inside the @c std::allocate_shared control
212  * block. Callers cannot invoke it because they cannot create the private @p token.
213  */
214  explicit GraphTask(PrivateToken token, Callback&& callback, int condition_number);
215 
216  /**
217  * @brief Factory-only private constructor for a named regular task.
218  *
219  * @details Declared in the public section only for @c std::allocate_shared. Callers cannot
220  * invoke it because they cannot create the private @p token.
221  */
222  explicit GraphTask(PrivateToken token, const std::string& name, Callback&& callback, int condition_number);
223 
224  /**
225  * @brief Factory-only private constructor for a condition task.
226  *
227  * @details Declared in the public section only for @c std::allocate_shared. Callers cannot
228  * invoke it because they cannot create the private @p token.
229  */
230  explicit GraphTask(PrivateToken token, ConditionCallback&& callback, int condition_number);
231 
232  /**
233  * @brief Factory-only private constructor for a named condition task.
234  *
235  * @details Declared in the public section only for @c std::allocate_shared. Callers cannot
236  * invoke it because they cannot create the private @p token.
237  */
238  explicit GraphTask(PrivateToken token, const std::string& name, ConditionCallback&& callback, int condition_number);
239 
240  /**
241  * @brief Destroys the task.
242  *
243  * @details Public because the @c std::allocate_shared control block performs destruction.
244  * Instances must still be created by @c create or @c create_condition and owned by
245  * @c std::shared_ptr; callers must not destroy the managed pointer directly.
246  */
247  ~GraphTask();
248  /// @endcond
249 
250  /**
251  * @brief Submits the reachable sub-graph to @p graph_engine.
252  *
253  * @details
254  * Traverses the sub-graph, identifies ready nodes and posts them via @c post_task or
255  * @c post_task_with_priority when available. Compatible with @c MessageLoop, @c MultiLoop and
256  * @c ThreadPool.
257  *
258  * @tparam GraphEngineT Engine type exposing @c post_task and optionally @c post_task_with_priority.
259  * @param graph_engine Target engine instance.
260  */
261  template <class GraphEngineT>
262  void execute(GraphEngineT* graph_engine);
263 
264  /**
265  * @brief Cancels this node and propagates the cancellation downstream.
266  *
267  * @details
268  * Sets this node's status to @c kStatusInActive and walks the successor list forward marking
269  * every reachable node inactive. Predecessors are unaffected. Inactive nodes are skipped
270  * by the engine and by downstream @c kPolicyWaitAll counters.
271  */
272  void cancel();
273 
274  /**
275  * @brief Declares that this node must complete before @p task starts.
276  *
277  * @details
278  * Runs a reachability pre-check on @p task 's downstream cone before mutating any list; the
279  * edge is rejected (with an error log) if it would form a cycle. On success @p task is
280  * appended to this node's successor list and this node is appended to @p task 's predecessor
281  * list. Topology mutation is serialised by a process-wide recursive mutex shared across
282  * every @c GraphTask instance, so concurrent writers are safe; read paths (@c execute,
283  * @c has_cycle, @c export_to_dot) read per-node snapshots without taking that mutex.
284  *
285  * @param task Successor node.
286  */
287  void precede(const std::shared_ptr<GraphTask>& task);
288 
289  /**
290  * @brief Declares that @p task must complete before this node starts.
291  *
292  * @details
293  * Mirror of @c precede; rejects edges that would form a cycle. Shares the same single-writer
294  * topology mutex as @c precede.
295  *
296  * @param task Predecessor node.
297  */
298  void succeed(const std::shared_ptr<GraphTask>& task);
299 
300  /**
301  * @brief Subscribes a callback to status transitions on this node.
302  *
303  * @details
304  * Each call appends a new subscriber. On every status change the node snapshots the current
305  * subscriber set under @c status_callbacks_mtx, releases the lock, then invokes the snapshot
306  * in unspecified order. Because callbacks fire without the mutex held they may freely
307  * register / unregister callbacks on the same node; such mutations apply on the next
308  * transition. Exceptions are caught and logged; remaining subscribers still fire.
309  *
310  * @param callback Status change callback.
311  * @return Subscription id (>0). Returns @c 0 when @p callback is empty.
312  */
314 
315  /**
316  * @brief Removes a previously registered status callback by id.
317  *
318  * @param id Subscription id returned by @c register_status_callback.
319  * @return @c true when the subscription was found and removed.
320  */
321  bool unregister_status_callback(uint32_t id);
322 
323  /**
324  * @brief Removes every status callback subscription on this node.
325  */
327 
328  /**
329  * @brief Sets the node name used in DOT output and status callbacks.
330  *
331  * @param name Node name.
332  */
333  void set_name(const std::string& name);
334 
335  /**
336  * @brief Sets a group name used to visually cluster nodes in DOT output.
337  *
338  * @param name Group name.
339  */
340  void set_group_name(const std::string& name);
341 
342  /**
343  * @brief Sets the number of outgoing condition branches.
344  *
345  * @param condition_number Branch count.
346  */
347  void set_condition_number(int condition_number);
348 
349  /**
350  * @brief Sets the dispatch priority used by priority-aware engines.
351  *
352  * @param priority Priority value.
353  */
354  void set_priority(uint16_t priority);
355 
356  /**
357  * @brief Sets the maximum recursion depth that bounds DFS traversals.
358  *
359  * @details
360  * Traversals exceeding this depth treat the graph as conservatively cyclic. Default: @c 10000.
361  *
362  * @param depth Maximum recursion depth.
363  */
364  void set_max_recursion_depth(uint32_t depth);
365 
366  /**
367  * @brief Sets the execution policy for this node.
368  *
369  * @param policy Policy enumerator.
370  */
371  void set_policy(Policy policy);
372 
373  /**
374  * @brief Returns the node name.
375  *
376  * @return Node name.
377  */
378  [[nodiscard]] std::string get_name() const;
379 
380  /**
381  * @brief Returns the group name.
382  *
383  * @return Group name.
384  */
385  [[nodiscard]] std::string get_group_name() const;
386 
387  /**
388  * @brief Returns the configured branch count.
389  *
390  * @return Branch count.
391  */
392  [[nodiscard]] int get_condition_number() const;
393 
394  /**
395  * @brief Returns the configured dispatch priority.
396  *
397  * @return Priority value.
398  */
399  [[nodiscard]] uint16_t get_priority() const;
400 
401  /**
402  * @brief Returns the maximum recursion depth used for cycle detection.
403  *
404  * @return Recursion depth bound.
405  */
406  [[nodiscard]] uint32_t get_max_recursion_depth() const;
407 
408  /**
409  * @brief Returns the configured execution policy.
410  *
411  * @return Policy enumerator.
412  */
413  [[nodiscard]] Policy get_policy() const;
414 
415  /**
416  * @brief Returns the current execution status of this node.
417  *
418  * @return Status enumerator.
419  */
420  [[nodiscard]] Status get_status() const;
421 
422  /**
423  * @brief Removes an outgoing edge created by @c precede.
424  *
425  * @details
426  * Removes @p task from the successor list and removes this node from @p task 's predecessor
427  * list. Logs an error when the edge does not exist.
428  *
429  * @param task Previously attached successor.
430  */
431  void remove_precede_task(const std::shared_ptr<GraphTask>& task);
432 
433  /**
434  * @brief Removes an incoming edge created by @c succeed.
435  *
436  * @param task Previously attached predecessor.
437  */
438  void remove_succeed_task(const std::shared_ptr<GraphTask>& task);
439 
440  /**
441  * @brief Returns the current predecessor list as weak pointers.
442  *
443  * @return Vector of weak predecessor pointers.
444  */
445  [[nodiscard]] std::vector<std::weak_ptr<GraphTask>> get_precede_task_list() const;
446 
447  /**
448  * @brief Returns the current successor list as weak pointers.
449  *
450  * @return Vector of weak successor pointers.
451  */
452  [[nodiscard]] std::vector<std::weak_ptr<GraphTask>> get_succeed_task_list() const;
453 
454  /**
455  * @brief Reports whether this node was created via @c create_condition.
456  *
457  * @return @c true for condition nodes.
458  */
459  [[nodiscard]] bool is_condition_task() const;
460 
461  /**
462  * @brief Detects whether the reachable sub-graph contains a cycle.
463  *
464  * @details
465  * Iterative DFS with a recursion stack. Acquires per-node mutexes one at a time and does
466  * not take the global topology mutex; safe to call from status callbacks fired by @c invoke
467  * or @c cancel.
468  *
469  * @return @c true when a cycle is found or the recursion bound is exceeded.
470  */
471  [[nodiscard]] bool has_cycle() const;
472 
473  /**
474  * @brief Exports the reachable sub-graph as a Graphviz DOT document.
475  *
476  * @return DOT source string.
477  */
478  [[nodiscard]] std::string export_to_dot() const;
479 
480  protected:
481  using FindTaskCallback = MoveFunction<void(const std::shared_ptr<GraphTask>&)>;
482 
484 
485  private:
486  template <typename TypeT>
487  struct SharedAllocator;
488 
489  int invoke(bool once);
490 
491  void wait();
492 
493  void notify(int condition_number);
494 
495  void notify_skip();
496 
497  bool mark_predecessor_satisfied(bool active, bool* has_active);
498 
499  void mark_ready(bool enable);
500 
501  void update_status(Status status);
502 
503  bool detect_cycle(const GraphTask* task, std::unordered_set<const GraphTask*>& visited,
504  std::unordered_set<const GraphTask*>& recursion_stack, uint32_t& depth, uint32_t max_depth) const;
505 
506  bool reaches_via_successors(const GraphTask* start_node,
507  const std::vector<std::weak_ptr<GraphTask>>& start_successors,
508  const GraphTask* target) const;
509 
510  static void clear_invalid_task(const std::shared_ptr<GraphTask>& task);
511 
512  struct Impl;
513  std::unique_ptr<Impl> impl_;
514 
516 };
517 
518 /**
519  * @typedef GraphTaskPtr
520  * @brief Convenience alias for the canonical @c shared_ptr<GraphTask> handle.
521  */
522 using GraphTaskPtr = std::shared_ptr<GraphTask>;
523 
524 ////////////////////////////////////////////////////////////////
525 /// Details
526 ////////////////////////////////////////////////////////////////
527 
528 template <class GraphEngineT>
529 inline void GraphTask::execute(GraphEngineT* graph_engine) {
530  auto self = shared_from_this();
531 
532  process_and_traverse([self, graph_engine](const std::shared_ptr<GraphTask>& task) {
533  constexpr bool kHaspriority = VLINK_HAS_MEMBER(GraphEngineT, post_task_with_priority);
534  [[maybe_unused]] constexpr uint8_t kPriorityType = 2;
535 
536  if VUNLIKELY (task->get_status() == kStatusInActive) {
537  return;
538  }
539 
540  auto task_func = [self, task]() {
541  if VLIKELY (task.get() != self.get()) {
542  task->wait();
543  }
544 
545  int ret = task->invoke(true);
546 
547  if VLIKELY (ret >= 0) {
548  task->notify(ret);
549  }
550  };
551 
552  auto post_task = [graph_engine](auto&& func) -> bool {
553  using Ret = decltype(graph_engine->post_task(std::forward<decltype(func)>(func)));
554 
555  if constexpr (std::is_same_v<Ret, bool>) {
556  return graph_engine->post_task(std::forward<decltype(func)>(func));
557  } else {
558  graph_engine->post_task(std::forward<decltype(func)>(func));
559  return true;
560  }
561  };
562 
563  bool posted = false;
564 
565  if constexpr (kHaspriority) {
566  auto post_task_with_priority = [graph_engine, task](auto&& func) -> bool {
567  using Ret =
568  decltype(graph_engine->post_task_with_priority(std::forward<decltype(func)>(func), task->get_priority()));
569 
570  if constexpr (std::is_same_v<Ret, bool>) {
571  return graph_engine->post_task_with_priority(std::forward<decltype(func)>(func), task->get_priority());
572  } else {
573  graph_engine->post_task_with_priority(std::forward<decltype(func)>(func), task->get_priority());
574  return true;
575  }
576  };
577 
578  if constexpr (VLINK_HAS_MEMBER(GraphEngineT, get_type)) {
579  if (graph_engine->get_type() == kPriorityType) {
580  posted = post_task_with_priority(std::move(task_func));
581  } else {
582  posted = post_task(std::move(task_func));
583  }
584  } else {
585  posted = post_task(std::move(task_func));
586  }
587  } else {
588  posted = post_task(std::move(task_func));
589  }
590 
591  if VUNLIKELY (!posted) {
592  task->cancel();
593  }
594  });
595 }
596 
597 [[maybe_unused]] static inline GraphTaskPtr& operator--(GraphTaskPtr& task, int) { return task; }
598 
599 [[maybe_unused]] static inline GraphTaskPtr& operator>(GraphTaskPtr& task, GraphTaskPtr& target_task) {
600  task->precede(target_task);
601  return target_task;
602 }
603 
604 [[maybe_unused]] static inline GraphTaskPtr& operator<(GraphTaskPtr& task, GraphTaskPtr& target_task) {
605  task->succeed(target_task);
606  return target_task;
607 }
608 
609 [[maybe_unused]] static inline GraphTaskPtr& operator--(GraphTaskPtr& task) { return task; }
610 
611 } // namespace vlink
Pool-backed type-erased callables: copyable vlink::Function and move-only vlink::MoveFunction.
Cross-platform macros for visibility, branch hints, copy prevention, singletons and string helpers.
#define VUNLIKELY(...)
Short alias for VLINK_UNLIKELY.
Definition: macros.h:289
#define VLINK_EXPORT
Definition: macros.h:81
#define VLIKELY(...)
Short alias for VLINK_LIKELY.
Definition: macros.h:284
#define VLINK_DISALLOW_COPY_AND_ASSIGN(classname)
Deletes the copy constructor and copy-assignment operator of classname.
Definition: macros.h:174
Compile-time type-trait helpers used across the VLink codebase.
#define VLINK_HAS_MEMBER(T, member)
Macro Definitions
Definition: traits.h:316