Uthread API

The uthread framework is a basic task scheduler that allows to run functions “in parallel” on a single CPU core. The scheduling is cooperative, not preemptive – meaning that context switches from one task to another task is voluntary, via a call to uthread_schedule(). This characteristic makes thread synchronization much easier, because a thread cannot be interrupted in the middle of a critical section (reading from or writing to shared state, for instance).

CONFIG_UTHREAD in lib/Kconfig enables the uthread framework. When disabled, the uthread_create() and uthread_schedule() functions may still be used so that code differences between uthreads enabled and disabled can be reduced to a minimum.

struct uthread

a thread object

Definition

struct uthread {
  void (*fn)(void *arg);
  void *arg;
  jmp_buf ctx;
  void *stack;
  bool done;
  unsigned int grp_id;
  struct list_head list;
};

Members

fn

thread entry point

arg

argument passed to the entry point when the thread is started

ctx

context to resume execution of this thread (via longjmp())

stack

initial stack pointer for the thread

done

true once fn has returned, false otherwise

grp_id

user-supplied identifier for this thread and possibly others. A thread can belong to zero or one group (not more), and a group may contain any number of threads.

list

link in the global scheduler list

enum uthread_mutex_state

internal state of a struct uthread_mutex

Constants

UTHREAD_MUTEX_UNLOCKED

mutex has no owner

UTHREAD_MUTEX_LOCKED

mutex has one owner

struct uthread_mutex

a mutex object

Definition

struct uthread_mutex {
  enum uthread_mutex_state state;
};

Members

state

the internal state of the mutex

int uthread_create(struct uthread *uthr, void (*fn)(void*), void *arg, size_t stack_sz, unsigned int grp_id)

Create a uthread object and make it ready for execution

Parameters

struct uthread *uthr

a pointer to a user-allocated uthread structure to store information about the new thread, or NULL to let the framework allocate and manage its own structure.

void (*fn)(void *)

the thread’s entry point

void *arg

argument passed to the thread’s entry point

size_t stack_sz

stack size for the new thread (in bytes). The stack is allocated on the heap.

unsigned int grp_id

an optional thread group ID that the new thread should belong to (zero for no group)

Description

Threads are automatically deleted when they return from their entry point.

bool uthread_schedule(void)

yield the CPU to the next runnable thread

Parameters

void

no arguments

Description

This function is called either by the main thread or any secondary thread (that is, any thread created via uthread_create()) to switch execution to the next runnable thread.

Return

true if a thread was scheduled, false if no runnable thread was found

unsigned int uthread_grp_new_id(void)

return a new ID for a thread group

Parameters

void

no arguments

Return

the new thread group ID

bool uthread_grp_done(unsigned int grp_id)

test if all threads in a group are done

Parameters

unsigned int grp_id

the ID of the thread group that should be considered

Return

false if the group contains at least one runnable thread (i.e., one thread which entry point has not returned yet), true otherwise

int uthread_mutex_lock(struct uthread_mutex *mutex)

lock a mutex

Parameters

struct uthread_mutex *mutex

pointer to the mutex to lock

Description

If the cwmutexlock is available (i.e., not owned by any other thread), then it is locked for use by the current thread. Otherwise the current thread blocks: it enters a wait loop by scheduling other threads until the mutex becomes unlocked.

Return

0 on success, in which case the lock is owned by the calling thread. != 0 otherwise (the lock is not owned by the calling thread).

int uthread_mutex_trylock(struct uthread_mutex *mutex)

lock a mutex if not currently locked

Parameters

struct uthread_mutex *mutex

pointer to the mutex to lock

Description

Similar to uthread_mutex_lock() except return immediately if the mutex is locked already.

Return

0 on success, in which case the lock is owned by the calling thread. EBUSY if the mutex is already locked by another thread. Any other non-zero value on error.

int uthread_mutex_unlock(struct uthread_mutex *mutex)

unlock a mutex

Parameters

struct uthread_mutex *mutex

pointer to the mutex to unlock

Description

The mutex is assumed to be owned by the calling thread on entry. On exit, it is unlocked.

Return

0 on success, != 0 on error

Example

