-
Notifications
You must be signed in to change notification settings - Fork 10.6k
[Concurrency] TaskPool, close cousin to TaskGroup #62271
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
…d tasks anymore" This reverts commit fd2eb5f.
A TaskPool is similar to a TaskGroup, but functions very differently. Tasks added to the pool execute the same way as in a group, however they cannot be awaited on explicitly. The implementation does not track ready tasks at all and therefore is not able to track the exact number of times a next() would have to be resumed. Instead, it automatically removes completed child tasks, and removes their pending counts from the pool status. This allows us to implement a waitAll() however it is not possible to wait for individual completions (one could "wait for the next completion", but that is not very useful, as we would not know _which_ one it was). A TaskPool is useful in "runs forever" core loops of rpc and http servers, where the top level task of the server runs a TaskPool, and dispatches new tasks to handle each incoming request; and these may be running concurrently. We want to use child tasks to keep this efficient, but using a Group this is not possible because we must collect the tasks as soon as they complete, but cannot do so from the task group since it is a single task awaiting on a sequence.
Task_CopyTaskLocals = 10, | ||
Task_InheritContext = 11, | ||
Task_EnqueueJob = 12, | ||
Task_AddPendingGroupTaskUnconditionally = 13, // used for: TaskGroup, TaskPool |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ABI: Can we rename this actually? I suspect no since ABI even though the value remains the same.
BUILTIN_OPERAND_OWNERSHIP(DestroyingConsume, EndAsyncLetLifetime) | ||
BUILTIN_OPERAND_OWNERSHIP(InstantaneousUse, CreateTaskGroup) | ||
BUILTIN_OPERAND_OWNERSHIP(InstantaneousUse, DestroyTaskGroup) | ||
BUILTIN_OPERAND_OWNERSHIP(InstantaneousUse, DestroyTaskGroup) // TODO: should this be destroying consume? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fly by question, wasn't sure?
SWIFT_EXPORT_FROM(swift_Concurrency), SWIFT_CC(swift), | ||
swift::, (TaskPool *pool, const Metadata *Void), (pool, Void)) | ||
|
||
OVERRIDE_TASK_STATUS(taskPool_attachChild, void, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, STATUS on purpose; has to be there to access the locks AFAIR
priority: priority, isChildTask: true, copyTaskLocals: false, | ||
inheritContext: false, enqueueJob: true, | ||
addPendingGroupTaskUnconditionally: false | ||
addPendingTaskUnconditionally: false |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ABI: I think we can rename the parameter...?
|
||
void lock() const { mutex_.lock(); } | ||
void unlock() const { mutex_.unlock(); } | ||
#endif |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TODO: we definitely can remove this lock; so will do so once we have agreed on exact semantics of the pool.
/// This is because we will NOT add a task to a cancelled group, unless doing | ||
/// so unconditionally. | ||
/// | ||
/// Returns *assumed* new status, including the just performed +1. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- TODO: go over all comments again and remove mentions of group and make sure the comments actually cover the details of the
Pool
impl
|
||
void lock() const { mutex_.lock(); } | ||
void unlock() const { mutex_.unlock(); } | ||
#endif |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TODO: remove the locks, we don't need them at all
|
||
TaskPoolTaskStatusRecord * TaskPool::getTaskRecord() { | ||
return asImpl(this)->getTaskRecord(); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The actual TaskPool impl
swift_asyncLet_end(nullptr); | ||
} | ||
|
||
// TODO: handle TaskPool |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TODO
var isChildTask: Bool | ||
var isFuture: Bool | ||
var isGroupChildTask: Bool | ||
// TODO: isPoolChildTask |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TODO: support in dump concurrency
@swift-ci please smoke test |
@swift-ci please build toolchain |
public func withTaskPool<PoolResult>( | ||
returning returnType: PoolResult.Type = PoolResult.self, | ||
body: (inout TaskPool) async throws -> PoolResult | ||
) async rethrows -> PoolResult { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think rethrows is right here: if the child tasks throw, this throws.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah that's wrong. The entire throws handling is not done :) this'll need the same dance as the group, so two with... method versions - thank you for noticing :)
We're going with the "option" version of the implementation for now: #62361 |
rdar://101965913
A
TaskPool
is similar to aTaskGroup
, but functions very differently.Tasks added to the pool execute the same way as in a group, however they cannot be awaited on explicitly. The implementation does not track ready tasks at all and therefore is not able to track the exact number of times a next() would have to be resumed.
Instead, it automatically removes completed child tasks, and removes their pending counts from the pool status. This allows us to implement a waitAll() however it is not possible to wait for individual completions (one could "wait for the next completion", but that is not very useful, as we would not know which one it was).
A TaskPool is useful in "runs forever" core loops of rpc and http servers, where the top level task of the server runs a TaskPool, and dispatches new tasks to handle each incoming request; and these may be running concurrently. We want to use child tasks to keep this efficient, but using a Group this is not possible because we must collect the tasks as soon as they complete, but cannot do so from the task group since it is a single task awaiting on a sequence.
Specifically, when implementing servers which stick to structured concurrency, we often end up with such code:
however, this use of a group is ill-suited for this use-case. As written, this task group will leak all the child
Task
objects until the listening socket either terminates or throws. If this was written for a long-running server, it is entirely possible for this Task Group to survive for a period of days, leaking thousands of Task objects.Instead, a
TaskPool
removes child tasks as soon as they complete, making such implementation not only possible but efficient:This is the only 100% safe way to utilize structured concurrency to implement such long lived servers, but we'll discuss this more in an upcoming proposal that @Lukasa has been working on.
TODOs:
DumpConcurrency
ready
queue, unlike the task groupcc @Lukasa @FranzBusch @PeterAdams-A