Ejecutando código en paralelo con el ESP32


El objetivo de esta post es en primer lugar comprender como se puede ejecutar la multitarea en el ESP32 para después ver como se implementa un algoritmo de cálculo de potencia simple en el ESP32 y probar la aceleración ejecutándolo en los dos núcleos del microcontrolador.

El ESP32 viene con 2 microprocesadores Xtensa LX6 de 32 bits: core 0 y core 1. Entonces, es dual core. Cuando ejecutamos código en Arduino IDE, de forma predeterminada, se ejecuta en el núcleo 1. En esta publicación, le mostraremos cómo ejecutar código en el segundo núcleo ESP32 mediante la creación de tareas. Puede ejecutar piezas de código simultáneamente en ambos núcleos y hacer que su ESP32 sea multitarea. Como nota imporante  no necesariamente necesita ejecutar doble núcleo para lograr la multitarea.

El ESP32 por tanto tiene dos núcleos Tensilica LX6 [1] que podemos usar para ejecutar código. Al momento de escribir, la forma más fácil de controlar la ejecución de código en los diferentes núcleos del ESP32 es usando FreeRTOS y asignar una tarea a cada CPU

Aunque de forma genérica son muchos los beneficios de tener más de un núcleo disponible para ejecutar código, uno de los más importantes es aumentar el rendimiento de nuestros programas. Entonces, aunque veremos en este post cómo ejecutar código en los dos núcleos del ESP32, también comprobaremos el aumento de rendimiento que podemos obtener de eso mediante una aplicación simple que sea capaz de calcular una potencia de los números de una matriz. Lo ejecutaremos en un solo núcleo y luego dividiremos el procesamiento por los dos núcleos y verificaremos si hay una ganancia en el tiempo de ejecución. Finalmente, solo para comparar, también dividiremos la ejecución entre cuatro tareas (dos asignadas a cada núcleo) solo para verificar si hay alguna aceleración para generar más tareas que núcleos.

Introducción

El ESP32 viene con 2 microprocesadores Xtensa LX6 de 32 bits, por lo que es de doble núcleo:

  • Núcleo 0
  • Núcleo 1

Cuando subimos el código al ESP32 usando el IDE de Arduino, simplemente se ejecuta; no tenemos que preocuparnos de qué núcleo ejecuta el código.

Hay una función que puede usar para identificar en qué núcleo se está ejecutando el código:

xPortGetCoreID()

Si usa esa función en un boceto de Arduino, verá que tanto setup() para la configuración() como el loopo()se ejecutan en el núcleo 1. Pruébelo usted mismo cargando el siguiente boceto en su ESP32.


void setup() {
  Serial.begin(115200);
  Serial.print("setup() running on core ");
  Serial.println(xPortGetCoreID());
}

void loop() {
  Serial.print("loop() running on core ");
  Serial.println(xPortGetCoreID());
}

Abra el Serial Monitor a una velocidad de transmisión de 115200 y verifique el núcleo en el que se ejecuta el boceto de Arduino.

Crear tareas

Arduino IDE es compatible con FreeRTOS para ESP32, que es un sistema operativo en tiempo real. Esto nos permite manejar varias tareas en paralelo que se ejecutan de forma independiente. Las tareas son fragmentos de código que ejecutan algo. Por ejemplo, puede hacer parpadear un LED, realizar una solicitud de red, medir lecturas de sensores, publicar lecturas de sensores, etc.

Para asignar partes específicas de código a un núcleo específico, debe crear tareas. Al crear una tarea, puede elegir en qué núcleo se ejecutará, así como su prioridad. Los valores de prioridad comienzan en 0, en el que 0 es la prioridad más baja. El procesador ejecutará primero las tareas con mayor prioridad.

Para crear tareas necesita seguir los siguientes pasos:

1. Crear un identificador de tarea. Un ejemplo para Task1:

TaskHandle_t Task1;

2. En la configuración()crear una tarea asignada a un núcleo específico usando elxTaskCreatePinnedToCorefunción. Esa función toma varios argumentos, incluida la prioridad y el núcleo donde se debe ejecutar la tarea (el último parámetro).

xTaskCreatePinnedToCore(
      Task1code, /* Function to implement the task */
      "Task1", /* Name of the task */
      10000,  /* Stack size in words */
      NULL,  /* Task input parameter */
      0,  /* Priority of the task */
      &Task1,  /* Task handle. */
      0); /* Core where the task should run */

3. Después de crear la tarea, debe crear una función que contenga el código para la tarea creada. En este ejemplo, debe crear la tarea e1código()función. Así es como se ve la función de tarea:

Void Task1code( void * parameter) {
  for(;;) {
    Code for task 1 - infinite loop
    (...)
  }
}

los for(;;)crean un bucle infinito. Por lo tanto, esta función se ejecuta de manera similar a la función loop(). Puede usarlo como un segundo ciclo en su código, por ejemplo.

Si durante la ejecución de su código desea eliminar la tarea creada, puede utilizar la función vTareaEliminar(), que acepta el identificador de tareas (Tarea 1) como argumento:

vTaskDelete(Task1);

Veamos cómo funcionan estos conceptos con un ejemplo sencillo.

Crear tareas en diferentes núcleos: ejemplo

Para seguir este ejemplo usaremos las siguientes partes:

Para crear diferentes tareas que se ejecuten en diferentes núcleos, crearemos dos tareas que parpadeen los LED con diferentes tiempos de retraso . Conectaremos mediante dos resistencias en serie de 330 ohmios el cátodo de un led rojo al puerto 4 (GPIO4) y el otro cátodo del led verde al GPIO2 uniendo ambas masas y conectando estas al ping GND del ESP32 .

El plano resultante seria similar al siguiente diagrama:

Crearemos dos tareas ejecutándose en diferentes núcleos:

  • Task1 se ejecuta en el núcleo 0;
  • Task2 se ejecuta en el núcleo 1;

Cargue el siguiente boceto en su ESP32 para hacer parpadear cada LED en un núcleo diferente:



TaskHandle_t Task1;
TaskHandle_t Task2;

// LED pins
const int led1 = 2;
const int led2 = 4;

void setup() {
  Serial.begin(115200); 
  pinMode(led1, OUTPUT);
  pinMode(led2, OUTPUT);

  //create a task that will be executed in the Task1code() function, with priority 1 and executed on core 0
  xTaskCreatePinnedToCore(
                    Task1code,   /* Task function. */
                    "Task1",     /* name of task. */
                    10000,       /* Stack size of task */
                    NULL,        /* parameter of the task */
                    1,           /* priority of the task */
                    &Task1,      /* Task handle to keep track of created task */
                    0);          /* pin task to core 0 */                  
  delay(500); 

  //create a task that will be executed in the Task2code() function, with priority 1 and executed on core 1
  xTaskCreatePinnedToCore(
                    Task2code,   /* Task function. */
                    "Task2",     /* name of task. */
                    10000,       /* Stack size of task */
                    NULL,        /* parameter of the task */
                    1,           /* priority of the task */
                    &Task2,      /* Task handle to keep track of created task */
                    1);          /* pin task to core 1 */
    delay(500); 
}

