diff --git a/core/Makefile b/core/Makefile index 2ce4fb9f02..15a1ce2726 100644 --- a/core/Makefile +++ b/core/Makefile @@ -1,5 +1,5 @@ # exclude submodule sources from *.c wildcard source selection -SRC := $(filter-out mbox.c msg.c msg_bus.c thread.c thread_flags.c,$(wildcard *.c)) +SRC := $(filter-out mbox.c msg.c msg_bus.c thread.c thread_flags.c thread_flags_group.c,$(wildcard *.c)) # enable submodules SUBMODULES := 1 diff --git a/core/include/thread_flags_group.h b/core/include/thread_flags_group.h new file mode 100644 index 0000000000..c2e7697fa5 --- /dev/null +++ b/core/include/thread_flags_group.h @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2025 ML!PA Consulting GmbH + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +#pragma once + +/** + * @defgroup core_thread_flags_group Thread Flags Group + * @ingroup core + * @brief Waiter groups for thread flags + * + * + * This API is optional and must be enabled by adding "core_thread_flags_group" + * to USEMODULE. + * + * A thread flags group allows setting thread flags for an arbitrary number of + * threads (called waiters) at the same time. The waiters can join and leave + * the group at any time. An additional advantage is that the signaler (the + * "flags setter") is de-coupled from the list of waiters, i.e. it does not + * need to know which specific thread must be signaled. + * + * Example (waiter): + * + * static thread_flags_group_t group = THREAD_FLAGS_GROUP_INIT; + * + * ... + * + * thread_flags_group_join(&group); + * + * while (!some_condition_is_met()) { + * thread_flags_wait_any(SOME_FLAG); + * } + * + * thread_flags_group_leave(&group); + * + * Example (signaler): + * + * fulfill_some_condition(); + * thread_flags_group_set(&group, SOME_FLAG); + * + * @{ + * + * @file + * @brief Thread Flags Group API + * + * @author Mihai Renea + */ + +#include + +#include "atomic_utils.h" +#include "thread.h" +#include "thread_flags.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/* UINT_WIDTH is only provided starting with -std=c23 or newer. Until RIOT + * requires C23 as C version, we need provide it by hand when missing. */ +#ifndef UINT_WIDTH +/** + * @brief Number of bits in unsigned int + */ +# define UINT_WIDTH (sizeof(unsigned) * 8) +#endif + +/** + * @brief Thread flags group. + */ +typedef struct { + /** + * Members bitfield. + */ + unsigned members[(MAXTHREADS / UINT_WIDTH) + !!(MAXTHREADS % UINT_WIDTH)]; +} thread_flags_group_t; + +/** + * @brief Initialize a thread flags group. + */ +#define THREAD_FLAGS_GROUP_INIT { .members = { 0 } } + +/** + * @brief Join a thread flags group. + * + * After joining the group, the thread may call any thread_flags_wait_*() + * method as usual. The thread will remain in the group until @ref + * thread_flags_group_leave() is called. + * + * @param[out] group The thread flags group to join. + */ +static inline void thread_flags_group_join(thread_flags_group_t *group) +{ + kernel_pid_t pid = thread_getpid(); + /* this also optimizes away the arithmetic below if MAXTHREADS <= UINT_WIDTH */ + assume(pid < MAXTHREADS); + atomic_set_bit_unsigned(atomic_bit_unsigned(&group->members[pid / UINT_WIDTH], + pid % UINT_WIDTH)); +} + +/** + * @brief Leave a thread flags group. + * + * After leaving the group, the thread will no longer be signaled by @ref + * thread_flags_group_set(). + * + * @param[out] group The thread flags group to leave. + */ +static inline void thread_flags_group_leave(thread_flags_group_t *group) +{ + kernel_pid_t pid = thread_getpid(); + /* this also optimizes away the arithmetic below if MAXTHREADS <= UINT_WIDTH */ + assume(pid < MAXTHREADS); + atomic_clear_bit_unsigned(atomic_bit_unsigned(&group->members[pid / UINT_WIDTH], + pid % UINT_WIDTH)); +} + +/** + * @brief Set thread flags for all threads in a group. + * + * @param[in] group The thread flags group to set flags to. + * @param[in] mask The flags to set. + */ +void thread_flags_group_set(thread_flags_group_t *group, thread_flags_t mask); + +#ifdef __cplusplus +} +#endif +/** @} */ diff --git a/core/thread_flags_group.c b/core/thread_flags_group.c new file mode 100644 index 0000000000..2bad93350c --- /dev/null +++ b/core/thread_flags_group.c @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2025 ML!PA Consulting GmbH + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @ingroup core + * @{ + * + * @file + * @brief thread flags group implementation + * + * @author Mihai Renea + * + * @} + */ + +#include "bitarithm.h" +#include "irq.h" +#include "thread.h" +#include "thread_flags_group.h" + +void thread_flags_group_set(thread_flags_group_t *group, thread_flags_t mask) +{ + /* Interrupts must be disabled because the threads are not ordered by + * priority. */ + unsigned irq_state = irq_disable(); + + for (kernel_pid_t i = 0; i < (kernel_pid_t)ARRAY_SIZE(group->members); i++) { + unsigned pid_block = group->members[i]; + kernel_pid_t const pid_base = i * UINT_WIDTH; + uint8_t pid_offs = 0; + + while (pid_block) { + pid_block = bitarithm_test_and_clear(pid_block, &pid_offs); + thread_flags_set(thread_get(pid_base + pid_offs), mask); + } + } + + irq_restore(irq_state); +} diff --git a/tests/core/thread_flags_group/Makefile b/tests/core/thread_flags_group/Makefile new file mode 100644 index 0000000000..9fb500f93b --- /dev/null +++ b/tests/core/thread_flags_group/Makefile @@ -0,0 +1,12 @@ +include ../Makefile.core_common + +USEMODULE += core_thread_flags +USEMODULE += core_thread_flags_group + +ifneq (,$(filter native32 native64,$(BOARD))) + # test non-trivial membership array on native + CFLAGS += -DMAXTHREADS=64 + CFLAGS += -DWAITER_THREADS_CNT=40 +endif + +include $(RIOTBASE)/Makefile.include diff --git a/tests/core/thread_flags_group/Makefile.ci b/tests/core/thread_flags_group/Makefile.ci new file mode 100644 index 0000000000..b9ff275375 --- /dev/null +++ b/tests/core/thread_flags_group/Makefile.ci @@ -0,0 +1,3 @@ +BOARD_INSUFFICIENT_MEMORY := \ + nucleo-l011k4 \ + # diff --git a/tests/core/thread_flags_group/main.c b/tests/core/thread_flags_group/main.c new file mode 100644 index 0000000000..5e8fe491af --- /dev/null +++ b/tests/core/thread_flags_group/main.c @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2025 ML!PA Consulting GmbH + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @ingroup tests + * @{ + * + * @file + * @brief thread flags group test application + * + * @author Mihai Renea + * + * @} + */ + +#include + +#include "atomic_utils.h" +#include "test_utils/expect.h" +#include "thread.h" +#include "thread_flags_group.h" + +#ifndef WAITER_THREADS_CNT +# define WAITER_THREADS_CNT 3 +#endif + +#ifdef CPU_NATIVE +# define WAITER_STACKSIZE THREAD_STACKSIZE_MAIN +#else +# define WAITER_STACKSIZE THREAD_STACKSIZE_SMALL +#endif + +static char stacks[WAITER_THREADS_CNT][WAITER_STACKSIZE]; + +#define GOOD_FLAG 0x2 +#define BAD_FLAG 0x4 + +static uint8_t woken_up = 0; +static uint8_t last_prio = 0; +static thread_flags_group_t group = THREAD_FLAGS_GROUP_INIT; + +static void _print_waiting(char const *what_who, kernel_pid_t pid) +{ +#ifdef CPU_NATIVE + printf("%s %d\n", what_who, pid); +#else + puts(what_who); + (void)pid; +#endif +} + +static void *forever_waiter(void *arg) +{ + _print_waiting("waiting forever-waiter", (kernel_pid_t)(uintptr_t)arg); + thread_flags_wait_any(GOOD_FLAG | BAD_FLAG); + expect(false); +} + +static void *waiter(void *arg) +{ + thread_flags_group_join(&group); + + _print_waiting("waiting waiter", (kernel_pid_t)(uintptr_t)arg); + thread_flags_wait_any(GOOD_FLAG); + + _print_waiting("woken up waiter", (kernel_pid_t)(uintptr_t)arg); + + expect(atomic_load_u8(&last_prio) <= thread_get_active()->priority); + + atomic_store_u8(&last_prio, thread_get_active()->priority); + atomic_fetch_add_u8(&woken_up, 1); + + return NULL; +} + +int main(void) +{ + puts("START"); + unsigned waiters_cnt = 0; + for (unsigned i = 0; i < WAITER_THREADS_CNT; i++) { + int prio = (int)THREAD_PRIORITY_MAIN - i - 1; + if (prio < 0) { + prio = 0; + } + + thread_task_func_t handler = i % 3 ? (waiters_cnt++, waiter) : forever_waiter; + int res = thread_create(stacks[i], sizeof(stacks[0]), + prio, THREAD_CREATE_STACKTEST, handler, + (void *)(uintptr_t)i, "waiter"); + expect(res >= 0); + } + + /* this shouldn't wake up */ + thread_flags_group_set(&group, BAD_FLAG); + expect(atomic_load_u8(&woken_up) == 0); + + puts("waking up!"); + + thread_flags_group_set(&group, GOOD_FLAG); + + /* waiters have higher prio, so they must have finished */ + expect(atomic_load_u8(&woken_up) == waiters_cnt); + + puts("SUCCESS"); + + return 0; +} diff --git a/tests/core/thread_flags_group/tests/01-run.py b/tests/core/thread_flags_group/tests/01-run.py new file mode 100755 index 0000000000..b8223c2825 --- /dev/null +++ b/tests/core/thread_flags_group/tests/01-run.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python3 + +import sys +from testrunner import run + + +def testfunc(child): + child.expect("START") + child.expect("SUCCESS") + + +if __name__ == "__main__": + sys.exit(run(testfunc))