Módulo 1.6

Multithreading en Zephyr

Ejecución concurrente con threads del RTOS

Santi Scagliusi, PhD

Comenzar arrow_downward
account_tree
Threads
Hilos de ejecución
schedule
Scheduler
Planificador de tareas
low_priority
Prioridades
Cooperativo y preemptivo
work
Workqueues
Colas de trabajo

Bare-metal vs RTOS

Comprender la diferencia entre programación secuencial y multitarea.

loop

Bare-metal

main.c
int main(void) {
init_hardware();
while (1) {
read_sensors();
process_data();
update_display();
check_buttons();
}
}
  • remove Tareas ejecutadas secuencialmente
  • remove Una tarea lenta bloquea todas las demás
  • remove Difícil gestionar tiempos de respuesta
account_tree

RTOS (Zephyr)

main.c
// Thread 1: Sensores
void sensor_thread() { ... }
// Thread 2: Display
void display_thread() { ... }
// Thread 3: Botones
void button_thread() { ... }
  • check_circle Cada thread tiene su propio contexto
  • check_circle El scheduler alterna entre threads
  • check_circle Mejor tiempo de respuesta a eventos

Contexto de un Thread

memory
Registros

Estado de la CPU guardado al cambiar de thread

layers
Stack propio

Memoria separada para variables locales

low_priority
Prioridad

Determina cuando puede ejecutarse

timer
Coste del cambio de contexto

Cada cambio de thread requiere guardar/restaurar registros y cambiar el stack pointer. En un Cortex-M4 @ 64 MHz esto toma aproximadamente ~2-5 µs.

~30 ciclos
Guardar registros
~30 ciclos
Restaurar registros
~100-200 ciclos
Scheduler + pipeline

Estados de un Thread

Un thread puede estar en uno de tres estados en cualquier momento.

Diagrama de estados

preemptado scheduler k_msleep() timeout/signal
play_arrow Running
hourglass_empty Runnable
pause Non-runnable
Running

El thread está ejecutándose actualmente en la CPU. Solo un thread puede estar en este estado a la vez (en sistemas single-core).

Runnable

El thread está listo para ejecutarse, pero espera a que el scheduler le asigne la CPU. Todos los threads listos están en una cola de prioridad.

Non-runnable

El thread está bloqueado esperando algo: un timeout (k_msleep), un semáforo, un mensaje, etc. No consume tiempo de CPU.

lightbulb
Nota: La transición de Non-runnable a Running siempre pasa por Runnable. El scheduler decide qué thread de la cola ejecutar.

Maquina de Estados Interactiva

Haz clic en cada estado para ver sus transiciones validas y descripcion. Usa el modo simulacion para ver un escenario en vivo.

Scheduler selecciona Preemptado / k_yield() k_sleep() / k_sem_take() Evento / timeout k_thread_suspend() k_thread_resume()
play_arrow RUNNING
hourglass_empty READY
schedule WAITING
pause_circle SUSPENDED
touch_app Selecciona un estado
Haz clic en cualquiera de los cuatro estados del diagrama para ver su descripcion y las transiciones disponibles.

movie Simulacion

Escenario: Thread A ejecuta, duerme con k_sleep(), el scheduler asigna Thread B. Un semaforo despierta a Thread A.

La simulacion aparecera aqui...

Tipos de Threads en Zephyr

Zephyr distingue entre threads del sistema y threads de usuario.

home

Main Thread

Punto de entrada de la aplicación. Ejecuta la función main(). Prioridad configurable (por defecto 0).

CONFIG_MAIN_THREAD_PRIORITY=0
bedtime

Idle Thread

Se ejecuta cuando ningún otro thread está listo. Tiene la prioridad más baja posible. Puede poner la CPU en modo de bajo consumo.

Prioridad: más baja del sistema
person

User Threads

Threads creados por la aplicación usando K_THREAD_DEFINE o k_thread_create(). ¡Tu código!

Prioridad: configurable
work

Workqueue Threads

Threads especiales que procesan elementos de trabajo (work items) de una cola. Zephyr incluye un system workqueue por defecto.

System Workqueue

Cola global del sistema. Útil para tareas diferidas desde ISRs o callbacks.

Custom Workqueue

