Skip to content

Commit 056024a

Browse files
authored
Merge pull request #36993 from eeckstein/task-allocation
concurrency: alloc an async-let task with the parent's allocator.
2 parents af789d5 + 075ad87 commit 056024a

File tree

6 files changed

+141
-42
lines changed

6 files changed

+141
-42
lines changed

include/swift/ABI/Task.h

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,10 @@ class TaskGroup;
3939
extern FullMetadata<DispatchClassMetadata> jobHeapMetadata;
4040

4141
/// A schedulable job.
42-
class alignas(2 * alignof(void*)) Job : public HeapObject {
42+
class alignas(2 * alignof(void*)) Job :
43+
// For async-let tasks, the refcount bits are initialized as "immortal"
44+
// because such a task is allocated with the parent's stack allocator.
45+
public HeapObject {
4346
public:
4447
// Indices into SchedulerPrivate, for use by the runtime.
4548
enum {
@@ -88,6 +91,14 @@ class alignas(2 * alignof(void*)) Job : public HeapObject {
8891
assert(isAsyncTask() && "wrong constructor for a non-task job");
8992
}
9093

94+
/// Create a job with "immortal" reference counts.
95+
/// Used for async let tasks.
96+
Job(JobFlags flags, TaskContinuationFunction *invoke,
97+
const HeapMetadata *metadata, InlineRefCounts::Immortal_t immortal)
98+
: HeapObject(metadata, immortal), Flags(flags), ResumeTask(invoke) {
99+
assert(isAsyncTask() && "wrong constructor for a non-task job");
100+
}
101+
91102
bool isAsyncTask() const {
92103
return Flags.isAsyncTask();
93104
}
@@ -201,6 +212,21 @@ class AsyncTask : public Job {
201212
assert(flags.isAsyncTask());
202213
}
203214

215+
/// Create a task with "immortal" reference counts.
216+
/// Used for async let tasks.
217+
AsyncTask(const HeapMetadata *metadata, InlineRefCounts::Immortal_t immortal,
218+
JobFlags flags,
219+
TaskContinuationFunction *run,
220+
AsyncContext *initialContext)
221+
: Job(flags, run, metadata, immortal),
222+
ResumeContext(initialContext),
223+
Status(ActiveTaskStatus()),
224+
Local(TaskLocal::Storage()) {
225+
assert(flags.isAsyncTask());
226+
}
227+
228+
~AsyncTask();
229+
204230
/// Given that we've already fully established the job context
205231
/// in the current thread, start running this task. To establish
206232
/// the job context correctly, call swift_job_run or

stdlib/public/CompatibilityOverride/CompatibilityOverrideConcurrency.def

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,10 +100,10 @@ OVERRIDE_TASK(task_create_group_future_common, AsyncTaskAndContext, , , ,
100100
(JobFlags flags, TaskGroup *group,
101101
const Metadata *futureResultType,
102102
FutureAsyncSignature::FunctionType *function,
103-
void *closureContext, bool owningClosureContext,
103+
void *closureContext, bool isAsyncLetTask,
104104
size_t initialContextSize),
105105
(flags, group, futureResultType, function, closureContext,
106-
owningClosureContext, initialContextSize))
106+
isAsyncLetTask, initialContextSize))
107107

108108
OVERRIDE_TASK(task_future_wait, void, SWIFT_EXPORT_FROM(swift_Concurrency),
109109
SWIFT_CC(swiftasync), swift::,

stdlib/public/Concurrency/AsyncLet.cpp

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -105,14 +105,13 @@ static void swift_asyncLet_startImpl(AsyncLet *alet,
105105
flags.task_setIsFuture(true);
106106
flags.task_setIsChildTask(true);
107107

108-
auto childTaskAndContext = swift_task_create_future_no_escaping(
108+
auto childTaskAndContext = swift_task_create_async_let_future(
109109
flags,
110110
futureResultType,
111111
closureEntryPoint,
112112
closureContext);
113113

114114
AsyncTask *childTask = childTaskAndContext.Task;
115-
swift_retain(childTask);
116115

117116
assert(childTask->isFuture());
118117
assert(childTask->hasChildFragment());
@@ -165,7 +164,13 @@ static void swift_asyncLet_endImpl(AsyncLet *alet) {
165164
// TODO: we need to implicitly await either before the end or here somehow.
166165

167166
// and finally, release the task and free the async-let
168-
swift_release(task);
167+
AsyncTask *parent = swift_task_getCurrent();
168+
assert(parent && "async-let must have a parent task");
169+
170+
#if SWIFT_TASK_PRINTF_DEBUG
171+
fprintf(stderr, "[%p] async let end of task %p, parent: %p\n", pthread_self(), task, parent);
172+
#endif
173+
_swift_task_dealloc_specific(parent, task);
169174
}
170175

171176
// =============================================================================

stdlib/public/Concurrency/Task.cpp

Lines changed: 93 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -174,23 +174,31 @@ static void destroyJob(SWIFT_CONTEXT HeapObject *obj) {
174174
assert(false && "A non-task job should never be destroyed as heap metadata.");
175175
}
176176

177-
SWIFT_CC(swift)
178-
static void destroyTask(SWIFT_CONTEXT HeapObject *obj) {
179-
auto task = static_cast<AsyncTask*>(obj);
177+
AsyncTask::~AsyncTask() {
180178
// For a future, destroy the result.
181-
if (task->isFuture()) {
182-
task->futureFragment()->destroy();
179+
if (isFuture()) {
180+
futureFragment()->destroy();
183181
}
184182

185183
// Release any objects potentially held as task local values.
186-
task->Local.destroy(task);
184+
Local.destroy(this);
185+
}
186+
187+
SWIFT_CC(swift)
188+
static void destroyTask(SWIFT_CONTEXT HeapObject *obj) {
189+
auto task = static_cast<AsyncTask*>(obj);
190+
191+
task->~AsyncTask();
187192

188193
// The task execution itself should always hold a reference to it, so
189194
// if we get here, we know the task has finished running, which means
190195
// swift_task_complete should have been run, which will have torn down
191196
// the task-local allocator. There's actually nothing else to clean up
192197
// here.
193198

199+
#if SWIFT_TASK_PRINTF_DEBUG
200+
fprintf(stderr, "[%p] destroy task %p\n", pthread_self(), task);
201+
#endif
194202
free(task);
195203
}
196204

@@ -250,13 +258,9 @@ static FullMetadata<DispatchClassMetadata> taskHeapMetadata = {
250258
const void *const swift::_swift_concurrency_debug_asyncTaskMetadata =
251259
static_cast<Metadata *>(&taskHeapMetadata);
252260

253-
/// The function that we put in the context of a simple task
254-
/// to handle the final return.
255-
SWIFT_CC(swiftasync)
256-
static void completeTask(SWIFT_ASYNC_CONTEXT AsyncContext *context,
257-
SWIFT_CONTEXT SwiftError *error) {
258-
// Set that there's no longer a running task in the current thread.
259-
auto task = _swift_task_clearCurrent();
261+
static void completeTaskImpl(AsyncTask *task,
262+
AsyncContext *context,
263+
SwiftError *error) {
260264
assert(task && "completing task, but there is no active task registered");
261265

262266
// Store the error result.
@@ -277,15 +281,41 @@ static void completeTask(SWIFT_ASYNC_CONTEXT AsyncContext *context,
277281
#endif
278282

279283
// Complete the future.
284+
// Warning: This deallocates the task in case it's an async let task.
285+
// The task must not be accessed afterwards.
280286
if (task->isFuture()) {
281287
task->completeFuture(context);
282288
}
283289

284290
// TODO: set something in the status?
285-
if (task->hasChildFragment()) {
291+
// if (task->hasChildFragment()) {
286292
// TODO: notify the parent somehow?
287293
// TODO: remove this task from the child-task chain?
288-
}
294+
// }
295+
}
296+
297+
/// The function that we put in the context of a simple task
298+
/// to handle the final return.
299+
SWIFT_CC(swiftasync)
300+
static void completeTask(SWIFT_ASYNC_CONTEXT AsyncContext *context,
301+
SWIFT_CONTEXT SwiftError *error) {
302+
// Set that there's no longer a running task in the current thread.
303+
auto task = _swift_task_clearCurrent();
304+
assert(task && "completing task, but there is no active task registered");
305+
306+
completeTaskImpl(task, context, error);
307+
}
308+
309+
/// The function that we put in the context of a simple task
310+
/// to handle the final return.
311+
SWIFT_CC(swiftasync)
312+
static void completeTaskAndRelease(SWIFT_ASYNC_CONTEXT AsyncContext *context,
313+
SWIFT_CONTEXT SwiftError *error) {
314+
// Set that there's no longer a running task in the current thread.
315+
auto task = _swift_task_clearCurrent();
316+
assert(task && "completing task, but there is no active task registered");
317+
318+
completeTaskImpl(task, context, error);
289319

290320
// Release the task, balancing the retain that a running task has on itself.
291321
// If it was a group child task, it will remain until the group returns it.
@@ -304,7 +334,7 @@ static void completeTaskWithClosure(SWIFT_ASYNC_CONTEXT AsyncContext *context,
304334
swift_release((HeapObject *)asyncContextPrefix->closureContext);
305335

306336
// Clean up the rest of the task.
307-
return completeTask(context, error);
337+
return completeTaskAndRelease(context, error);
308338
}
309339

310340
SWIFT_CC(swiftasync)
@@ -332,11 +362,16 @@ static void task_wait_throwing_resume_adapter(SWIFT_ASYNC_CONTEXT AsyncContext *
332362
}
333363

334364
/// All `swift_task_create*` variants funnel into this common implementation.
365+
///
366+
/// If \p isAsyncLetTask is true, the \p closureContext is not heap allocated,
367+
/// but stack-allocated (and must not be ref-counted).
368+
/// Also, async-let tasks are not heap allcoated, but allcoated with the parent
369+
/// task's stack allocator.
335370
static AsyncTaskAndContext swift_task_create_group_future_commonImpl(
336371
JobFlags flags, TaskGroup *group,
337372
const Metadata *futureResultType,
338373
FutureAsyncSignature::FunctionType *function,
339-
void *closureContext, bool owningClosureContext,
374+
void *closureContext, bool isAsyncLetTask,
340375
size_t initialContextSize) {
341376
assert((futureResultType != nullptr) == flags.task_isFuture());
342377
assert(!flags.task_isFuture() ||
@@ -378,10 +413,19 @@ static AsyncTaskAndContext swift_task_create_group_future_commonImpl(
378413

379414
assert(amountToAllocate % MaximumAlignment == 0);
380415

381-
// TODO: allow optionally passing in an allocation+sizeOfIt to reuse for the task
382-
// if the necessary space is enough, we can initialize into it rather than malloc.
383-
// this would allow us to stack-allocate async-let related tasks.
384-
void *allocation = malloc(amountToAllocate);
416+
constexpr unsigned initialSlabSize = 512;
417+
418+
void *allocation = nullptr;
419+
if (isAsyncLetTask) {
420+
assert(parent);
421+
allocation = _swift_task_alloc_specific(parent,
422+
amountToAllocate + initialSlabSize);
423+
} else {
424+
allocation = malloc(amountToAllocate);
425+
}
426+
#if SWIFT_TASK_PRINTF_DEBUG
427+
fprintf(stderr, "[%p] allocate task %p, parent = %p\n", pthread_self(), allocation, parent);
428+
#endif
385429

386430
AsyncContext *initialContext =
387431
reinterpret_cast<AsyncContext*>(
@@ -417,9 +461,17 @@ static AsyncTaskAndContext swift_task_create_group_future_commonImpl(
417461

418462
// Initialize the task so that resuming it will run the given
419463
// function on the initial context.
420-
AsyncTask *task =
421-
new(allocation) AsyncTask(&taskHeapMetadata, flags,
422-
function, initialContext);
464+
AsyncTask *task = nullptr;
465+
if (isAsyncLetTask) {
466+
// Initialize the refcount bits to "immortal", so that
467+
// ARC operations don't have any effect on the task.
468+
task = new(allocation) AsyncTask(&taskHeapMetadata,
469+
InlineRefCounts::Immortal, flags,
470+
function, initialContext);
471+
} else {
472+
task = new(allocation) AsyncTask(&taskHeapMetadata, flags,
473+
function, initialContext);
474+
}
423475

424476
// Initialize the child fragment if applicable.
425477
if (parent) {
@@ -471,15 +523,21 @@ static AsyncTaskAndContext swift_task_create_group_future_commonImpl(
471523
// as if they might be null, even though the only time they ever might
472524
// be is the final hop. Store a signed null instead.
473525
initialContext->Parent = nullptr;
474-
initialContext->ResumeParent = reinterpret_cast<TaskContinuationFunction *>(
475-
(closureContext && owningClosureContext) ? &completeTaskWithClosure :
476-
&completeTask);
477526
initialContext->Flags = AsyncContextKind::Ordinary;
478527
initialContext->Flags.setShouldNotDeallocateInCallee(true);
479528

480529
// Initialize the task-local allocator.
481-
// TODO: consider providing an initial pre-allocated first slab to the allocator.
482-
_swift_task_alloc_initialize(task);
530+
if (isAsyncLetTask) {
531+
initialContext->ResumeParent = reinterpret_cast<TaskContinuationFunction *>(
532+
&completeTask);
533+
assert(parent);
534+
void *initialSlab = (char*)allocation + amountToAllocate;
535+
_swift_task_alloc_initialize_with_slab(task, initialSlab, initialSlabSize);
536+
} else {
537+
initialContext->ResumeParent = reinterpret_cast<TaskContinuationFunction *>(
538+
closureContext ? &completeTaskWithClosure : &completeTaskAndRelease);
539+
_swift_task_alloc_initialize(task);
540+
}
483541

484542
// TODO: if the allocator would be prepared earlier we could do this in some
485543
// other existing if-parent if rather than adding another one here.
@@ -494,7 +552,7 @@ static AsyncTaskAndContext swift_task_create_group_future_commonImpl(
494552
static AsyncTaskAndContext swift_task_create_group_future_common(
495553
JobFlags flags, TaskGroup *group, const Metadata *futureResultType,
496554
FutureAsyncSignature::FunctionType *function,
497-
void *closureContext, bool owningClosureContext,
555+
void *closureContext, bool isAsyncLetTask,
498556
size_t initialContextSize);
499557

500558
AsyncTaskAndContext
@@ -524,7 +582,7 @@ AsyncTaskAndContext swift::swift_task_create_group_future_f(
524582
return swift_task_create_group_future_common(flags, group,
525583
futureResultType,
526584
function, nullptr,
527-
/*owningClosureContext*/ false,
585+
/*isAsyncLetTask*/ false,
528586
initialContextSize);
529587
}
530588

@@ -560,11 +618,11 @@ AsyncTaskAndContext swift::swift_task_create_future(JobFlags flags,
560618
return swift_task_create_group_future_common(
561619
flags, nullptr, futureResultType,
562620
taskEntry, closureContext,
563-
/*owningClosureContext*/ true,
621+
/*isAsyncLetTask*/ false,
564622
initialContextSize);
565623
}
566624

567-
AsyncTaskAndContext swift::swift_task_create_future_no_escaping(JobFlags flags,
625+
AsyncTaskAndContext swift::swift_task_create_async_let_future(JobFlags flags,
568626
const Metadata *futureResultType,
569627
void *closureEntry,
570628
void *closureContext) {
@@ -579,7 +637,7 @@ AsyncTaskAndContext swift::swift_task_create_future_no_escaping(JobFlags flags,
579637
return swift_task_create_group_future_common(
580638
flags, nullptr, futureResultType,
581639
taskEntry, closureContext,
582-
/*owningClosureContext*/ false,
640+
/*isAsyncLetTask*/ true,
583641
initialContextSize);
584642
}
585643

@@ -599,7 +657,7 @@ swift::swift_task_create_group_future(
599657
return swift_task_create_group_future_common(
600658
flags, group, futureResultType,
601659
taskEntry, closureContext,
602-
/*owningClosureContext*/ true,
660+
/*isAsyncLetTask*/ false,
603661
initialContextSize);
604662
}
605663

stdlib/public/Concurrency/TaskAlloc.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,12 @@ void swift::_swift_task_alloc_initialize(AsyncTask *task) {
5151
new (task->AllocatorPrivate) TaskAllocator();
5252
}
5353

54+
void swift::_swift_task_alloc_initialize_with_slab(AsyncTask *task,
55+
void *firstSlabBuffer,
56+
size_t bufferCapacity) {
57+
new (task->AllocatorPrivate) TaskAllocator(firstSlabBuffer, bufferCapacity);
58+
}
59+
5460
static TaskAllocator &allocator(AsyncTask *task) {
5561
if (task)
5662
return reinterpret_cast<TaskAllocator &>(task->AllocatorPrivate);

stdlib/public/Concurrency/TaskPrivate.h

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ class TaskGroup;
3434
/// Initialize the task-local allocator in the given task.
3535
void _swift_task_alloc_initialize(AsyncTask *task);
3636

37+
void _swift_task_alloc_initialize_with_slab(AsyncTask *task,
38+
void *firstSlabBuffer,
39+
size_t bufferCapacity);
40+
3741
/// Destroy the task-local allocator in the given task.
3842
void _swift_task_alloc_destroy(AsyncTask *task);
3943

@@ -55,7 +59,7 @@ void runJobInEstablishedExecutorContext(Job *job);
5559
/// Clear the active task reference for the current thread.
5660
AsyncTask *_swift_task_clearCurrent();
5761

58-
AsyncTaskAndContext swift_task_create_future_no_escaping(JobFlags flags,
62+
AsyncTaskAndContext swift_task_create_async_let_future(JobFlags flags,
5963
const Metadata *futureResultType,
6064
void *closureEntry,
6165
void *closureContext);

0 commit comments

Comments
 (0)