¿Cómo se maneja la memoria compartida entre threads?

¿Cómo se maneja la memoria compartida entre threads?

La programación en paralelo es un tema fundamental en la informática, ya que permite aprovechar al máximo los recursos de los procesadores modernos. Uno de los conceptos clave en este ámbito es el manejo de la memoria compartida entre threads. En el lenguaje ZIG, este tema se aborda de manera explícita y segura, gracias a sus características de programación sistémica.

Introducción a la memoria compartida

En un programa multihilo, cada thread tiene su propio stack y su propia memoria local. Sin embargo, en muchos casos, es necesario que los threads compartan datos entre sí. Esto se logra mediante el uso de variables globales o estructuras de datos compartidas. El problema surge cuando varios threads intentan acceder y modificar estos datos simultáneamente, lo que puede dar lugar a situaciones de carrera y resultados impredecibles.

Mecanismos de sincronización en ZIG

ZIG proporciona varios mecanismos para sincronizar el acceso a la memoria compartida entre threads. A continuación, se presentan algunos de los más importantes:

  • Mutexes: Un mutex (abreviatura de “mutual exclusion”) es un objeto que permite a un solo thread acceder a una sección crítica del código. Los demás threads deben esperar a que el mutex sea liberado antes de poder acceder a la sección crítica.
  • Condicionales: Un condicional es un objeto que permite a un thread esperar a que se cumpla una condición específica antes de continuar ejecutando el código.
  • Barreas: Una barrera es un objeto que permite a un grupo de threads esperar a que todos hayan completado una tarea específica antes de continuar ejecutando el código.

Ejemplo de uso de mutexes en ZIG

A continuación, se presenta un ejemplo de código ZIG que demuestra el uso de un mutex para sincronizar el acceso a una variable compartida:

const std = @import("std");

pub fn main() !void {
  var mutex = std.Thread.Mutex.init();
  defer mutex.destroy();

  var contador: i32 = 0;

  var thread1 = try std.Thread.spawn(try std.heap.page_allocator.alloc(u8, 1024), struct {
    fn runner(mutex: *std.Thread.Mutex, contador: *i32) callconv(.C) void {
      for ([_]i32{ 0 } ** 100000) {
        mutex.lock();
        defer mutex.unlock();
        contador.* += 1;
      }
    }
  }.runner, .{&mutex, &contador});

  var thread2 = try std.Thread.spawn(try std.heap.page_allocator.alloc(u8, 1024), struct {
    fn runner(mutex: *std.Thread.Mutex, contador: *i32) callconv(.C) void {
      for ([_]i32{ 0 } ** 100000) {
        mutex.lock();
        defer mutex.unlock();
        contador.* += 1;
      }
    }
  }.runner, .{&mutex, &contador});

  thread1.join();
  thread2.join();

  std.debug.print("Valor final del contador: {d}\n", .{contador});
}

En este ejemplo, se crean dos threads que intentan incrementar una variable compartida `contador` 100.000 veces cada uno. Sin el uso de un mutex, los resultados serían impredecibles debido a la concurrencia. Sin embargo, gracias al mutex, solo un thread puede acceder a la variable en cada momento, lo que garantiza la corrección del resultado final.

Conclusión

En resumen, el manejo de la memoria compartida entre threads es un tema crucial en la programación en paralelo. ZIG proporciona mecanismos de sincronización como mutexes, condicionales y barreas para garantizar la seguridad y corrección de los programas multihilo. A través de ejemplos prácticos como el presentado anteriormente, podemos ver cómo estos mecanismos pueden ser utilizados para escribir programas eficientes y fiables.

Comments

No comments yet. Why don’t you start the discussion?

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *