Loading...
Searching...
No Matches
task.hpp
1#pragma once
2
3#include "graph.hpp"
4
9
10namespace tf {
11
12// ----------------------------------------------------------------------------
13// Task Types
14// ----------------------------------------------------------------------------
15
39
44inline constexpr std::array<TaskType, 7> TASK_TYPES = {
52};
53
66inline const char* to_string(TaskType type) {
67
68 const char* val;
69
70 switch(type) {
71 case TaskType::PLACEHOLDER: val = "placeholder"; break;
72 case TaskType::STATIC: val = "static"; break;
73 case TaskType::RUNTIME: val = "runtime"; break;
74 case TaskType::SUBFLOW: val = "subflow"; break;
75 case TaskType::CONDITION: val = "condition"; break;
76 case TaskType::MODULE: val = "module"; break;
77 case TaskType::ASYNC: val = "async"; break;
78 default: val = "undefined"; break;
79 }
80
81 return val;
82}
83
84// ----------------------------------------------------------------------------
85// Static Task Trait
86// ----------------------------------------------------------------------------
87
139template <typename C>
140concept StaticTaskLike = std::invocable<C> &&
141 std::same_as<std::invoke_result_t<C>, void>;
142
150template <typename C>
152
153// ----------------------------------------------------------------------------
154// Subflow Task Trait
155// ----------------------------------------------------------------------------
156
219template <typename C>
220concept SubflowTaskLike = std::invocable<C, tf::Subflow&> &&
221 std::same_as<std::invoke_result_t<C, tf::Subflow&>, void>;
222
230template <typename C>
232
233// ----------------------------------------------------------------------------
234// Runtime Task Trait
235// ----------------------------------------------------------------------------
236
302template <typename C>
304 (std::invocable<C, tf::Runtime&> &&
305 std::same_as<std::invoke_result_t<C, tf::Runtime&>, void>) ||
306 (std::invocable<C, tf::NonpreemptiveRuntime&> &&
307 std::same_as<std::invoke_result_t<C, tf::NonpreemptiveRuntime&>, void>);
308
316template <typename C>
318
319
320// ----------------------------------------------------------------------------
321// Condition Task Trait
322// ----------------------------------------------------------------------------
323
396template <typename C>
397concept ConditionTaskLike = std::invocable<C> &&
398 std::convertible_to<std::invoke_result_t<C>, int>;
399
407template <typename C>
409
493template <typename C>
494concept MultiConditionTaskLike = std::invocable<C> &&
495 std::same_as<std::invoke_result_t<C>, SmallVector<int>>;
496
504template <typename C>
506
507
508// ----------------------------------------------------------------------------
509// Task
510// ----------------------------------------------------------------------------
511
569class Task {
570
571 friend class FlowBuilder;
572 friend class Runtime;
573 friend class NonpreemptiveRuntime;
574 friend class Taskflow;
575 friend class TaskView;
576 friend class Executor;
577
578 public:
579
585 Task() = default;
586
599 Task(const Task& other);
600
612 Task& operator = (const Task& other);
613
622 Task& operator = (std::nullptr_t);
623
636 bool operator == (const Task& rhs) const;
637
650 bool operator != (const Task& rhs) const;
651
663 const std::string& name() const;
664
677 size_t num_successors() const;
678
691 size_t num_predecessors() const;
692
720 size_t num_strong_dependencies() const;
721
749 size_t num_weak_dependencies() const;
750
763 Task& name(const std::string& name);
764
787 template <typename C>
788 Task& work(C&& callable);
789
807 template <GraphLike T>
808 Task& composed_of(T& object);
809
831 Task& adopt(tf::Graph&& graph);
832
852 template <typename... Ts>
853 Task& precede(Ts&&... tasks);
854
874 template <typename... Ts>
875 Task& succeed(Ts&&... tasks);
876
904 template <typename... Ts>
905 Task& remove_predecessors(Ts&&... tasks);
906
934 template <typename... Ts>
935 Task& remove_successors(Ts&&... tasks);
936
943 Task& release(Semaphore& semaphore);
944
951 template <typename I>
952 Task& release(I first, I last);
953
960 Task& acquire(Semaphore& semaphore);
961
968 template <typename I>
969 Task& acquire(I first, I last);
970
1000 Task& data(void* data);
1001
1002
1031
1047 TaskPriority priority() const;
1048
1061 void reset();
1062
1066 void reset_work();
1067
1082 bool empty() const;
1083
1097 bool has_work() const;
1098
1121 template <typename V>
1122 void for_each_successor(V&& visitor) const;
1123
1145 template <typename V>
1146 void for_each_predecessor(V&& visitor) const;
1147
1168 template <typename V>
1169 void for_each_subflow_task(V&& visitor) const;
1170
1183 size_t hash_value() const;
1184
1197 TaskType type() const;
1198
1208 void dump(std::ostream& ostream) const;
1209
1237 void* data() const;
1238
1279 std::exception_ptr exception_ptr() const;
1280
1286 bool has_exception_ptr() const;
1287
1288 private:
1289
1290 Task(Node*);
1291
1292 Node* _node {nullptr};
1293};
1294
1295// Constructor
1296inline Task::Task(Node* node) : _node {node} {
1297}
1298
1299// Constructor
1300inline Task::Task(const Task& rhs) : _node {rhs._node} {
1301}
1302
1303// Function: precede
1304template <typename... Ts>
1305Task& Task::precede(Ts&&... tasks) {
1306 (_node->_precede(tasks._node), ...);
1307 //_precede(std::forward<Ts>(tasks)...);
1308 return *this;
1309}
1310
1311// Function: succeed
1312template <typename... Ts>
1313Task& Task::succeed(Ts&&... tasks) {
1314 (tasks._node->_precede(_node), ...);
1315 //_succeed(std::forward<Ts>(tasks)...);
1316 return *this;
1317}
1318
1319// Function: remove_predecessors
1320template <typename... Ts>
1322 (tasks._node->_remove_successors(_node), ...);
1323 (_node->_remove_predecessors(tasks._node), ...);
1324 return *this;
1325}
1326
1327// Function: remove_successors
1328template <typename... Ts>
1330 (_node->_remove_successors(tasks._node), ...);
1331 (tasks._node->_remove_predecessors(_node), ...);
1332 return *this;
1333}
1334
1335// Function: composed_of
1336template <GraphLike T>
1338 _node->_handle.emplace<Node::Module>(retrieve_graph(target));
1339 return *this;
1340}
1341
1342// Function: adopt
1343inline Task& Task::adopt(Graph&& graph) {
1344 _node->_handle.emplace<Node::AdoptedModule>(std::move(graph));
1345 return *this;
1346}
1347
1348// Operator =
1349inline Task& Task::operator = (const Task& rhs) {
1350 _node = rhs._node;
1351 return *this;
1352}
1353
1354// Operator =
1355inline Task& Task::operator = (std::nullptr_t ptr) {
1356 _node = ptr;
1357 return *this;
1358}
1359
1360// Operator ==
1361inline bool Task::operator == (const Task& rhs) const {
1362 return _node == rhs._node;
1363}
1364
1365// Operator !=
1366inline bool Task::operator != (const Task& rhs) const {
1367 return _node != rhs._node;
1368}
1369
1370// Function: name
1371inline Task& Task::name(const std::string& name) {
1372 _node->_name = name;
1373 return *this;
1374}
1375
1376// Function: acquire
1378 if(!_node->_semaphores) {
1379 _node->_semaphores = std::make_unique<Node::Semaphores>();
1380 }
1381 _node->_semaphores->to_acquire.push_back(&s);
1382 return *this;
1383}
1384
1385// Function: acquire
1386template <typename I>
1387Task& Task::acquire(I first, I last) {
1388 if(!_node->_semaphores) {
1389 _node->_semaphores = std::make_unique<Node::Semaphores>();
1390 }
1391 _node->_semaphores->to_acquire.reserve(
1392 _node->_semaphores->to_acquire.size() + std::distance(first, last)
1393 );
1394 for(auto s = first; s != last; ++s){
1395 _node->_semaphores->to_acquire.push_back(&(*s));
1396 }
1397 return *this;
1398}
1399
1400// Function: release
1402 if(!_node->_semaphores) {
1403 _node->_semaphores = std::make_unique<Node::Semaphores>();
1404 }
1405 _node->_semaphores->to_release.push_back(&s);
1406 return *this;
1407}
1408
1409// Function: release
1410template <typename I>
1411Task& Task::release(I first, I last) {
1412 if(!_node->_semaphores) {
1413 _node->_semaphores = std::make_unique<Node::Semaphores>();
1414 }
1415 _node->_semaphores->to_release.reserve(
1416 _node->_semaphores->to_release.size() + std::distance(first, last)
1417 );
1418 for(auto s = first; s != last; ++s) {
1419 _node->_semaphores->to_release.push_back(&(*s));
1420 }
1421 return *this;
1422}
1423
1424// Procedure: reset
1425inline void Task::reset() {
1426 _node = nullptr;
1427}
1428
1429// Procedure: reset_work
1430inline void Task::reset_work() {
1431 _node->_handle.emplace<std::monostate>();
1432}
1433
1434// Function: name
1435inline const std::string& Task::name() const {
1436 return _node->_name;
1437}
1438
1439// Function: num_predecessors
1440inline size_t Task::num_predecessors() const {
1441 return _node->num_predecessors();
1442}
1443
1444// Function: num_strong_dependencies
1445inline size_t Task::num_strong_dependencies() const {
1446 return _node->num_strong_dependencies();
1447}
1448
1449// Function: num_weak_dependencies
1450inline size_t Task::num_weak_dependencies() const {
1451 return _node->num_weak_dependencies();
1452}
1453
1454// Function: num_successors
1455inline size_t Task::num_successors() const {
1456 return _node->num_successors();
1457}
1458
1459// Function: empty
1460inline bool Task::empty() const {
1461 return _node == nullptr;
1462}
1463
1464// Function: has_work
1465inline bool Task::has_work() const {
1466 return _node ? _node->_handle.index() != 0 : false;
1467}
1468
1469// Function: exception
1470inline std::exception_ptr Task::exception_ptr() const {
1471 return _node ? _node->_exception_ptr : nullptr;
1472}
1473
1474// Function: has_exception
1475inline bool Task::has_exception_ptr() const {
1476 return _node ? (_node->_exception_ptr != nullptr) : false;
1477}
1478
1479// Function: task_type
1480inline TaskType Task::type() const {
1481 switch(_node->_handle.index()) {
1482 case Node::PLACEHOLDER: return TaskType::PLACEHOLDER;
1483 case Node::STATIC: return TaskType::STATIC;
1484 case Node::RUNTIME: return TaskType::RUNTIME;
1485 case Node::NONPREEMPTIVE_RUNTIME: return TaskType::RUNTIME;
1486 case Node::SUBFLOW: return TaskType::SUBFLOW;
1487 case Node::CONDITION: return TaskType::CONDITION;
1488 case Node::MULTI_CONDITION: return TaskType::CONDITION;
1489 case Node::MODULE: return TaskType::MODULE;
1490 case Node::ADOPTED_MODULE: return TaskType::MODULE;
1491 case Node::ASYNC: return TaskType::ASYNC;
1492 case Node::DEPENDENT_ASYNC: return TaskType::ASYNC;
1493 default: return TaskType::UNDEFINED;
1494 }
1495}
1496
1497// Function: for_each_successor
1498template <typename V>
1499void Task::for_each_successor(V&& visitor) const {
1500 for(size_t i=0; i<_node->_num_successors; ++i) {
1501 visitor(Task(_node->_edges[i]));
1502 }
1503}
1504
1505// Function: for_each_predecessor
1506template <typename V>
1507void Task::for_each_predecessor(V&& visitor) const {
1508 for(size_t i=_node->_num_successors; i<_node->_edges.size(); ++i) {
1509 visitor(Task(_node->_edges[i]));
1510 }
1511}
1512
1513// Function: for_each_subflow_task
1514template <typename V>
1515void Task::for_each_subflow_task(V&& visitor) const {
1516 if(auto ptr = std::get_if<Node::Subflow>(&_node->_handle); ptr) {
1517 for(auto itr = ptr->subgraph.begin(); itr != ptr->subgraph.end(); ++itr) {
1518 visitor(Task(*itr));
1519 }
1520 }
1521}
1522
1523// Function: hash_value
1524inline size_t Task::hash_value() const {
1525 return std::hash<Node*>{}(_node);
1526}
1527
1528// Procedure: dump
1529inline void Task::dump(std::ostream& os) const {
1530 os << "task ";
1531 if(name().empty()) os << _node;
1532 else os << name();
1533 os << " [type=" << to_string(type()) << ']';
1534}
1535
1536// Function: work
1537template <typename C>
1539
1540 if constexpr(is_static_task_v<C>) {
1541 _node->_handle.emplace<Node::Static>(std::forward<C>(c));
1542 }
1543 else if constexpr(is_runtime_task_v<C>) {
1544 _node->_handle.emplace<Node::Runtime>(std::forward<C>(c));
1545 }
1546 else if constexpr(is_subflow_task_v<C>) {
1547 _node->_handle.emplace<Node::Subflow>(std::forward<C>(c));
1548 }
1549 else if constexpr(is_condition_task_v<C>) {
1550 _node->_handle.emplace<Node::Condition>(std::forward<C>(c));
1551 }
1552 else if constexpr(is_multi_condition_task_v<C>) {
1553 _node->_handle.emplace<Node::MultiCondition>(std::forward<C>(c));
1554 }
1555 else {
1556 static_assert(dependent_false_v<C>, "invalid task callable");
1557 }
1558 return *this;
1559}
1560
1561// Function: data
1562inline void* Task::data() const {
1563 return _node->_data;
1564}
1565
1566// Function: data
1567inline Task& Task::data(void* data) {
1568 _node->_data = data;
1569 return *this;
1570}
1571
1572
1573// Function: priority
1575#ifdef TF_ENABLE_TASK_PRIORITY
1576 // priority bits are stored directly as the TaskPriority value
1577 // (HIGH=0, NORMAL=1, LOW=2); an unset priority reads back as HIGH
1578 return static_cast<TaskPriority>(_node->priority());
1579#else
1580 return TaskPriority::HIGH;
1581#endif
1582}
1583
1584// Function: priority
1585inline Task& Task::priority([[maybe_unused]] TaskPriority p) {
1586#ifdef TF_ENABLE_TASK_PRIORITY
1587 _node->_set_priority(p);
1588#endif
1589 return *this;
1590}
1591
1592// ----------------------------------------------------------------------------
1593// global ostream
1594// ----------------------------------------------------------------------------
1595
1599inline std::ostream& operator << (std::ostream& os, const Task& task) {
1600 task.dump(os);
1601 return os;
1602}
1603
1604// ----------------------------------------------------------------------------
1605// Task View
1606// ----------------------------------------------------------------------------
1607
1613class TaskView {
1614
1615 friend class Executor;
1616
1617 public:
1618
1622 const std::string& name() const;
1623
1627 size_t num_successors() const;
1628
1632 size_t num_predecessors() const;
1633
1637 size_t num_strong_dependencies() const;
1638
1642 size_t num_weak_dependencies() const;
1643
1652 template <typename V>
1653 void for_each_successor(V&& visitor) const;
1654
1663 template <typename V>
1664 void for_each_predecessor(V&& visitor) const;
1665
1669 TaskType type() const;
1670
1674 size_t hash_value() const;
1675
1676 private:
1677
1678 TaskView(const Node&);
1679 TaskView(const TaskView&) = default;
1680
1681 const Node& _node;
1682};
1683
1684// Constructor
1685inline TaskView::TaskView(const Node& node) : _node {node} {
1686}
1687
1688// Function: name
1689inline const std::string& TaskView::name() const {
1690 return _node._name;
1691}
1692
1693// Function: num_predecessors
1694inline size_t TaskView::num_predecessors() const {
1695 return _node.num_predecessors();
1696}
1697
1698// Function: num_strong_dependencies
1700 return _node.num_strong_dependencies();
1701}
1702
1703// Function: num_weak_dependencies
1704inline size_t TaskView::num_weak_dependencies() const {
1705 return _node.num_weak_dependencies();
1706}
1707
1708// Function: num_successors
1709inline size_t TaskView::num_successors() const {
1710 return _node.num_successors();
1711}
1712
1713// Function: type
1714inline TaskType TaskView::type() const {
1715 switch(_node._handle.index()) {
1716 case Node::PLACEHOLDER: return TaskType::PLACEHOLDER;
1717 case Node::STATIC: return TaskType::STATIC;
1718 case Node::RUNTIME: return TaskType::RUNTIME;
1719 case Node::NONPREEMPTIVE_RUNTIME: return TaskType::RUNTIME;
1720 case Node::SUBFLOW: return TaskType::SUBFLOW;
1721 case Node::CONDITION: return TaskType::CONDITION;
1722 case Node::MULTI_CONDITION: return TaskType::CONDITION;
1723 case Node::MODULE: return TaskType::MODULE;
1724 case Node::ADOPTED_MODULE: return TaskType::MODULE;
1725 case Node::ASYNC: return TaskType::ASYNC;
1726 case Node::DEPENDENT_ASYNC: return TaskType::ASYNC;
1727 default: return TaskType::UNDEFINED;
1728 }
1729}
1730
1731// Function: hash_value
1732inline size_t TaskView::hash_value() const {
1733 return std::hash<const Node*>{}(&_node);
1734}
1735
1736// Function: for_each_successor
1737template <typename V>
1738void TaskView::for_each_successor(V&& visitor) const {
1739 for(size_t i=0; i<_node._num_successors; ++i) {
1740 visitor(TaskView(*_node._edges[i]));
1741 }
1742 //for(size_t i=0; i<_node._successors.size(); ++i) {
1743 // visitor(TaskView(*_node._successors[i]));
1744 //}
1745}
1746
1747// Function: for_each_predecessor
1748template <typename V>
1749void TaskView::for_each_predecessor(V&& visitor) const {
1750 for(size_t i=_node._num_successors; i<_node._edges.size(); ++i) {
1751 visitor(TaskView(*_node._edges[i]));
1752 }
1753 //for(size_t i=0; i<_node._predecessors.size(); ++i) {
1754 // visitor(TaskView(*_node._predecessors[i]));
1755 //}
1756}
1757
1758} // end of namespace tf. ----------------------------------------------------
1759
1760namespace std {
1761
1812template <>
1813struct hash<tf::Task> {
1814 auto operator() (const tf::Task& task) const noexcept {
1815 return task.hash_value();
1816 }
1817};
1818
1819
1820
1874template <>
1875struct hash<tf::TaskView> {
1876 auto operator() (const tf::TaskView& task_view) const noexcept {
1877 return task_view.hash_value();
1878 }
1879};
1880
1881} // end of namespace std ----------------------------------------------------
class to create a graph object
Definition graph.hpp:47
class to create a semophore object for building a concurrency constraint
Definition semaphore.hpp:68
class to define a vector optimized for small array
Definition small_vector.hpp:931
class to access task information from the observer interface
Definition task.hpp:1613
size_t num_predecessors() const
queries the number of predecessors of the task
Definition task.hpp:1694
void for_each_predecessor(V &&visitor) const
applies an visitor callable to each predecessor of the task
Definition task.hpp:1749
void for_each_successor(V &&visitor) const
applies an visitor callable to each successor of the task
Definition task.hpp:1738
TaskType type() const
queries the task type
Definition task.hpp:1714
size_t num_weak_dependencies() const
queries the number of weak dependencies of the task
Definition task.hpp:1704
size_t hash_value() const
obtains a hash value of the underlying node
Definition task.hpp:1732
const std::string & name() const
queries the name of the task
Definition task.hpp:1689
size_t num_strong_dependencies() const
queries the number of strong dependencies of the task
Definition task.hpp:1699
size_t num_successors() const
queries the number of successors of the task
Definition task.hpp:1709
class to create a task handle over a taskflow node
Definition task.hpp:569
Task & acquire(Semaphore &semaphore)
makes the task acquire the given semaphore
Definition task.hpp:1377
const std::string & name() const
queries the name of the task
Definition task.hpp:1435
size_t num_strong_dependencies() const
queries the number of strong dependencies of the task
Definition task.hpp:1445
Task & remove_successors(Ts &&... tasks)
removes successor links from this to other tasks
Definition task.hpp:1329
size_t num_successors() const
queries the number of successors of the task
Definition task.hpp:1455
size_t hash_value() const
obtains a hash value of the underlying node
Definition task.hpp:1524
void for_each_subflow_task(V &&visitor) const
applies an visitor callable to each subflow task
Definition task.hpp:1515
Task & release(Semaphore &semaphore)
makes the task release the given semaphore
Definition task.hpp:1401
Task & work(C &&callable)
assigns a callable
Definition task.hpp:1538
std::exception_ptr exception_ptr() const
retrieves the exception pointer of this task
Definition task.hpp:1470
void reset()
resets the task handle to null
Definition task.hpp:1425
void for_each_predecessor(V &&visitor) const
applies an visitor callable to each predecessor of the task
Definition task.hpp:1507
void * data() const
queries pointer to user data
Definition task.hpp:1562
void dump(std::ostream &ostream) const
dumps the task through an output stream
Definition task.hpp:1529
Task & succeed(Ts &&... tasks)
adds precedence links from other tasks to this
Definition task.hpp:1313
Task & operator=(const Task &other)
replaces the contents with a copy of the other task
Definition task.hpp:1349
Task()=default
constructs an empty task
bool empty() const
queries if the task handle is associated with a taskflow node
Definition task.hpp:1460
Task & precede(Ts &&... tasks)
adds precedence links from this to other tasks
Definition task.hpp:1305
bool has_exception_ptr() const
queries if the task has an exception pointer
Definition task.hpp:1475
Task & adopt(tf::Graph &&graph)
creates a module task from a graph by taking over its ownership
Definition task.hpp:1343
Task & composed_of(T &object)
creates a module task from a taskflow
Definition task.hpp:1337
TaskPriority priority() const
queries the priority level of the task
Definition task.hpp:1574
Task & remove_predecessors(Ts &&... tasks)
removes predecessor links from other tasks to this
Definition task.hpp:1321
size_t num_weak_dependencies() const
queries the number of weak dependencies of the task
Definition task.hpp:1450
bool operator==(const Task &rhs) const
compares if two tasks are associated with the same taskflow node
Definition task.hpp:1361
size_t num_predecessors() const
queries the number of predecessors of the task
Definition task.hpp:1440
void reset_work()
resets the associated work to a placeholder
Definition task.hpp:1430
TaskType type() const
returns the task type
Definition task.hpp:1480
bool operator!=(const Task &rhs) const
compares if two tasks are not associated with the same taskflow node
Definition task.hpp:1366
bool has_work() const
queries if the task has a work assigned
Definition task.hpp:1465
Task & data(void *data)
assigns pointer to user data
Definition task.hpp:1567
void for_each_successor(V &&visitor) const
applies an visitor callable to each successor of the task
Definition task.hpp:1499
concept to check if a callable is a condition task
Definition task.hpp:397
determines if a callable is a multi-condition task
Definition task.hpp:494
concept to check if a callable is a runtime task
Definition task.hpp:303
concept to check if a callable is a static task
Definition task.hpp:140
concept to check if a callable is a subflow task
Definition task.hpp:220
taskflow namespace
Definition small_vector.hpp:20
constexpr bool is_condition_task_v
concept to check if a callable is a condition task (variable template)
Definition task.hpp:408
constexpr bool is_static_task_v
concept to check if a callable is a static task (variable template)
Definition task.hpp:151
TaskType
enumeration of all task types
Definition task.hpp:21
@ UNDEFINED
undefined task type (for internal use only)
Definition task.hpp:37
@ MODULE
module task type
Definition task.hpp:33
@ SUBFLOW
dynamic (subflow) task type
Definition task.hpp:29
@ CONDITION
condition task type
Definition task.hpp:31
@ ASYNC
asynchronous task type
Definition task.hpp:35
@ PLACEHOLDER
placeholder task type
Definition task.hpp:23
@ RUNTIME
runtime task type
Definition task.hpp:27
@ STATIC
static task type
Definition task.hpp:25
const char * to_string(TaskType type)
convert a task type to a human-readable string
Definition task.hpp:66
constexpr bool is_multi_condition_task_v
concept to check if a callable is a multi-condition task (variable template)
Definition task.hpp:505
Graph & retrieve_graph(T &target)
retrieves a reference to the underlying tf::Graph from an object
Definition graph.hpp:1108
TaskPriority
enumeration of all task priority levels
Definition graph.hpp:152
@ HIGH
the highest task priority level (most urgent)
Definition graph.hpp:154
std::ostream & operator<<(std::ostream &os, const Task &task)
overload of ostream inserter operator for Task
Definition task.hpp:1599
constexpr bool is_subflow_task_v
concept to check if a callable is a subflow task (variable template)
Definition task.hpp:231
constexpr bool is_runtime_task_v
concept to check if a callable is a runtime task (variable template)
Definition task.hpp:317
hash specialization for std::hash<tf::Task>