//Task1code: blinks an LED every 1000 ms
void Task1code( void * pvParameters ){
  Serial.print("Task1 running on core ");
  Serial.println(xPortGetCoreID());

  for(;;){
    digitalWrite(led1, HIGH);
    delay(1000);
    digitalWrite(led1, LOW);
    delay(1000);
  } 
}

//Task2code: blinks an LED every 700 ms
void Task2code( void * pvParameters ){
  Serial.print("Task2 running on core ");
  Serial.println(xPortGetCoreID());

  for(;;){
    digitalWrite(led2, HIGH);
    delay(700);
    digitalWrite(led2, LOW);
    delay(700);
  }
}

void loop() {
  
}

Cómo funciona el código

Nota: en el código creamos dos tareas y asignamos una tarea al núcleo 0 y otra al núcleo 1. Los bocetos de Arduino se ejecutan en el núcleo 1 de forma predeterminada. Por lo tanto, podría escribir el código para Task2 en el loop()(no hubo necesidad de crear otra tarea). En este caso, creamos dos tareas diferentes con fines de aprendizaje.

Sin embargo, según los requisitos de su proyecto, puede ser más práctico organizar su código en tareas como se muestra en este ejemplo.

El código comienza creando un identificador de tarea para Task1 y Task2 llamadoTarea 1yTarea 2.

TaskHandle_t Task1;
TaskHandle_t Task2;

Asigne GPIO 2 y GPIO 4 a los LED:

const int led1 = 2; 
const int led2 = 4;

En la configuración(), inicialice el monitor serie a una velocidad en baudios de 115200:

Serial.begin(115200);

Declare los LED como salidas:

pinMode(led1, OUTPUT); 
pinMode(led2, OUTPUT);

Luego, cree Task1 usando la función  xTaskCreatePinnedToCore() :

xTaskCreatePinnedToCore(
             Task1code, /* Task function. */
             "Task1",   /* name of task. */
             10000,     /* Stack size of task */
             NULL,      /* parameter of the task */
             1,         /* priority of the task */
             &Task1,    /* Task handle to keep track of created task */
             0);        /* pin task to core 0 */

Task1 se implementará con eltarea1código()función. Entonces, necesitamos crear esa función más adelante en el código. Le damos a la tarea la prioridad 1 y la anclamos al núcleo 0.

Creamos Task2 usando el mismo método:

xTaskCreatePinnedToCore(
             Task2code,  /* Task function. */
             "Task2",    /* name of task. */
             10000,      /* Stack size of task */
             NULL,       /* parameter of the task */
             1,          /* priority of the task */
             &Task2,     /* Task handle to keep track of created task */
             1);         /* pin task to core 0 */

Después de crear las tareas, necesitamos crear las funciones que ejecutarán esas tareas.

void Task1code( void * pvParameters ){
  Serial.print("Task1 running on core ");
  Serial.println(xPortGetCoreID());

  for(;;){
    digitalWrite(led1, HIGH);
    delay(1000);
    digitalWrite(led1, LOW);
    delay(1000);
  }
}

La función para Task1 se llamatarea1código()(Puedes llamarlo como quieras). Para fines de depuración, primero imprimimos el núcleo en el que se ejecuta la tarea:

Serial.print("Task1 running on core ");
Serial.println(xPortGetCoreID());

Entonces, tenemos un ciclo infinito similar alcírculo()en el boceto de Arduino. En ese ciclo, parpadeamos el LED1 cada segundo.

Lo mismo sucede con Task2, pero parpadeamos el LED con un tiempo de retraso diferente.

void Task2code( void * pvParameters ){
  Serial.print("Task2 running on core ");
  Serial.println(xPortGetCoreID());

  for(;;){
    digitalWrite(led2, HIGH);
    delay(700);
    digitalWrite(led2, LOW);
    delay(700);
  }
}

Finalmente, el bucle loop tiene ()la función está vacía:

void loop() { }

Nota: como se mencionó anteriormente, el bucle loop ()se ejecuta en el núcleo 1. Entonces, en lugar de crear una tarea para ejecutar en el núcleo 1, simplemente puede escribir su código dentro del loop().

Demostración

Suba el código a su ESP32. Asegúrese de tener la placa y el puerto COM correctos seleccionados.

Abra el monitor serie a una velocidad en baudios de 115200. Debería recibir los siguientes mensajes:

Como era de esperar, Task1 se ejecuta en el núcleo 0, mientras que Task2 se ejecuta en el núcleo 1.

En su circuito, un LED debe parpadear cada 1 segundo y el otro debe parpadear cada 700 milisegundos.

En resumen:

  • El ESP32 es de doble núcleo;
  • Los bocetos de Arduino se ejecutan en el núcleo 1 de forma predeterminada;
  • Para usar core 0 necesitas crear tareas;
  • Puede usar la función TaskCreatePinnedToCore() para fijar una tarea específica a un núcleo específico;
  • Con este método, puede ejecutar dos tareas diferentes de forma independiente y simultánea utilizando los dos núcleos.

Hemos visto un ejemplo simple con LED. La idea es utilizar este método con proyectos más avanzados con aplicaciones del mundo real. Por ejemplo, puede ser útil usar un núcleo para tomar lecturas de sensores y otro para publicar esas lecturas en un sistema de automatización del hogar. Ahora a continuación usando la multitarea vamos a calcular como aumenta la aceleración usando ambos núcleos


Cálculo de aceleración

Básicamente, lo que queremos comprobar es cuánto aumenta la velocidad de ejecución de nuestro programa cuando pasamos de una ejecución de un solo núcleo a una ejecución de doble núcleo. Entonces, una de las formas más fáciles de hacerlo es calcular la relación entre el tiempo de ejecución del programa que se ejecuta en un solo núcleo y el que se ejecuta en los dos núcleos del ESP32 [2].

Mediremos el tiempo de ejecución para cada enfoque utilizando la función Arduino micros , que devuelve la cantidad de microsegundos desde que la placa comenzó a ejecutar el programa [3].

Entonces, para medir un bloque de ejecución de código, haremos algo similar a lo que se indica a continuación.

start = micros();
//Run code
end = micros();
execTime = end - start;

Podríamos haber usado la función FreeRTOS xTaskGetTickCount  para una mayor precisión, pero la función micros será suficiente para lo que queremos mostrar y es una función muy conocida de Arduino.

Variables globales

Comenzaremos nuestro código declarando algunas variables globales auxiliares. No vamos a necesitar ningún include.

Como vamos a comparar aceleraciones en diferentes situaciones, vamos a especificar una matriz con múltiples exponentes para probar. Además, vamos a tener una variable con el tamaño de la matriz, para que podamos iterarla. Primero vamos a probar valores pequeños para el exponente y luego comenzaremos a aumentarlos mucho.

