
ESP32 Multi - threading
Are you stuck wondering how to make your ESP32 juggle more than one task at a time? If you’re an Arduino beginner, this is probably your problem: the ESP32 has two powerful cores, but most sketches only ever use one. By learning how to run multi-threaded functions the easy way—without diving into tricky ESP-IDF calls—you’ll unlock the ability to run multiple functions at once. That means smoother projects, faster response times, and more freedom to experiment. In this post, I’ll show you exactly how to get started.
Using the ESPx library
This project makes use of the espx
library.
The espx
library for Arduino defines a set of abstractions that make the ESP32 features easily accessible with
few lines of code. Some of the features are:
- WiFi connection (
wifix
) - HTTP client (
httpx
) - JSON generator (
jsonx
) - Camera manipulation (
camx
)
Installation
Install the latest version of espx
from the Arduino Library Manager.
The code examples on this page have been tested with version 1.0.2
: if you get weird
errors about missing variables or method, double check that you have the correct version.
If the error persists, please open an issue on GitHub.
The ugly way to create a thread
As usual, the native ESP-IDF API is brutal in terms of usability.
// create a task using the native ESP-IDF
xTaskCreatePinnedToCore(
TaskCode, /* Function to implement the task */
"Task", /* Name of the task */
10000, /* Stack size in words */
NULL, /* Task input parameter */
0, /* Priority of the task */
&Task, /* Task handle. */
0); /* Core where the task should run */
// create a task using the native ESP-IDF
xTaskCreatePinnedToCore(
TaskCode, /* Function to implement the task */
"Task", /* Name of the task */
10000, /* Stack size in words */
NULL, /* Task input parameter */
0, /* Priority of the task */
&Task, /* Task handle. */
0); /* Core where the task should run */
If it wasn't for the line by line comments, you would be looking at this single line:
xTaskCreatePinnedToCore(TaskCode, "Task", 10000, NULL, 0, &Task, 0);
xTaskCreatePinnedToCore(TaskCode, "Task", 10000, NULL, 0, &Task, 0);
Good luck memorizing what each parameter does.
That's where the espx
library comes into play. It gives you a fluent interface to do the exact same thing, but 100x more readable and understandable to you and others who will read your code. Also, it leverages the IDE autocomplete feature so you don't even need to memorize the exact names: just start typing and let the magic happen.
The ESPx way to create a thread
threadx([](void *) {
while (true) {
Serial.println("Inside anonymous thread");
delay(1000);
}
});
threadx([](void *) {
while (true) {
Serial.println("Inside anonymous thread");
delay(1000);
}
});
This is the bare minimum syntax to endlessly run a function on its own thread. Here we're using a C++ lambda function, which allows us to define a function right where it is needed. If you prefer the C-style syntax, you can define the function beforehand also.
void taskCode(void*) {
while (true) {
Serial.println("Inside anonymous thread");
delay(1000);
}
}
void setup() {
threadx(taskCode);
}
void taskCode(void*) {
while (true) {
Serial.println("Inside anonymous thread");
delay(1000);
}
}
void setup() {
threadx(taskCode);
}
Now that's a lot more readable. But it is not a 1:1 mapping to the ESP-IDF syntax: it is missing all the configurations.
How to configure the thread
The following code lists all available configurations you can set when creating a thread. Consider that they are all optional (as seen in the shortest example above). Also, order doesn't matter, so you can re-arrange them as you prefer.
// global variables, outside setup()
String name = "John Doe";
TaskHandle_t handle;
// create fully configured thread
// put this inside setup()
threadx(
[](void *userdata) {
// convert userdata back to String
String name = *((String*) userdata);
while (true) {
Serial.print("Inside fully configured thread. My name is: ");
Serial.println(name);
delay(1000);
}
},
// give a name to the thread
threadx.Name("HelloWorldThread"),
// choose which core will run on (0 or 1)
threadx.Core(1),
// how much RAM to give the thread
threadx.Stack("5 kb"),
// you can choose between
// - NotImportant (priority = 0)
// - Important (priority = MAX / 2)
// - Critical (priority = MAX)
threadx.Important(),
// Userdata is passed as first argument to the task function
// (must be a pointer)
threadx.Userdata(&name),
// bind thread to handler (to e.g. later abort it)
threadx.Handler(handle)
);
// global variables, outside setup()
String name = "John Doe";
TaskHandle_t handle;
// create fully configured thread
// put this inside setup()
threadx(
[](void *userdata) {
// convert userdata back to String
String name = *((String*) userdata);
while (true) {
Serial.print("Inside fully configured thread. My name is: ");
Serial.println(name);
delay(1000);
}
},
// give a name to the thread
threadx.Name("HelloWorldThread"),
// choose which core will run on (0 or 1)
threadx.Core(1),
// how much RAM to give the thread
threadx.Stack("5 kb"),
// you can choose between
// - NotImportant (priority = 0)
// - Important (priority = MAX / 2)
// - Critical (priority = MAX)
threadx.Important(),
// Userdata is passed as first argument to the task function
// (must be a pointer)
threadx.Userdata(&name),
// bind thread to handler (to e.g. later abort it)
threadx.Handler(handle)
);
Here's the short version of the above example without comments, to give you a real glimpse on how it will look like in your project.
threadx(
[](void *userdata) {
// ...your code here...
},
threadx.Name("HelloWorldThread"),
threadx.Core(1),
threadx.Stack("5 kb"),
threadx.Important(),
threadx.Userdata(&name),
threadx.Handler(handle)
);
threadx(
[](void *userdata) {
// ...your code here...
},
threadx.Name("HelloWorldThread"),
threadx.Core(1),
threadx.Stack("5 kb"),
threadx.Important(),
threadx.Userdata(&name),
threadx.Handler(handle)
);
You can still see that you have a few options to (optionally) configure, but they're all scoped under the threadx
object. This means that, by typing threadx.
in the IDE, you will see the list of available autocompletions: you'll only have to pick the one that you need!
A little aside
If you're not a C++ expert, you may be wandering: "is threadx
a function or an object?". Well, it is a callable object: an object (with properties and methods) that can also be called as a function. This allows the espx
library to be very adaptable without polluting the global name space with many different variables.
Complete example
/**
* Create background threads
*/
#include <espx.h>
#include <espx/threadx.h>
String name = "John Doe";
TaskHandle_t handle;
void setup() {
delay(2000);
Serial.begin(115200);
Serial.setTimeout(4000);
Serial.println("Threadx example: create threads");
delay(2000);
// create an "anonymous" thread
// with default config
threadx([](void *) {
while (true) {
Serial.println("Inside anonymous thread");
delay(1000);
}
});
// you can also create a "detached" function that runs once
// in background and exits (no while loop needed!)
threadx([](void *) {
Serial.println("Run and exit!");
});
// create fully configured thread
threadx(
[](void *userdata) {
// convert userdata back to String
String name = *((String*) userdata);
while (true) {
Serial.print("Inside fully configured thread. My name is: ");
Serial.println(name);
delay(2000);
}
},
// give a name to the thread
threadx.Name("HelloWorldThread"),
// choose which core will run on (0 or 1)
threadx.Core(1),
// how much RAM to give the thread
threadx.Stack("5 kb"),
// you can choose between
// - NotImportant (priority = 0)
// - Important (priority = MAX / 2)
// - Critical (priority = MAX)
threadx.Important(),
// Userdata is passed as first argument to the task function
// (must be a pointer)
threadx.Userdata(&name),
// bind thread to handler (see loop)
threadx.Handler(handle)
);
}
void loop() {
// cancel thread on user input
String answer = promptString("Do you want to abort HelloWorldThread? (yes|no)");
if (answer.startsWith("yes")) {
Serial.println("Aborting thread...");
vTaskDelete(handle);
}
}
/**
* Create background threads
*/
#include <espx.h>
#include <espx/threadx.h>
String name = "John Doe";
TaskHandle_t handle;
void setup() {
delay(2000);
Serial.begin(115200);
Serial.setTimeout(4000);
Serial.println("Threadx example: create threads");
delay(2000);
// create an "anonymous" thread
// with default config
threadx([](void *) {
while (true) {
Serial.println("Inside anonymous thread");
delay(1000);
}
});
// you can also create a "detached" function that runs once
// in background and exits (no while loop needed!)
threadx([](void *) {
Serial.println("Run and exit!");
});
// create fully configured thread
threadx(
[](void *userdata) {
// convert userdata back to String
String name = *((String*) userdata);
while (true) {
Serial.print("Inside fully configured thread. My name is: ");
Serial.println(name);
delay(2000);
}
},
// give a name to the thread
threadx.Name("HelloWorldThread"),
// choose which core will run on (0 or 1)
threadx.Core(1),
// how much RAM to give the thread
threadx.Stack("5 kb"),
// you can choose between
// - NotImportant (priority = 0)
// - Important (priority = MAX / 2)
// - Critical (priority = MAX)
threadx.Important(),
// Userdata is passed as first argument to the task function
// (must be a pointer)
threadx.Userdata(&name),
// bind thread to handler (see loop)
threadx.Handler(handle)
);
}
void loop() {
// cancel thread on user input
String answer = promptString("Do you want to abort HelloWorldThread? (yes|no)");
if (answer.startsWith("yes")) {
Serial.println("Aborting thread...");
vTaskDelete(handle);
}
}