Repository navigation
Threads
MicroCS is single-threaded by default: one VM, a polled job scheduler, and nothing that
needs an RTOS. If you don't choose an OS, nothing changes: no thread code is compiled,
jobs.cfg behaves exactly as before, and Thread.Os is "None".
Once you pick an OS, C# scripts and functions can run on their own OS tasks, and you can pin work to a core. That includes the second core of the ESP32 / ESP32-S3 and the RP2040 (FreeRTOS SMP), and the CPUs of SMP Zephyr boards.
| OS | How to turn it on | Cores |
|---|---|---|
| ESP-IDF (FreeRTOS) |
idf.py menuconfig → MicroCS → Real threads on FreeRTOS tasks (CONFIG_MICROCS_THREADS=y) |
2 on ESP32 / S3, 1 on C2 / C3 / C6 |
| FreeRTOS (any port) | CMake -DMICROCS_OS=freertos, make OS=freertos FREERTOS_PATH=…, or #define MCS_OS MCS_OS_FREERTOS
|
SMP builds: configNUMBER_OF_CORES
|
| RP2040 (pico-sdk) |
ports/rp2/example: -DMICROCS_OS=freertos -DFREERTOS_KERNEL_PATH=<FreeRTOS-Kernel>
|
2 (FreeRTOS SMP) |
| Zephyr |
CONFIG_MICROCS_THREADS=y (see ports/zephyr/example/overlay-threads.conf) |
SMP: CONFIG_MP_MAX_NUM_CPUS
|
| Linux / macOS host | CMake -DMICROCS_OS=posix, make OS=posix
|
all |
| detect |
MCS_OS_AUTO: Zephyr in a Zephyr build, FreeRTOS under ESP-IDF or when FreeRTOS.h + FreeRTOSConfig.h are on the include path, else none |
// call a function in another script on its own thread (any core) and get the result
var w = Thread.Run("/math.cs", "Fib", 30);
Console.WriteLine(w.Result); // waits; also w.Join([ms]), w.IsAlive, w.Stop()
// the same, pinned to core 1 (the REPL keeps core 0)
var c1 = Thread.RunOn(1, "/dsp.cs", "Filter", samples);
// a whole script on a thread, optionally on a core with its own heap size (KB)
var logger = Thread.Start("/logger.cs", 1, 24);
// OS-timed periodic task: Tick() every 10 ms on core 1, no drift
var loop = Thread.Every(10, "/control.cs", "Tick", 1);
// named channels copy values between threads
var ch = new Channel("samples", 16); // the same name opens the same channel anywhere
ch.Send(42); // blocks while full; Send(v, ms), TrySend(v)
var v = ch.Receive(500); // null after 500 ms; Receive(), TryReceive()
Console.WriteLine($"{Thread.Os} cores={Thread.Cores} on core {Thread.CurrentCore}");| Member | |
|---|---|
Thread.Start(path [, core [, heapKB]]) |
run a script; returns a Worker
|
Thread.Run(path, function, args…) |
load the script, call function(args…); Result is its return value |
Thread.RunOn(core, path, function, args…) |
same, pinned to core
|
Thread.Every(ms, path [, function] [, core]) |
run the script (or call function) every ms, OS-timed |
Thread.StopAll() |
ask every other thread to stop |
Thread.Cores, Thread.CurrentCore, Thread.Os, Thread.Count, Thread.Id
|
(Id 0 = main VM) |
Worker.Join([ms]) Stop() IsAlive State Result Error Runs Id Core
|
State: starting, running, done, failed, stopped |
new Channel(name [, capacity]) Send TrySend Receive TryReceive Count Capacity Name
|
capacity 1..256, default 8 |
Thread.Sleep, Thread.Cores (1), Thread.CurrentCore (0) and Thread.Os ("None")
also exist without an OS, so one script can check what it runs on.
-
Its own VM and heap. VMs are never shared, so threads on different cores run truly in
parallel without a global lock. Globals are not shared: pass data as arguments, results and
through
Channels. Values are copied:null,bool,int,char,float/double,string, and arrays /Lists of them (nested up to 4 levels,MCS_CHANNEL_MSG_MAXbytes). Objects and dictionaries are rejected withArgumentException. - Shared filesystem. Every filesystem call is locked, so threads can read scripts and write logs at the same time as the REPL.
-
Shared console. Each thread prints whole lines. Unhandled exceptions are printed with a
[thread N]prefix and are also stored inWorker.Error. ReadingResultrethrows them asInvalidOperationException. -
Peripherals, not callbacks. Threads can use
GPIO,I2C,SPI, … Interrupt callbacks (GPIO.OnChange,Timer, …) stay with the main VM. Setting one in a thread raisesInvalidOperationException. To get events to a thread, forward them with aChannel. -
Stopping.
Stop()is cooperative: the thread notices it between statements, and also while it sleeps or waits on a channel. -
Priority. Threads start at the priority of the thread that starts them, plus
MCS_THREAD_PRIORITY(0) orprio=injobs.cfg. A thread busy-looping at that priority starves lower-priority tasks on its core. Give the OS a moment withThread.Sleep(…). The ESP32 example turns off the idle-task watchdog on core 1 for this reason.
When an OS is chosen, file jobs (every, after, at) run on their own OS thread, timed by
the OS (MCS_SCHED_THREADS, on by default with threads; set it to 0 to make thread opt-in).
startup jobs and jobs marked main stay on the main VM. thread, core=, prio=, stack=
and heap= ask for a thread explicitly:
every 10ms /control.cs core=1 heap=24k
every 1s /log.cs stack=8k prio=-1
every 50ms /ui.cs main # polled on the main VM, as without an OS
startup /main.cs # always on the main VM
If no thread slot or heap is free, the job falls back to the main VM. Without an OS these
options are ignored and the job is polled as before. jobs / .jobs
in the shell show thread core=N worker=ID for running thread jobs.
#include "mcs_threads.h"
mcs_thread_spec_t s = MCS_THREAD_SPEC_DEFAULTS;
s.path = "/worker.cs"; s.core = 1; s.period_ms = 100; // or s.code/s.code_len, s.function
int id = mcs_thread_start(&s); // > 0, or -1 full, -2 memory, -3 core ...
mcs_thread_join(id, 1000); mcs_thread_stop(id); mcs_thread_release(id);mcs_runtime (the REPL firmware) sets threads up by itself. The thread heaps come from
cfg.heap (behind a lock), or from the OS heap when cfg.heap is NULL. cfg.thread_setup
registers your own C# bindings in every thread VM. cfg.thread_heap and cfg.thread_stack
set the defaults. Without the runtime, call mcs_threads_init() and then
mcs_threads_open_lib(vm) (see ports/unix/main.c).
| Option | Default | |
|---|---|---|
MCS_OS |
MCS_OS_NONE |
MCS_OS_FREERTOS, MCS_OS_ZEPHYR, MCS_OS_POSIX, MCS_OS_AUTO
|
MCS_THREADS_MAX |
8 | threads running at once |
MCS_THREAD_HEAP |
32 KB (256 KB with POSIX threads, 64 KB on other 64-bit builds) | C# heap per thread (Thread.Start heapKB, heap=) |
MCS_THREAD_STACK |
8 KB (12 KB on ESP32) | OS stack per thread (stack=) |
MCS_THREAD_SLOTS / MCS_THREAD_FRAMES
|
256 / 48 | VM stack of a thread |
MCS_THREAD_PRIORITY |
0 | relative to the starting thread |
MCS_SCHED_THREADS |
MCS_ENABLE_THREADS |
jobs.cfg file jobs on threads by default (main opts out) |
MCS_CHANNELS_MAX / MCS_CHANNEL_MSG_MAX
|
8 / 1024 |
ESP-IDF and Zephyr bring their own kernel. For plain FreeRTOS, MicroCS looks in these places, in order:
-
CMake: an existing
freertos_kernel(FreeRTOS-Kernel's CMake),FreeRTOS-Kernel(pico-sdk import), orfreertostarget, or a CubeMXstm32cubemxtarget with FreeRTOS inMiddlewares/. -
MICROCS_FREERTOS_PATH(or theFREERTOS_KERNEL_PATH/FREERTOS_PATHenvironment variables), plusMICROCS_FREERTOS_PORT(e.g.portable/GCC/ARM_CM4F) andMICROCS_FREERTOS_CONFIG_DIR(the folder with yourFreeRTOSConfig.h, searched in the project when not set). - Common folders in the project:
FreeRTOS-Kernel/,third_party/FreeRTOS-Kernel/,lib/…,Middlewares/Third_Party/FreeRTOS/Source/.
If none of these fits, the configure step stops and says which variable to set. The make
build stops the same way (make OS=freertos needs FREERTOS_PATH, FREERTOS_PORT,
FREERTOS_CONFIG_DIR). In an IDE, a missing FreeRTOS.h stops the compile with an
#error that names the include paths to add. FreeRTOSConfig.h needs configUSE_MUTEXES 1
and configUSE_COUNTING_SEMAPHORES 1. SMP builds also need configUSE_CORE_AFFINITY 1 for
RunOn.
Your application starts the scheduler. Run MicroCS (mcs_runtime_run, or your own VM)
from a task, then call vTaskStartScheduler(). ports/rp2/example/main.c shows how.
CONFIG_MICROCS_THREADS=y selects CONFIG_DYNAMIC_THREAD and
CONFIG_DYNAMIC_THREAD_ALLOC. On SMP boards it also implies CONFIG_SCHED_CPU_MASK, which
RunOn needs. Thread stacks come from the system heap, so CONFIG_HEAP_MEM_POOL_SIZE must
hold MCS_THREADS_MAX × MCS_THREAD_STACK. The heap is the part to size for your board:
west build -b <board> ports/zephyr/example -- -DEXTRA_CONF_FILE=overlay-threads.conf
make threads-test (part of make test) runs the C# suite and the runtime test on POSIX
threads. make tsan-test runs them under ThreadSanitizer. make freertos-test runs the
runtime test as a task of the FreeRTOS POSIX simulator port, and checks the error message
for missing headers.
Generated from the MicroCS repository - edit the files there, not in the wiki.
Start here
Step-by-step guides
- Guides
- Guide PC Simulator
- Guide ESP32
- Guide ESP32 C3 OLED
- Guide Raspberry Pi Pico
- Guide STM32
- Guide Arduino
- Guide Zephyr
- Guide Peripheral Settings
- Guide DMA
Hardware
- Hardware API
- Peripheral Configuration
- DMA
- Device Drivers
- Wi Fi and Networking
- U8g2 Displays
- Filesystem
- Threads
- Scheduler
Ports
- Porting and Build Systems
- Port ESP32
- Port RP2040 RP2350
- Port STM32
- Port STM32 Firmware
- Port Zephyr
- Port Arduino
C# and the runtime
Embedding and tuning
Internals
Project