int n[10] = {2, 3, 4, 5, 10, 50, 100, 1000, 2000, 10000 };
int nArraySize = 10;

También vamos a tener algunas variables para almacenar el tiempo de ejecución del código en los diferentes casos de uso. Reutilizaremos las  variables de inicio y finalización  de la ejecución , pero usaremos una variable diferente para cada caso de uso: ejecución de una tarea, ejecución de dos tareas y ejecución de cuatro tareas. De esta forma, almacenaremos los valores para una última comparación.

unsigned long start;
unsigned long end;
unsigned long execTimeOneTask, execTimeTwoTask, execTimeFourTask ;

Luego, declararemos un semáforo de conteo para poder sincronizar la función de configuración (que se ejecuta en una tarea de FreeRTOS) con las tareas que vamos a ejecutar. Consulte esta publicación para obtener una explicación detallada sobre cómo lograr este tipo de sincronización.

Tenga en cuenta que la función xSemaphoreCreateCounting recibe como entrada el recuento máximo y el recuento inicial del semáforo. Como tendremos como máximo 4 tareas para sincronizar, su valor máximo será 4.

SemaphoreHandle_t barrierSemaphore = xSemaphoreCreateCounting( 4, 0 );

Finalmente, declararemos dos arreglos: uno con los valores iniciales (llamado bigArray ) y otro para almacenar los resultados (llamado resultArray ). Haremos sus tallas grandes.

int bigArray[10000], resultArray[10000];


La tarea de FreeRTOS

Definiremos la función que implementará nuestro algoritmo de cálculo de potencia, para ser lanzada como tarea. Si necesita ayuda con los detalles sobre cómo definir una tarea de FreeRTOS, consulte este tutorial anterior.

Lo primero que debemos tener en cuenta es que nuestra función recibirá algunos parámetros de configuración. Dado que en algunos de los casos de uso vamos a dividir la ejecución del algoritmo en varias tareas, necesitamos controlar la parte de la matriz que cubrirá cada tarea.

Para crear solo una tarea genérica que pueda responder a todos nuestros casos de uso, pasaremos como parámetro los índices de la matriz de la que será responsable la tarea. Además, vamos a pasar el exponente, así que ahora sabemos cuántas multiplicaciones necesitamos realizar. Puede consultar con más detalle cómo pasar un argumento a una tarea de FreeRTOS en este tutorial anterior.

Entonces, primero definiremos una estructura con estos 3 parámetros, para poder pasarla a nuestra función. Puedes leer más sobre estructuras aquí . Tenga en cuenta que esta estructura se declara fuera de cualquier función. Entonces, el código se puede colocar cerca de la declaración de variables globales.

struct argsStruct {
  int arrayStart;
  int arrayEnd;
  int n;
};

Como ya tenemos declarada nuestra estructura, dentro de la función declararemos una variable del tipo de esta estructura. Luego, asignaremos el parámetro de entrada de la función a esta variable. Recuerde que los parámetros se pasan a las funciones de FreeRTOS como un puntero a nulo ( void * ) y es nuestra responsabilidad devolverlo al tipo original.

Tenga en cuenta también que le pasamos a la función un puntero a la variable original y no a la variable real, por lo que necesitamos usar el puntero para acceder al valor. 

	
argsStruct myArgs = *((argsStruct*)parameters);

Ahora, implementaremos la función de cálculo de potencia. Tenga en cuenta que podríamos haber usado la función Arduino pow , pero luego explicaré por qué no lo hicimos. Entonces, implementaremos la función con un ciclo donde multiplicaremos un valor por sí mismo n veces.

Comenzamos declarando una variable para contener el producto parcial. Luego, haremos un ciclo for para iterar todos los elementos de la matriz asignada a la tarea. Recuerda que este era nuestro objetivo inicial. Luego, para cada elemento, calcularemos su potencia y al final le asignaremos el valor a la matriz de resultados. Esto asegura que usaremos los mismos datos en todas nuestras pruebas y no cambiaremos la matriz original.

Para un código más limpio, podríamos haber implementado el algoritmo pow en una función dedicada, pero se intenta minimizar las llamadas a funciones auxiliares para un código más compacto.

Verifique el código a continuación. Dejo ahí comentada una línea de código para calcular la potencia usando la función pow , así que puedes probarlo si quieres. Tenga en cuenta que los resultados de aceleración serán considerablemente diferentes. Además, para acceder a un elemento de una variable de estructura, usamos el nombre de la variable punto («.») el nombre del elemento.

int product;
for (int i = myArgs.arrayStart; i < myArgs.arrayEnd; i++) {
 
    product = 1;
    for (int j = 0; j < myArgs.n; j++) {
      product =  product * bigArray[i];
    }
    resultArray[i]=product;
    //resultArray [i] = pow(bigArray[i], myArgs.n);
}

Compruebe el código de función completo a continuación. Tenga en cuenta que al final del código estamos aumentando el semáforo de conteo global en una unidad. Esto se hace para asegurar la sincronización con la función de configuración, ya que contaremos el tiempo de ejecución a partir de ahí.  Además, al final, estamos eliminando la tarea.

void powerTask( void * parameters ) {
 
  argsStruct  myArgs = *((argsStruct*)parameters);
 
  int product;
  for (int i = myArgs.arrayStart; i < myArgs.arrayEnd; i++) {
 
    product = 1;
    for (int j = 0; j < myArgs.n; j++) {
      product =  product * bigArray[i];
    }
    resultArray[i]=product;
    //resultArray [i] = pow(bigArray[i], myArgs.n);
  }
 
  xSemaphoreGive(barrierSemaphore);
 
  vTaskDelete(NULL);
 
}


La función de configuración

Vamos a hacer todo el código restante en la función de configuración, por lo que nuestro ciclo principal estará vacío. Comenzaremos abriendo una conexión en serie para generar los resultados de nuestras pruebas.

Serial.begin(115200);
Serial.println();

Ahora, inicializaremos nuestra matriz con algunos valores para aplicar el cálculo. Tenga en cuenta que no nos preocupa el contenido de la matriz ni el resultao real, sino los tiempos de ejecución. Entonces, vamos a inicializar la matriz con valores aleatorios solo para mostrar esas funciones, ya que no vamos a imprimir la matriz que contendrá los resultados.

Primero vamos a llamar a la función randomSeed , por lo que los valores aleatorios generados diferirán en diferentes ejecuciones del programa [4]. Si el pin analógico está desconectado, devolverá un valor correspondiente a ruido aleatorio, lo cual es ideal para pasar como entrada de la función randomSeed .

Después de eso, podemos simplemente llamar a la función aleatoria  , pasando como argumentos los valores mínimo y máximo que se pueden devolver [4]. Nuevamente, estamos haciendo esto solo con fines ilustrativos, ya que no vamos a imprimir el contenido de las matrices.

randomSeed(analogRead(0));
for (int i = 0; i < 10000; i++) {
  bigArray[i] = random(1, 10);
}

Ahora, vamos a definir las variables que se utilizarán como argumentos para nuestras tareas. Recuerda la estructura declarada anteriormente, que contendrá los índices del arreglo que procesará cada tarea y el exponente.

Entonces, el primer elemento de la estructura es el índice inicial, el segundo es el índice final y el tercero es el exponente. Declararemos estructuras para cada caso de uso (una tarea, dos tareas y cuatro tareas) como se puede ver en el código a continuación. Tenga en cuenta que en el último elemento de la estructura estamos pasando un elemento de la matriz global que declaramos con los exponentes. Entonces, como veremos en el código final, iteraremos toda la matriz, pero por ahora mantengamos las cosas simples.

argsStruct oneTask = { 0 , 1000 , n[i] };
 
argsStruct twoTasks1 = { 0 , 1000 / 2 , n[i] };
argsStruct twoTasks2 = { 1000 / 2 , 1000 , n[i] };
 
argsStruct fourTasks1 = { 0 , 1000 / 4 , n[i] };
argsStruct fourTasks2 = { 1000 / 4 , 1000 / 4 * 2, n[i]};
argsStruct fourTasks3 = { 1000 / 4 * 2, 1000 / 4 * 3, n[i]};
argsStruct fourTasks4 = { 1000 / 4 * 3 , 1000, n[i]};

Ahora, vamos a hacer la prueba usando una sola tarea. Empezamos por obtener el tiempo de ejecución con la función micros. Luego, usaremos la función  xTaskCreatePinnedToCore para crear una tarea de FreeRTOS anclada a uno de los núcleos. Elegiremos el núcleo 1.

Vamos a pasar como parámetro la dirección de la variable de estructura oneTask , que contiene los argumentos necesarios para que se ejecute la función. No olvides el yeso al vacío* .

Una cosa muy importante a tener en cuenta es que aún no hemos analizado qué tareas pueden haber sido lanzadas por el núcleo de Arduino y que pueden influir en el tiempo de ejecución. Entonces, para garantizar que nuestra tarea se ejecutará con mayor prioridad, le asignaremos un valor de 20. Recuerde, los números más altos significan una mayor prioridad de ejecución para el programador de FreeRTOS.

Después de iniciar la tarea, solicitaremos una unidad del semáforo, asegurándonos de que la función de configuración se mantendrá hasta que la nueva tarea termine de ejecutarse. Finalmente, imprimiremos el tiempo de ejecución.

Serial.println("");
Serial.println("------One task-------");
 
start = micros();
 
xTaskCreatePinnedToCore(
  powerTask,               /* Function to implement the task */
  "powerTask",              /* Name of the task */
  10000,                   /* Stack size in words */
  (void*)&oneTask,         /* Task input parameter */
  20,                      /* Priority of the task */
  NULL,                    /* Task handle. */
  1);                      /* Core where the task should run */
 
xSemaphoreTake(barrierSemaphore, portMAX_DELAY);
 
end = micros();
execTimeOneTask = end - start;
Serial.print("Exec time: ");
Serial.println(execTimeOneTask);
Serial.print("Start: ");
Serial.println(start);
Serial.print("end: ");
Serial.println(end);

Esto es más o menos lo que vamos a hacer para el resto de los casos de uso. La única diferencia es que vamos a lanzar más tareas y vamos a intentar sacar más unidades del semáforo (tantas como tareas lanzadas para ese caso de uso).

Consulte el código fuente completo a continuación, que ya incluye los casos de uso para ejecutar el código con dos tareas (una por núcleo ESP32) y cuatro tareas (dos por núcleo ESP32). Además, al final de la función de configuración, incluye la impresión de los resultados de aceleración para cada iteración.

Tenga en cuenta que el parámetro extra de la función Serial.println indica el número de lugares decimales para el número de punto flotante.

int n[10] = {2, 3, 4, 5, 10, 50, 100, 1000, 2000, 10000  };
int nArraySize = 10;
 
struct argsStruct {
  int arrayStart;
  int arrayEnd;
  int n;
};
 
unsigned long start;
unsigned long end;
unsigned long execTimeOneTask, execTimeTwoTask, execTimeFourTask ;
 
SemaphoreHandle_t barrierSemaphore = xSemaphoreCreateCounting( 4, 0 );
 
int bigArray[10000], resultArray[10000];
 
void setup() {
 
  Serial.begin(115200);
  Serial.println();
 
  randomSeed(analogRead(0));
 
  for (int i = 0; i < 10000; i++) {
    bigArray[i] = random(1, 10);
  }
 
  for (int i = 0; i < nArraySize; i++) {
 
    Serial.println("#############################");
    Serial.print("Starting test for n= ");
    Serial.println(n[i]);
 
    argsStruct oneTask = { 0 , 1000 , n[i] };
 
    argsStruct twoTasks1 = { 0 , 1000 / 2 , n[i] };
    argsStruct twoTasks2 = { 1000 / 2 , 1000 , n[i] };
 
    argsStruct fourTasks1 = { 0 , 1000 / 4 , n[i] };
    argsStruct fourTasks2 = { 1000 / 4 , 1000 / 4 * 2,   n[i]};
    argsStruct fourTasks3 = { 1000 / 4 * 2, 1000 / 4 * 3, n[i]};
    argsStruct fourTasks4 = { 1000 / 4 * 3 , 1000,     n[i]};
 
    Serial.println("");
    Serial.println("------One task-------");
 
    start = micros();
 
    xTaskCreatePinnedToCore(
      powerTask,               /* Function to implement the task */
      "powerTask",              /* Name of the task */
      10000,                   /* Stack size in words */
      (void*)&oneTask,         /* Task input parameter */
      20,                      /* Priority of the task */
      NULL,                    /* Task handle. */
      1);                      /* Core where the task should run */
 
    xSemaphoreTake(barrierSemaphore, portMAX_DELAY);
 
    end = micros();
    execTimeOneTask = end - start;
    Serial.print("Exec time: ");
    Serial.println(execTimeOneTask);
    Serial.print("Start: ");
    Serial.println(start);
    Serial.print("end: ");
    Serial.println(end);
 
    Serial.println("");
    Serial.println("------Two tasks-------");
 
    start = micros();
 
    xTaskCreatePinnedToCore(
      powerTask,                /* Function to implement the task */
      "powerTask",              /* Name of the task */
      10000,                    /* Stack size in words */
      (void*)&twoTasks1,        /* Task input parameter */
      20,                       /* Priority of the task */
      NULL,                     /* Task handle. */
      0);                       /* Core where the task should run */
 
    xTaskCreatePinnedToCore(
      powerTask,               /* Function to implement the task */
      "coreTask",              /* Name of the task */
      10000,                   /* Stack size in words */
      (void*)&twoTasks2,       /* Task input parameter */
      20,                      /* Priority of the task */
      NULL,                    /* Task handle. */
      1);                      /* Core where the task should run */
 
    for (int i = 0; i < 2; i++) {
      xSemaphoreTake(barrierSemaphore, portMAX_DELAY);
    }
 
    end = micros();
    execTimeTwoTask = end - start;
    Serial.print("Exec time: ");
    Serial.println(execTimeTwoTask);
    Serial.print("Start: ");
    Serial.println(start);
    Serial.print("end: ");
    Serial.println(end);
 
    Serial.println("");
    Serial.println("------Four tasks-------");
 
    start = micros();
 
    xTaskCreatePinnedToCore(
      powerTask,                /* Function to implement the task */
      "powerTask",              /* Name of the task */
      10000,                    /* Stack size in words */
      (void*)&fourTasks1,       /* Task input parameter */
      20,                       /* Priority of the task */
      NULL,                     /* Task handle. */
      0);                       /* Core where the task should run */
 
    xTaskCreatePinnedToCore(
      powerTask,                /* Function to implement the task */
      "powerTask",              /* Name of the task */
      10000,                    /* Stack size in words */
      (void*)&fourTasks2,       /* Task input parameter */
      20,                       /* Priority of the task */
      NULL,                     /* Task handle. */
      0);                       /* Core where the task should run */
 
    xTaskCreatePinnedToCore(
      powerTask,                /* Function to implement the task */
      "powerTask",              /* Name of the task */
      10000,                    /* Stack size in words */
      (void*)&fourTasks3,       /* Task input parameter */
      20,                       /* Priority of the task */
      NULL,                     /* Task handle. */
      1);                       /* Core where the task should run */
 
    xTaskCreatePinnedToCore(
      powerTask,                /* Function to implement the task */
      "powerTask",              /* Name of the task */
      10000,                    /* Stack size in words */
      (void*)&fourTasks4,       /* Task input parameter */
      20,                       /* Priority of the task */
      NULL,                     /* Task handle. */
      1);                       /* Core where the task should run */
 
    for (int i = 0; i < 4; i++) {
      xSemaphoreTake(barrierSemaphore, portMAX_DELAY);
    }
 
    end = micros();
    execTimeFourTask = end - start;
    Serial.print("Exec time: ");
    Serial.println(execTimeFourTask);
    Serial.print("Start: ");
    Serial.println(start);
    Serial.print("end: ");
    Serial.println(end);
 
    Serial.println();
    Serial.println("------Results-------");
 
    Serial.print("Speedup two tasks: ");
    Serial.println((float) execTimeOneTask / execTimeTwoTask, 4 );
 
    Serial.print("Speedup four tasks: ");
    Serial.println((float)execTimeOneTask / execTimeFourTask, 4 );
 
    Serial.print("Speedup four tasks vs two tasks: ");
    Serial.println((float)execTimeTwoTask / execTimeFourTask, 4 );
 
    Serial.println("#############################");
    Serial.println();
  }
 
}
 
void loop() {
 
}
 
void powerTask( void * parameters ) {
 
  argsStruct  myArgs = *((argsStruct*)parameters);
 
  int product;
  for (int i = myArgs.arrayStart; i < myArgs.arrayEnd; i++) {
 
    product = 1;
    for (int j = 0; j < myArgs.n; j++) {
      product =  product * bigArray[i];
    }
 
    resultArray[i]=product;
    //resultArray [i] = pow(bigArray[i], myArgs.n);
  }
 
  xSemaphoreGive(barrierSemaphore);
 
  vTaskDelete(NULL);
 
}


Probando el código

Para probar el código, simplemente cárguelo con el IDE de Arduino y abra el monitor serie. Debería obtener un resultado similar a la figura 1. Naturalmente, los tiempos de ejecución pueden variar.

Análisis de aceleración de doble núcleo ESP32

Figura 1 : Salida del programa de prueba de aceleración.

Los resultados para cada exponente se muestran en la siguiente tabla, en la tabla 1.

Exponente1 tarea [µs]2 tareas [µs]4 tareas [µs]Acelerar 1 tarea frente a 2 tareasAcelerar 1 tarea frente a 4 tareasAcelerar 2 tareas vs 4 tareas
22291832961.25140.77360.6182
32712073251.30920.83380.6369
43122243401.39290.91760.6588
53542493671.42170.96460.6785
105563474511.60231.23280.7694
502235118813051.88131.71260.9103
1004331223423431.93871.84850.9535
10004207221108212121.99321.98340.9951
20008399242138421901.99331.99080.9988
100004194002102832103101.99451.99420.9999

Tabla 1 – Resultados de aceleración para los exponentes definidos en el código.


Analizando los resultados

Para comprender los resultados, primero debemos tener en cuenta que no se puede paralelizar completamente todo el programa. Entonces, siempre habrá partes del código que se ejecuten en un solo núcleo, como el lanzamiento de las tareas o los mecanismos de sincronización.

Aunque no teníamos este caso de uso, muchos algoritmos de paralelización también tienen una parte en la que cada resultado parcial se agrega secuencialmente (por ejemplo, si tenemos varias tareas calculando el valor máximo de su parte de la matriz y luego la tarea principal calcula el valor máximo entre todos los resultados parciales).

Entonces, hagamos lo que hagamos, teóricamente no podemos lograr una aceleración igual a la cantidad de núcleos (hay algunas excepciones en, por ejemplo, algoritmos que buscan un valor específico y salen cuando lo encuentran, pero no nos compliquemos).

Entonces, mientras más cómputos ejecutemos en paralelo versus la porción que ejecutamos en secuencia, más aceleración tendremos. Y eso es precisamente lo que vemos en nuestros resultados. Si comenzamos con un exponente de 2 , nuestra aceleración de la ejecución de una tarea a dos tareas es solo de  1.2514 .

Pero a medida que aumentamos el valor del exponente, hacemos más iteraciones del ciclo más interno y, por lo tanto, más cálculos podemos realizar en paralelo. Así, a medida que aumentamos el exponente, vemos un aumento de la aceleración. Por ejemplo, con un exponente de 1000 , nuestra aceleración es  1.9933 . Lo cual es un valor muy alto en comparación con el anterior.

Entonces, esto significa que la parte secuencial se diluye y para grandes exponentes la paralelización compensa. Tenga en cuenta que podemos lograr este tipo de valores altos porque el algoritmo es muy simple y fácil de paralelizar. Además, debido a la forma en que FreeRTOS maneja las prioridades y los cambios de contexto de ejecución de tareas, no hay mucha sobrecarga después de que se ejecutan las tareas. Una computadora normal tiende a tener más cambios de contexto entre subprocesos y, por lo tanto, la sobrecarga secuencial es mayor.

