Self Operations and Yielding
Self operations give ULTs direct control over their execution: yielding to the scheduler, suspending/resuming, and explicit control flow transfers. These are advanced techniques critical for implementing custom execution patterns and integrating with external event loops.
Key Concepts
- Self Operations
Operations that a ULT performs on itself: -
ABT_self_yield()orABT_thread_yield(): Voluntarily return control to scheduler -ABT_self_suspend(): Suspend self, requiring external resume -ABT_self_exit(): Terminate self immediately- Cooperative Scheduling
Unlike preemptive scheduling (OS threads), Argobots ULTs run until they voluntarily yield or block. This gives predictable execution but requires cooperation.
- Progress Polling
Common pattern in asynchronous systems: repeatedly check for completion while yielding to let other work progress. Essential in Margo for network progress.
Yield-Based Synchronization
Yielding enables fair cooperative scheduling:
1/*
2 * Yield-based synchronization: Cooperative multitasking
3 */
4
5#include <stdio.h>
6#include <abt.h>
7
8#define NUM_WORKERS 3
9#define ITERATIONS 5
10
11typedef struct {
12 int worker_id;
13 int *shared_counter;
14 ABT_mutex mutex;
15} worker_arg_t;
16
17void cooperative_worker(void *arg)
18{
19 worker_arg_t *worker = (worker_arg_t *)arg;
20
21 for (int i = 0; i < ITERATIONS; i++) {
22 /* Try to acquire lock */
23 ABT_mutex_lock(worker->mutex);
24
25 (*worker->shared_counter)++;
26 printf("Worker %d: iteration %d, counter = %d\n",
27 worker->worker_id, i, *worker->shared_counter);
28
29 ABT_mutex_unlock(worker->mutex);
30
31 /* Voluntarily yield to let other workers run */
32 printf(" Worker %d yielding...\n", worker->worker_id);
33 ABT_self_yield();
34 }
35
36 printf("Worker %d completed\n", worker->worker_id);
37}
38
39int main(int argc, char **argv)
40{
41 ABT_xstream xstream;
42 ABT_pool pool;
43 ABT_thread workers[NUM_WORKERS];
44 ABT_mutex mutex;
45 worker_arg_t worker_args[NUM_WORKERS];
46 int shared_counter = 0;
47
48 ABT_init(argc, argv);
49
50 printf("=== Yield-Based Cooperative Scheduling ===\n\n");
51
52 ABT_mutex_create(&mutex);
53 ABT_xstream_self(&xstream);
54 ABT_xstream_get_main_pools(xstream, 1, &pool);
55
56 /* Create workers */
57 for (int i = 0; i < NUM_WORKERS; i++) {
58 worker_args[i].worker_id = i;
59 worker_args[i].shared_counter = &shared_counter;
60 worker_args[i].mutex = mutex;
61
62 ABT_thread_create(pool, cooperative_worker, &worker_args[i],
63 ABT_THREAD_ATTR_NULL, &workers[i]);
64 }
65
66 /* Wait for all */
67 for (int i = 0; i < NUM_WORKERS; i++) {
68 ABT_thread_free(&workers[i]);
69 }
70
71 ABT_mutex_free(&mutex);
72
73 printf("\nYielding enabled fair cooperative scheduling\n");
74 printf("Final counter: %d\n", shared_counter);
75
76 ABT_finalize();
77 return 0;
78}
- Key Points:
ABT_self_yield()(line 31): Voluntarily gives up CPUScheduler selects next ULT to run
Yielding ULT remains runnable, will be rescheduled
Enables fair sharing without preemption
- Use Cases:
Long-running computations that should share CPU
Cooperative critical sections
Polling loops
Progress Polling Pattern
Critical pattern for Margo/Mochi network progress:
1/*
2 * Progress polling pattern: Non-blocking checks with yield
3 * Common in Margo/Mochi for network progress
4 */
5
6#include <stdio.h>
7#include <abt.h>
8
9#define NUM_REQUESTS 5
10
11typedef struct {
12 int request_id;
13 int completed;
14} async_request_t;
15
16typedef struct {
17 async_request_t *requests;
18 int num_requests;
19} poller_arg_t;
20
21void request_simulator(void *arg)
22{
23 async_request_t *requests = (async_request_t *)arg;
24
25 /* Simulate async completion of requests */
26 for (int i = 0; i < NUM_REQUESTS; i++) {
27 /* Simulate delay */
28 for (int j = 0; j < 100000 * (i + 1); j++);
29
30 requests[i].completed = 1;
31 printf(" Background: Request %d completed\n", requests[i].request_id);
32 ABT_self_yield();
33 }
34}
35
36void progress_poller(void *arg)
37{
38 poller_arg_t *poller = (poller_arg_t *)arg;
39 int completed_count = 0;
40
41 printf("Poller: Starting to poll for completions\n\n");
42
43 while (completed_count < poller->num_requests) {
44 /* Poll for completed requests (non-blocking) */
45 for (int i = 0; i < poller->num_requests; i++) {
46 if (poller->requests[i].completed &&
47 poller->requests[i].request_id >= 0) {
48 printf("Poller: Detected completion of request %d\n",
49 poller->requests[i].request_id);
50 poller->requests[i].request_id = -1; /* Mark as processed */
51 completed_count++;
52 }
53 }
54
55 /* Yield to let other work progress */
56 ABT_self_yield();
57 }
58
59 printf("\nPoller: All requests completed\n");
60}
61
62int main(int argc, char **argv)
63{
64 ABT_xstream xstream;
65 ABT_pool pool;
66 ABT_thread simulator;
67 ABT_thread poller;
68 async_request_t requests[NUM_REQUESTS];
69 poller_arg_t poller_arg;
70
71 ABT_init(argc, argv);
72
73 printf("=== Progress Polling Pattern ===\n");
74 printf("Simulates async I/O or network progress polling\n\n");
75
76 /* Initialize requests */
77 for (int i = 0; i < NUM_REQUESTS; i++) {
78 requests[i].request_id = i;
79 requests[i].completed = 0;
80 }
81
82 poller_arg.requests = requests;
83 poller_arg.num_requests = NUM_REQUESTS;
84
85 ABT_xstream_self(&xstream);
86 ABT_xstream_get_main_pools(xstream, 1, &pool);
87
88 /* Create poller thread */
89 ABT_thread_create(pool, progress_poller, &poller_arg,
90 ABT_THREAD_ATTR_NULL, &poller);
91
92 /* Create simulator thread */
93 ABT_thread_create(pool, request_simulator, requests,
94 ABT_THREAD_ATTR_NULL, &simulator);
95
96 /* Wait for both */
97 ABT_thread_free(&poller);
98 ABT_thread_free(&simulator);
99
100 printf("\nProgress polling with yield: common pattern in Margo\n");
101
102 ABT_finalize();
103 return 0;
104}
- Key Points:
Poll for completion in loop
Yield after each check
Non-blocking: doesn’t wait, just checks
Lets other ULTs (like background workers) make progress
- Example Usage:
/* Typical progress loop pattern */ while (!request_completed) { make_progress(...); /* Non-blocking progress */ ABT_self_yield(); /* Let other work run */ }
Other Self Operations
- Suspend and Resume:
/* ULT A */ ABT_self_suspend(); /* Suspends self, requires external resume */ /* ULT B or external thread */ ABT_thread_resume(thread_handle_of_A); /* Resume A */
Use for event-driven execution where external events trigger continuation.
- Exit:
ABT_self_exit(); /* Terminate self immediately */
Cleaner than returning from function for early termination.
- Yield To:
ABT_self_yield_to(target_thread); /* Yield and schedule target next */
Direct control flow transfer. Use sparingly; breaks scheduler abstraction.
Performance Implications
- Yielding Overhead:
ABT_self_yield()has context switch overheadDon’t yield in tight loops
Balance between fairness and overhead
- Progress Polling:
Good: Allows concurrent progress
Bad: Burns CPU if polling too frequently
Best practice: Yield after each poll
Common Pitfalls
- Infinite Yield Loop:
/* Bad: Yields forever with no progress */ while (!condition) { ABT_self_yield(); }
Ensure some ULT can make progress towards the condition!
- Forgetting to Yield in Polling:
/* Bad: Busy-wait, starves other ULTs */ while (!ready) { /* No yield - monopolizes CPU */ }
Always yield in polling loops.
- Suspending Without Resume Path:
ABT_self_suspend(); /* Who will resume us? */
Ensure there’s a clear path for resume, or ULT hangs forever.
API Reference
- Self Operations:
int ABT_self_yield()Voluntarily yield to scheduler. Returns when rescheduled.
int ABT_self_yield_to(ABT_thread thread)Yield and request specific ULT be scheduled next.
int ABT_self_suspend()Suspend self. Requires
ABT_thread_resume()from another ULT.int ABT_self_exit()Terminate self immediately.
int ABT_self_schedule(ABT_thread thread, ABT_pool pool)Schedule another ULT to a pool while running.
- Related Operations:
int ABT_thread_resume(ABT_thread thread)Resume a suspended ULT.