Threading in nanoFramework is the way to execute multiple tasks concurrently within the same program.
Until now, all our programs followed a linear flow: “do A, then do B, then wait a second, and start over”.
But the real world isn’t linear. What if you want to blink a status LED every 500ms to indicate the system is alive, but at the same time you want to listen to the serial port without missing a single character?
If you use Thread.Sleep(500) for the LED inside the only loop, that flow won’t be able to handle the serial port for half a second.
The solution is Multitasking (Multithreading). In this post, we’ll learn how to use the System.Threading.Thread class to split our program into several independent workers that cooperate with each other.
What is a thread?
Imagine your microcontroller is a kitchen.
- Single-Thread (What we did before): You have only one cook. They chop onions, then put water to boil, then wait staring at the pot… If they’re watching the pot, they can’t chop more onions.
- Multi-Thread: You hire several cooks. One watches the pot (LED), another chops onions (Sensors), and another attends to the waiter (Wi-Fi). Everything happens “at the same time”.
In nanoFramework, the CLR (the virtual machine) is responsible for distributing the processor’s time among the different threads. Even though there’s only one core (on most MCUs), the system switches between tasks so quickly that it gives the illusion of complete parallelism.
Creating the first thread
To use threads, we need the System.Threading namespace.
The structure is simple:
You define a void method that contains the code for the task.
You create an instance of Thread passing that method to it.
You call .Start() so it begins working.
using System;
using System.Diagnostics;
using System.Threading;
namespace MultitareaEjemplo
{
public class Program
{
public static void Main()
{
Debug.WriteLine("Main Thread Started (Main)");
// 1. Create the thread and tell it which method to execute
// 'TareaParpadeo' is the name of the function defined below
Thread hiloLed = new Thread(TareaParpadeo);
// 2. Optional: Give it a priority (Normal, AboveNormal, Highest...)
hiloLed.Priority = ThreadPriority.Normal;
// 3. Start!
hiloLed.Start();
// The Main Thread continues its own work
int counter = 0;
while (true)
{
Debug.WriteLine($"[Main] Working on complex logic... {counter++}");
// Pause Main for 2 seconds.
// Notice that the LED will keep blinking even while Main sleeps!
Thread.Sleep(2000);
}
}
// This method will run in parallel
private static void TareaParpadeo()
{
Debug.WriteLine("[LED Thread] Started");
// GPIO configuration would go here...
// GpioPin led = ...
while (true)
{
Debug.WriteLine("[LED Thread] Blink!");
// led.Toggle();
// IMPORTANT: This Sleep ONLY pauses this thread, not Main
Thread.Sleep(500);
}
}
}
}If you run this, you’ll see the messages from [Main] and [LED Thread] interleaved in the console. They are alive at the same time!
Passing data to a thread
Sometimes we want to start a thread with parameters. For example, a thread that controls a motor and needs to know the motor’s ID.
For this, we use a special delegate. The only limitation is that the method must accept a single parameter of type object.
public static void Main()
{
// Start two threads using the same function, but with different data
Thread motor1 = new Thread(ControlMotor);
motor1.Start(1); // Pass ID 1 as argument
Thread motor2 = new Thread(ControlMotor);
motor2.Start(2); // Pass ID 2 as argument
}
private static void ControlMotor(object idMotor)
{
// Cast the object to the actual type (int)
int id = (int)idMotor;
while(true)
{
Debug.WriteLine($"Controlling Motor {id}");
Thread.Sleep(1000);
}
}Race conditions
When two threads try to modify the same variable at the same time, we can get an incorrect result.
Imagine a global counter int total = 0.
- Thread A reads 0, adds 1, and prepares to write.
- Right before writing, the system pauses Thread A and lets Thread B run.
- Thread B reads 0 (because A hasn’t written yet), adds 1, and stores 1.
- Thread A resumes and stores 1.
Result: Two additions were performed, but the counter is 1 instead of 2. This is a Race Condition and produces bugs that are extremely difficult to detect.
Protecting a section with lock
To avoid this, we use the lock keyword.
The lock guarantees that only one thread can enter that block of code at a time. If another one arrives while it’s busy, it waits at the door.
We need a “token” object (usually a private object) to manage the lock.
public class ContadorSeguro
{
private int _count = 0;
// Lock object (token)
private readonly object _lock = new object();
public void Increment()
{
// No one else can enter here while I'm inside
lock (_lock)
{
_count++;
Debug.WriteLine($"Current count: {_count}");
// Inside here we are safe. This is an "exclusive access" zone.
}
}
}Use lock only for quick operations. If you put a Thread.Sleep(5000) inside a lock, you’ll block all other threads that need that resource, defeating the purpose of multitasking.
Thread priorities
Not all threads are equal.
- Maintaining the Wi-Fi connection is critical (High Priority).
- Reading a temperature sensor every 10 minutes is secondary (Lowest Priority).
nanoFramework respects these priorities. Use them wisely, because a high-priority thread that never yields can prevent others from executing.
Thread critical = new Thread(WifiTask);
critical.Priority = ThreadPriority.Highest;
Thread background = new Thread(LogTask);
background.Priority = ThreadPriority.Lowest;Best practices and limitations
Threads facilitate concurrency, but they also consume resources:
- RAM: Each thread needs its own memory stack (Stack). Creating a thread consumes RAM immediately. On an ESP32 you have a decent amount, but don’t create 100 threads. Try to stay under 5-10 threads in complex applications.
- Terminating Threads: Threads die when their method finishes (reaches the end of the function). If you have a
while(true), the thread will live forever (which is usually desired for background tasks). - Debugging: Debugging threads is difficult because the error might depend on “when” each one is paused. Use
Debug.WriteLineincluding the thread’s name or ID to trace what’s happening.