Tenga en cuenta que no usamos la función pow para mostrar esta progresión en los valores de aceleración. La función pow usa flotantes, lo que significa que su implementación es más intensiva en computación. Entonces, obtendríamos aceleraciones mucho mejores en exponentes más bajos porque la parte paralela sería mucho más relevante que la secuencial. Puede comentar nuestro algoritmo implementado y usar la función pow  para comparar los resultados.

Además, es importante tener en cuenta que lanzar más tareas que la cantidad de núcleos no aumenta la velocidad. De hecho, tiene el efecto contrario. Como podemos ver en los resultados, la aceleración de ejecutar con 4 tareas siempre es menor que la de ejecutar con 2 tareas. De hecho, para exponentes bajos, en realidad es más lento ejecutar el código con 4 tareas (aunque estén divididas entre los dos núcleos del ESP32) que ejecutar con una tarea en un solo núcleo.

Esto es normal porque cada CPU solo puede ejecutar una tarea en un momento dado y, por lo tanto, lanzar más tareas que CPU significa más tiempo secuencial para manejarlas y más sobrecarga en los puntos de sincronización.

Sin embargo, tenga en cuenta que esto no siempre es blanco y negro. Si las tareas tuvieran algún tipo de punto de rendimiento en el que dejarían de esperar algo, entonces tener más tareas que núcleos podría ser beneficioso para usar los ciclos de CPU libres. Sin embargo, en nuestro caso, las CPU nunca rinden y siempre se están ejecutando, por lo que no hay ningún beneficio en lanzar más de 2 tareas, en cuanto a la aceleración.


La ley de Amdhal

Para complementar los resultados, veremos la ley de Amdahl . Entonces, vamos a aplicar algunas transformaciones a la fórmula de aceleración que estábamos usando. La fórmula inicial era que la aceleración es igual al tiempo de ejecución secuencial (lo llamaremos T ) dividido por el tiempo de ejecución en paralelo (lo llamaremos T Paralelo ).

fórmula de aceleración

Pero, como vimos, hay una parte del tiempo de ejecución que siempre es secuencial y otra que puede ser paralelizada. Llamemos p a la fracción del programa que podemos ejecutar en paralelo. Como es una fracción, su valor estará entre 0 y 1.

Entonces, podemos representar el tiempo de ejecución secuencial T como la porción que puede ejecutarse en paralelo ( p*T ) más la porción que no puede (1-p)*T .

ejecución secuencial

Como solo podemos dividir la parte paralela entre los núcleos de nuestra máquina, el tiempo T Parallel es similar a la fórmula anterior, excepto que la parte paralela aparece dividida por la cantidad de núcleos que tenemos disponibles.

ejecución en paralelo

Entonces, nuestra fórmula de aceleración se convierte en:

Fórmula modificada de aceleración

Ahora tenemos la aceleración escrita en función de T , el tiempo de ejecución original sin mejoras. Entonces, podemos dividir cada término por T :

acelerar la ley de amdahl

Entonces, ahora tenemos la aceleración escrita en función de la parte que puede ejecutarse en paralelo y la cantidad de núcleos. Para terminar, supongamos que tenemos una cantidad infinita de núcleos disponibles para ejecutar (el caso de uso real sería un número muy grande, pero analizaremos matemáticamente la fórmula).

Acelera un número infinito de núcleos

Como una constante dividida por infinito es igual a 0, terminamos con:

Acelerar un número infinito de núcleos expresión final

Así que, aunque la aceleración aumenta con la cantidad de recursos disponibles para la paralelización (la cantidad de núcleos, en nuestro caso), lo cierto es que la máxima aceleración teórica posible está limitada por la parte no paralela, que no podemos optimizar.

Esta es una conclusión interesante para que decidamos si vamos o no a la paralelización. Algunas veces, los algoritmos pueden paralelizarse fácilmente y tenemos una tremenda aceleración, otras el esfuerzo necesario no justifica la ganancia.

Además, otra cosa importante a tener en cuenta es que el mejor algoritmo secuencial no siempre es el mejor después de la paralelización. Entonces, a veces, es mejor usar un algoritmo que tiene menos rendimiento en la ejecución secuencial, pero termina siendo mucho mejor que el mejor secuencial en la paralelización.


Resumen

Con este post, confirmamos que las funciones disponibles para la ejecución multinúcleo funcionan bien y podemos aprovecharlas para obtener beneficios de rendimiento.

La última parte más teórica tenía el objetivo de mostrar que la paralelización no es un tema trivial, y uno no puede saltar directamente a un enfoque paralelo sin un análisis previo y esperar una aceleración igual a la cantidad de núcleos disponibles. Este razonamiento se extiende más allá del alcance del ESP32 y se aplica a la computación paralela en general.

Entonces, para aquellos que van a comenzar con la computación paralela utilizando el ESP32 para mejorar el rendimiento, es un buen comienzo para aprender primero algunos de los conceptos más teóricos.

Finalmente, tenga en cuenta que el código está orientado a mostrar los resultados, por lo que se podrían haber hecho muchas optimizaciones, como lanzar las tareas en un ciclo en lugar de repetir el código o condensar el algoritmo en una función.

Además, no buscábamos los resultados de la ejecución para confirmar que la matriz de salida era la misma porque ese no era nuestro enfoque principal. Naturalmente, pasar de secuencial a paralelo requiere una implementación cuidadosa del código y la ejecución de muchas pruebas para garantizar que el resultado sea el mismo.

Mas información en

[1] https://espressif.com/en/products/hardware/esp32/overview

[2] http://www.dcc.fc.up.pt/~fds/aulas/PPD/1112/metrics_en.pdf

[3] https://www.arduino.cc/en/reference/micros

[4] https://www.arduino.cc/en/reference/random

[5] https://techtutorialsx.com/2017/05/16/esp32-dual-core-execution-speedup/

Como ejecutar un cuaderno de Jupyter Notebook en Visual Studio Code


En efecto gracias a la extensión de Jupyter para Visual Studio Code podemos ejecutar nuestros cuadernos en python de Jupyter Notebook desde el interfaz de Visual Studio Code, editor que como probablemente amigo lector sabrà es gratuito y uno de los más potentes IDEs hasta la fecha.

Jupyter Notebook es una herramienta increíblemente poderosa para desarrollar y presentar proyectos de ciencia de datos de forma interactiva. Este post intentara guiarle de cómo usar Jupyter Notebooks para proyectos de ciencia de datos y cómo configurarlo en su máquina local pero en vez de usar un navegador lo cual nos genera ciertos problemas ( versionado, restauración de código , etc.) lo usaremos desde el potente IDE de Visual Studio Code.

jupyter-notebook las mejores herramientas gratuitas de ciencia de datos

Pero primero: ¿qué es un “cuaderno”?

Un cuaderno integra código y su salida en un solo documento que combina visualizaciones, texto narrativo, ecuaciones matemáticas y otros medios enriquecidos. En otras palabras: es un documento único en el que puede ejecutar código, mostrar el resultado y también agregar explicaciones, fórmulas, gráficos y hacer que su trabajo sea más transparente, comprensible, repetible y compartible. 