Here is an example of how to use this API:

  1// SPDX-License-Identifier: GPL-2.0+
  2/*
  3 * Copyright 2025 Linaro Limited
  4 *
  5 * Unit test for uthread
  6 */
  7
  8#include <stdbool.h>
  9#include <test/lib.h>
 10#include <test/ut.h>
 11#include <uthread.h>
 12
 13static int count;
 14
 15/* A thread entry point */
 16static void worker(void *arg)
 17{
 18	int loops = (int)(unsigned long)arg;
 19	int i;
 20
 21	for (i = 0; i < loops; i++) {
 22		count++;
 23		uthread_schedule();
 24	}
 25}
 26
 27/*
 28 * uthread() - testing the uthread API
 29 *
 30 * This function creates two threads with the same entry point. The first one
 31 * receives 5 as an argument, the second one receives 10. The number indicates
 32 * the number of time the worker thread should loop on uthread_schedule()
 33 * before returning. The workers increment a global counter each time they loop.
 34 * As a result the main thread knows how many times it should call
 35 * uthread_schedule() to let the two threads proceed, and it also knows which
 36 * value the counter should have at any moment.
 37 */
 38static int uthread(struct unit_test_state *uts)
 39{
 40	int i;
 41	int id1, id2;
 42
 43	count = 0;
 44	id1 = uthread_grp_new_id();
 45	ut_assert(id1 != 0);
 46	id2 = uthread_grp_new_id();
 47	ut_assert(id2 != 0);
 48	ut_assert(id1 != id2);
 49	ut_assertok(uthread_create(NULL, worker, (void *)5, 0, id1));
 50	ut_assertok(uthread_create(NULL, worker, (void *)10, 0, 0));
 51	/*
 52	 * The first call is expected to schedule the first worker, which will
 53	 * schedule the second one, which will schedule back to the main thread
 54	 * (here). Therefore count should be 2.
 55	 */
 56	ut_assert(uthread_schedule());
 57	ut_asserteq(2, count);
 58	ut_assert(!uthread_grp_done(id1));
 59	/* Four more calls should bring the count to 10 */
 60	for (i = 0; i < 4; i++) {
 61		ut_assert(!uthread_grp_done(id1));
 62		ut_assert(uthread_schedule());
 63	}
 64	ut_asserteq(10, count);
 65	/* This one allows the first worker to exit */
 66	ut_assert(uthread_schedule());
 67	/* At this point there should be no runnable thread in group 'id1' */
 68	ut_assert(uthread_grp_done(id1));
 69	/* Five more calls for the second worker to finish incrementing  */
 70	for (i = 0; i < 5; i++)
 71		ut_assert(uthread_schedule());
 72	ut_asserteq(15, count);
 73	/* Plus one call to let the second worker return from its entry point */
 74	ut_assert(uthread_schedule());
 75	/* Now both tasks should be done, schedule should return false */
 76	ut_assert(!uthread_schedule());
 77
 78	return 0;
 79}
 80LIB_TEST(uthread, 0);
 81
 82struct mw_args {
 83	struct unit_test_state *uts;
 84	struct uthread_mutex *m;
 85	int flag;
 86};
 87
 88static int mutex_worker_ret;
 89
 90static int _mutex_worker(struct mw_args *args)
 91{
 92	struct unit_test_state *uts = args->uts;
 93
 94	ut_asserteq(-EBUSY, uthread_mutex_trylock(args->m));
 95	ut_assertok(uthread_mutex_lock(args->m));
 96	args->flag = 1;
 97	ut_assertok(uthread_mutex_unlock(args->m));
 98
 99	return 0;
100}
101
102static void mutex_worker(void *arg)
103{
104	mutex_worker_ret = _mutex_worker((struct mw_args *)arg);
105}
106
107/*
108 * thread_mutex() - testing uthread mutex operations
109 *
110 */
111static int uthread_mutex(struct unit_test_state *uts)
112{
113	struct uthread_mutex m = UTHREAD_MUTEX_INITIALIZER;
114	struct mw_args args = { .uts = uts, .m = &m, .flag = 0 };
115	int id;
116	int i;
117
118	id = uthread_grp_new_id();
119	ut_assert(id != 0);
120	/* Take the mutex */
121	ut_assertok(uthread_mutex_lock(&m));
122	/* Start a thread */
123	ut_assertok(uthread_create(NULL, mutex_worker, (void *)&args, 0,
124				   id));
125	/* Let the thread run for a bit */
126	for (i = 0; i < 100; i++)
127		ut_assert(uthread_schedule());
128	/* Thread should not have set the flag due to the mutex */
129	ut_asserteq(0, args.flag);
130	/* Release the mutex */
131	ut_assertok(uthread_mutex_unlock(&m));
132	/* Schedule the thread until it is done */
133	while (uthread_schedule())
134		;
135	/* Now the flag should be set */
136	ut_asserteq(1, args.flag);
137	/* And the mutex should be available */
138	ut_assertok(uthread_mutex_trylock(&m));
139	ut_assertok(uthread_mutex_unlock(&m));
140
141	/* Of course no error are expected from the thread routine */
142	ut_assertok(mutex_worker_ret);
143
144	return 0;
145}
146LIB_TEST(uthread_mutex, 0);