Colas personalizadas con prioridad y stack propios. Mejor control.

Crear Threads con K_THREAD_DEFINE

La macro K_THREAD_DEFINE crea threads en tiempo de compilacion.

main.c
// Función de entrada del thread
void thread_entry_point(void *arg1, void *arg2, void *arg3)
{
while (1) {
printk("Thread running\n");
k_msleep(1000);
}
}
// Definir el thread estáticamente
K_THREAD_DEFINE(my_thread, 1024, thread_entry_point,
NULL, NULL, NULL, 5, 0, 0);
lightbulb
Estatico vs dinamico: K_THREAD_DEFINE crea el thread en tiempo de compilacion. Para creacion en tiempo de ejecucion, usa k_thread_create().

Parametros de K_THREAD_DEFINE

name

Identificador del thread (usado internamente)

stack_size

Bytes de stack para el thread (tipico: 512-2048)

entry

Funcion que ejecuta el thread

p1, p2, p3

Argumentos pasados a la funcion de entrada

prio

Prioridad (menor numero = mayor prioridad)

delay

Milisegundos antes de iniciar (0 = inmediato)

warning
Stack overflow: Un stack demasiado pequeno causara fallos dificiles de depurar. Usa CONFIG_THREAD_ANALYZER para verificar uso real.

Prioridades de Threads

Las prioridades determinan que thread se ejecuta cuando hay varios listos.

Rango de prioridades

-CONFIG_NUM_COOP_PRIORITIES -1 0 CONFIG_NUM_PREEMPT_PRIORITIES-1
Cooperativos (negativos)
Preemptivos (no negativos)
Mayor prioridad Menor prioridad
handshake

Cooperativos

prioridad < 0
  • check No pueden ser interrumpidos por el scheduler
  • check Deben ceder la CPU voluntariamente
  • check Útiles para secciones críticas
swap_vert

Preemptivos

prioridad >= 0
  • check Pueden ser interrumpidos por threads de mayor prioridad
  • check El scheduler puede forzar el cambio de contexto
  • check Mejor para aplicaciones con múltiples tareas
tips_and_updates
Regla importante: Menor número = mayor prioridad. Un thread con prioridad -5 tiene más prioridad que uno con 0, y éste más que uno con 5.

El Scheduler de Zephyr

El scheduler decide qué thread se ejecuta y cuándo cambia de contexto.

schedule ¿Cuándo ejecuta el scheduler?

1
Thread llama a k_yield()
Cede voluntariamente la CPU
2
Thread se bloquea
k_msleep(), k_sem_take(), k_msgq_get()...
3
Thread preemptivo interrumpido
Otro thread de mayor prioridad está listo
4
Thread termina
La función de entrada retorna

alt_route Puntos de replanificación

k_yield()

Cede CPU voluntariamente

k_msleep()

Duerme N milisegundos

k_sem_take()

Espera semáforo

k_msgq_get()

Espera mensaje

warning
Threads cooperativos: Solo ceden la CPU en puntos de replanificación. Un bucle infinito sin k_yield() o k_msleep() bloqueará todo el sistema.
tips_and_updates
Buena práctica: Incluye siempre un k_msleep() o k_yield() en los bucles infinitos de tus threads.

k_yield() vs k_msleep()

Dos formas de ceder la CPU con comportamientos diferentes.

sync

k_yield()

  • arrow_right Cede a threads de igual o mayor prioridad
  • arrow_right Retorna inmediatamente si no hay otros listos
  • arrow_right El thread queda en estado Runnable
Uso: cuando necesitas dar oportunidad a otros threads pero no quieres perder tiempo.
bedtime

k_msleep(ms)

  • arrow_right Pone el thread a dormir por tiempo especificado
  • arrow_right El thread pasa a estado Non-runnable
  • arrow_right No consume tiempo de CPU mientras duerme
Uso: para tareas periodicas o cuando se necesita un retardo especifico.

Comparacion Visual

Como afecta cada funcion al flujo de ejecucion de los threads.

sync k_yield() - Thread A y B (misma prioridad 5)
A
B
A
Ambos threads alternan porque tienen la misma prioridad
bedtime k_msleep(100) - Thread A duerme, Thread B ejecuta
A
B (A duerme)
A
B
Thread A no compite por CPU mientras duerme
tips_and_updates
Regla general: Usa k_msleep() para tareas periodicas. Usa k_yield() solo cuando necesitas dar oportunidad a otros threads sin esperar.

División de Tiempo

Permite que threads de igual prioridad compartan la CPU de forma equitativa.

settings Configuración en prj.conf

prj.conf
# Habilitar time slicing
CONFIG_TIMESLICING=y
# Duración del slice en ms
CONFIG_TIMESLICE_SIZE=10
# Prioridad máxima afectada
CONFIG_TIMESLICE_PRIORITY=0
info
Solo prioridades preemptivas: Time slicing no afecta a threads cooperativos (prioridad negativa). Solo aplica a threads con prioridad >= CONFIG_TIMESLICE_PRIORITY.

¿Cómo funciona?

Sin time slicing
Thread A ejecuta hasta ceder CPU

Un thread puede monopolizar la CPU si no cede voluntariamente

Con time slicing (10ms)
A
B
C
A
B

Threads de igual prioridad se turnan cada 10ms

lightbulb
Caso de uso: Útil cuando tienes varios threads que deben procesar datos de forma continua sin bloquear a los demás.

Colas de Trabajo

Alternativa a crear threads para ejecutar tareas diferidas.

Concepto

Un workqueue es un thread especial que procesa elementos de trabajo (work items) de una cola. Los work items se pueden enviar desde cualquier contexto, incluyendo ISRs.

pending Work 1
arrow_forward
pending Work 2
arrow_forward
play_arrow Processing

Ventajas de workqueues

  • check_circle Un solo thread puede procesar multiples tipos de trabajo
  • check_circle Menor uso de memoria que crear threads individuales
  • check_circle Seguro enviar trabajo desde ISRs
  • check_circle El sistema ya incluye un workqueue global

System Workqueue

Usando el workqueue del sistema para tareas diferidas.

workqueue_example.c
#include <zephyr/kernel.h>
struct k_work my_work;
void work_handler(struct k_work *work) {
printk("Work ejecutado!\n");
}
int main(void) {
k_work_init(&my_work, work_handler);
k_work_submit(&my_work); // Al system workqueue
return 0;
}
info
System workqueue: k_work_submit() envia al workqueue del sistema. Para un workqueue personalizado, usa k_work_submit_to_queue().

Workqueue Personalizado

Crear tu propia cola de trabajo con prioridad y stack propios.

custom_workqueue.c
// Definir stack y estructura
K_THREAD_STACK_DEFINE(my_wq_stack, 1024);
struct k_work_q my_work_q;
// Inicializar y arrancar workqueue
k_work_queue_init(&my_work_q);
k_work_queue_start(&my_work_q, my_wq_stack,
K_THREAD_STACK_SIZEOF(my_wq_stack), 5, NULL);
// Enviar trabajo al workqueue personalizado
k_work_submit_to_queue(&my_work_q, &my_work);
tips_and_updates
Cuando usar custom workqueue: Cuando necesitas controlar la prioridad del procesamiento o aislar tareas criticas del system workqueue.

Puntos Clave

Conceptos fundamentales de multithreading en Zephyr.

account_tree

Threads

Unidades de ejecucion con contexto propio (registros, stack, prioridad)

sync_alt

Estados

Running, Runnable, Non-runnable. El scheduler gestiona las transiciones

low_priority

Prioridades

Cooperativos (negativas) y preemptivos (no negativas). Menor = mayor prioridad

schedule

Scheduler

Ejecuta en puntos de replanificacion: yield, sleep, bloqueo, preempcion

timer

Time Slicing

Reparto equitativo de CPU entre threads de igual prioridad

work

Workqueues

Colas de trabajo para tareas diferidas. Alternativa eficiente a crear threads

tips_and_updates

"Multithreading permite dividir la aplicacion en tareas independientes que se ejecutan de forma concurrente."

Usa prioridades y puntos de sincronizacion para controlar el flujo de ejecucion.

arrow_forward Siguiente modulo

1.7 Sincronizacion entre Threads

Aprenderemos a coordinar threads usando semaforos, mutexes y colas de mensajes.