El uso de Notebooks es ahora una parte importante del flujo de trabajo de la ciencia de datos en empresas de todo el mundo. Si su objetivo es trabajar con datos, el uso de una computadora portátil acelerará su flujo de trabajo y facilitará la comunicación y el intercambio de resultados. 

Lo mejor de todo es que, como parte del proyecto de código abierto  Jupyter , los cuadernos de Jupyter son completamente gratuitos. Puede descargar el software  solo o como parte del kit de herramientas de ciencia de datos de Anaconda .

Aunque es posible utilizar muchos lenguajes de programación diferentes en Jupyter Notebooks, nos centraremosen Python, ya que es el caso de uso más común. (Entre los usuarios de R, R Studio  tiende a ser una opción más popular).Jupyter Notebooks también puede actuar como una plataforma flexible para familiarizarse con los pandas e incluso con Python, como se verá en este tutorial.


Las extensiones de Visual Studio Code proporcionan soporte básico de cuaderno para los kernels de lenguaje que son compatibles con los cuadernos Jupyter hoy en día. Muchos kernels de lenguaje funcionarán sin ninguna modificación. Para habilitar las características avanzadas, pueden ser necesarias modificaciones en las extensiones de lenguaje de VS Code.

La extensión Jupyter incluye por defecto las extensiones Jupyter Keymaps y Jupyter Notebook Renderers. La extensión Jupyter Keymaps proporciona mapas de teclado consistentes con Jupyter y la extensión Jupyter Notebook Renderers proporciona renderizadores para tipos MIME como latex, plotly, vega, y similares. Ambas extensiones pueden deshabilitarse o desinstalarse. Y por cierto, la extensión está disponible en varios idiomas: de, en, es, fa, fr, it, ja, ko-kr, nl, pl, pt-br, ru, tr, zh-cn, zh-tw

La Extensión Jupyter utiliza el soporte de cuadernos incorporado de VS Code. Esta interfaz de usuario ofrece una serie de ventajas a los usuarios de cuadernos:

  • Soporte inmediato de la amplia gama de funciones básicas de edición de código de VS Code, como la salida en caliente, la búsqueda y el reemplazo, y el plegado de código.
  • Extensiones del editor como VIM, coloración de corchetes, linters y muchas más están disponibles mientras se edita una celda.
  • Profunda integración con el banco de trabajo general y con las funciones basadas en archivos de VS Code, como la vista de esquema (tabla de contenidos), las migas de pan y otras operaciones.
  • Tiempos de carga rápidos para los archivos Jupyter notebook (.ipynb). Cualquier archivo de cuaderno se carga y renderiza lo más rápidamente posible, mientras que las operaciones relacionadas con la ejecución se inicializan entre bastidores.
  • Incluye una herramienta de diferencias para cuadernos, que facilita la comparación y la visualización de las diferencias entre las celdas de código, los resultados y los metadatos.
  • Extensibilidad más allá de lo que proporciona la extensión Jupyter. Las extensiones ahora pueden añadir su propio lenguaje o tiempo de ejecución específico a los cuadernos, como los cuadernos interactivos de .NET y Gather
  • Aunque la extensión de Jupyter viene con un amplio conjunto de los renderizadores más utilizados para la salida, el mercado admite renderizadores personalizados instalables para que el trabajo con tus cuadernos sea aún más productivo. Para empezar a escribir los tuyos propios, consulta la documentación de la api de renderizadores de VS Code.

Trabajar con Python


Si quiere trabajar con Python sólo tiene que asegurarte de que utiliza la última versión de la Extensión Python para disfrutar de la asociación conjunta de las Extensiones Python y Juypter.

Por favor, siga las instrucciones de Léame de la Extensión Python para empezar y visite la Documentación de Python para aprender más sobre cómo la Extensión Python y Jupyter trabajan juntas para proporcionar una experiencia óptima de cuadernos Python.

Ejecutar por línea


Para iniciar una sesión de depuración ligera y ejecutar celdas de código línea por línea en cuadernos de Python, pulse F10 mientras selecciona una celda o haga clic en el botón Ejecutar por línea de la barra de herramientas de la celda. También admite núcleos remotos.

Una vez que inicies una sesión de Ejecutar por Línea, aparecerá el Explorador de Variables y los valores de las variables se actualizarán a medida que itera a través de su código.

Para recorrer el resto de la celda durante una sesión Run by Line pulse Ctrl+Enter. Para parar, puede hacer clic en el botón de interrupción en el lado izquierdo de la celda.


Por cierto como ya se ha comentado ,la Extensión Jupyter soporta otros lenguajes además de Python como Julia, R y C#.

Estsos serian los pasos a seguir para un Inicio rápido


Para crear un nuevo cuaderno, abra la paleta de comandos (Windows: Ctrl + Shift + P, macOS: Command + Shift + P) y seleccione el comando «Crear: Nuevo cuaderno Jupyter».

Selecciona su kernel haciendo clic en el selector de kernel en la parte superior derecha del cuaderno o invocando el comando «Notebook: Seleccionar núcleo de cuaderno».(Notebook: Select Notebook Kernel)

Cambie el idioma de la celda haciendo clic en el selector de idioma o invocando el comando «Cuaderno: Cambiar idioma de la celda». (Notebook: Change Cell Language)

Comandos útiles


Abre la paleta de comandos (Comando+Mayúsculas+P en macOS y Ctrl+Mayúsculas+P en Windows/Linux) y escribe uno de los siguientes comandos:

CommandDescription
Create: New Jupyter NotebookCreate: New Jupyter Notebook
Notebook: Select Notebook KernelSelect or switch kernels within your notebook
Notebook: Change Cell LanguageChange the language of the cell currently in focus
Jupyter: Export to HTML Jupyter: Export to PDFCreate a presentation-friendly version of your notebook in HTML or PDF

Para ver todos los comandos disponibles de Jupyter Notebook, abra la paleta de comandos y escriba Jupyter o Notebook.

Teclas de contexto para los enlaces de teclas


Puede utilizar las teclas de contexto de la extensión en cláusulas «cuando». He aquí un ejemplo:

  {
    "key": "ctrl+i",
    "command": "jupyter.runAndDebugCell",
    "when": "!jupyter.webExtension"
  }

Ese keybinding establece que el comando jupyter.runAndDebugCell debe asignarse a CTRL+I cuando no está en la jupyter.webExtension.

La lista completa de teclas de contexto se puede encontrar aquí: https://github.com/microsoft/vscode-jupyter/wiki/Extensibility-for-other-extensions#context-keys-for-keybindings

Mas información:


Aprenda más sobre las ricas características de la extensión Jupyter:

IntelliSense: Edite su código con autocompletado, navegación por el código, comprobación de sintaxis y mucho más. (mas info en https://code.visualstudio.com/docs/python/editing#_autocomplete-and-intellisense)

Cuadernos Jupyter: Cree y edite cuadernos Jupyter, añada y ejecute celdas de código/marcadas, haga trazados, cree versiones de su cuaderno aptas para la presentación exportándolas a HTML o PDF y mucho más. (mas info en https://code.visualstudio.com/docs/python/jupyter-support)

Descarga extension Jupyter para Visual Studio Code:Mas información en https://marketplace.visualstudio.com/items?itemName=ms-toolsai.jupyter

Como crear una aplicacion Blazor


En este post vamos a ver la creación de una aplicación web de un chat  en tiempo real mediante SignalR con Blazor WebAssembly. 

Los pasos a seguir son los siguientes:

  • Crear un proyecto de aplicación Blazor WebAssembly hospedado
  • Adición de la biblioteca cliente de SignalR
  • Agregar un concentrador de SignalR
  • Agregar servicios de SignalR y un punto de conexión para el concentrador de SignalR
  • Agregar código de componente de Razor para chat

Al final de este post, tendrá una aplicación de chat funcional

 

Antes de comenzar este post, se recomienda instalar un editor.

https://dotnet.microsoft.com/download

 

https://dotnet.microsoft.com/download/dotnet/thank-you/sdk-5.0.100-windows-x64-installer 

 

 

 

 

Creación de un proyecto de aplicación Blazor WebAssembly hospedado

Siga las instrucciones para su elección de herramientas:

Se requiere Visual Studio 16.8 o posterior y el SDK de .NET Core 5.0.0 o posterior.

  1. Cree un nuevo proyecto.
  2. Seleccione Aplicación Blazor y luego Siguiente.
  3. Escriba BlazorSignalRApp en el campo Nombre del proyecto. Confirme que la entrada de Ubicación es correcta o proporcione una ubicación para el proyecto. Seleccione Crear.
  4. Elija la plantilla Aplicación de Blazor WebAssembly .
  5. En Avanzado, active la casilla ASP.NET Core hospedado.
  6. Seleccione Crear.

Adición de la biblioteca cliente de SignalR

  • En el Explorador de soluciones, haga clic con el botón derecho en el proyecto BlazorSignalRApp.Client y seleccione Administrar paquetes NuGet.
  • En el cuadro de diálogo Administrar paquetes NuGet, confirme que Origen del paquete se ha establecido en nuget.org.
  • Con Examinar seleccionado, escriba Microsoft.AspNetCore.SignalR.Client en el cuadro de búsqueda.
  • En los resultados de la búsqueda, seleccione el paquete Microsoft.AspNetCore.SignalR.Client y, después, seleccione Instalar.
  • Si aparece el cuadro de diálogo Vista previa de los cambios, seleccione Aceptar.
  • Si aparece el cuadro de diálogo Aceptación de la licencia, seleccione Acepto si está de acuerdo con los términos de la licencia.

Agregar un concentrador de SignalR

En el proyecto BlazorSignalRApp.Server, cree una carpeta Hubs (plural) y agregue la siguiente clase ChatHub (Hubs/ChatHub.cs):

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR;

namespace BlazorSignalRApp.Server.Hubs
{
    public class ChatHub : Hub
    {
        public async Task SendMessage(string user, string message)
        {
            await Clients.All.SendAsync("ReceiveMessage", user, message);
        }
    }
}

Adición de servicios y de un punto de conexión para el concentrador de SignalR

  1. En el proyecto BlazorSignalRApp.Server, abra el archivo Startup.cs.
  2. Agregue el espacio de nombres para la clase ChatHub en la parte superior del archivo:C#Copiarusing BlazorSignalRApp.Server.Hubs;
  3. Agregue servicios de middleware de compresión de respuesta y SignalR a Startup.ConfigureServices:

 

public void ConfigureServices(IServiceCollection services)
{
    services.AddSignalR();
    services.AddControllersWithViews();
    services.AddRazorPages();
    services.AddResponseCompression(opts =>
    {
        opts.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(
            new[] { "application/octet-stream" });
    });
}
  1. En Startup.Configure:
    • Use el middleware de compresión de respuesta de la parte superior de la configuración de la canalización de procesamiento.
    • Entre los puntos de conexión de los controladores y la reserva del lado cliente, agregue un punto de conexión para el concentrador.

 

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    app.UseResponseCompression();

    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
        app.UseWebAssemblyDebugging();
    }
    else
    {
        app.UseExceptionHandler("/Error");
        app.UseHsts();
    }

    app.UseHttpsRedirection();
    app.UseBlazorFrameworkFiles();
    app.UseStaticFiles();

    app.UseRouting();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorPages();
        endpoints.MapControllers();
        endpoints.MapHub<ChatHub>("/chathub");
        endpoints.MapFallbackToFile("index.html");
    });
}

Agregar código de componente de Razor para chat

  1. En el proyecto BlazorSignalRApp.Client, abra el archivo Pages/Index.razor.
  2. Reemplace el marcado con el código siguiente:

 

@page "/"
@using Microsoft.AspNetCore.SignalR.Client
@inject NavigationManager NavigationManager
@implements IAsyncDisposable

<div class="form-group">
    <label>
        User:
        <input @bind="userInput" />
    </label>
</div>
<div class="form-group">
    <label>
        Message:
        <input @bind="messageInput" size="50" />
    </label>
</div>
<button @onclick="Send" disabled="@(!IsConnected)">Send</button>

<hr>

<ul id="messagesList">
    @foreach (var message in messages)
    {
        <li>@message</li>
    }
</ul>

@code {
    private HubConnection hubConnection;
    private List<string> messages = new List<string>();
    private string userInput;
    private string messageInput;

    protected override async Task OnInitializedAsync()
    {
        hubConnection = new HubConnectionBuilder()
            .WithUrl(NavigationManager.ToAbsoluteUri("/chathub"))
            .Build();

        hubConnection.On<string, string>("ReceiveMessage", (user, message) =>
        {
            var encodedMsg = $"{user}: {message}";
            messages.Add(encodedMsg);
            StateHasChanged();
        });

        await hubConnection.StartAsync();
    }

    Task Send() =>
        hubConnection.SendAsync("SendMessage", userInput, messageInput);

    public bool IsConnected =>
        hubConnection.State == HubConnectionState.Connected;
        
    public async ValueTask DisposeAsync()
    {
        await hubConnection.DisposeAsync();
    }
}

Ejecutar la aplicación

  1. En el Explorador de soluciones, seleccione el proyecto BlazorSignalRApp.Server. Presione F5 para ejecutar la aplicación con depuración o Ctrl+F5 para ejecutarla sin depuración.
  2. Copie la dirección URL de la barra de direcciones, abra otra instancia o pestaña del explorador, y pegue la dirección URL en la barra de direcciones.
  3. Elija cualquier explorador, escriba un nombre y un mensaje, y haga clic en el botón para enviar el mensaje. El nombre y el mensaje se muestran en ambas páginas al instante: