laravelperformanceconcurrenciaphp

Concurrencia en Laravel 13: Executa Tareas Paralelas Eficientemente

Introducción

La concurrencia es uno de los desafíos más importantes en el desarrollo web moderno. Cuando necesitas ejecutar múltiples operaciones sin bloquear el flujo de tu aplicación, Laravel ofrece soluciones elegantes y poderosas.

Laravel 13 introduce mejoras significativas en el gestor de concurrencia, permitiéndote ejecutar tareas paralelas de forma segura y eficiente. Ya sea que necesites realizar múltiples llamadas a APIs, procesar datos en lotes o ejecutar operaciones de base de datos complejas, el sistema de concurrencia de Laravel te permite hacerlo sin complicaciones.

En esta guía, exploraremos cómo implementar concurrencia en tus proyectos Laravel, cuándo usarla y las mejores prácticas para evitar problemas comunes.

¿Qué es la Concurrencia en Laravel?

La concurrencia en Laravel se refiere a la capacidad de ejecutar múltiples tareas simultáneamente sin que una bloquee a la otra. Es importante distinguir entre:

Concurrencia: Múltiples tareas se ejecutan intercalándose (una después de otra muy rápidamente).

Paralelismo: Múltiples tareas se ejecutan realmente al mismo tiempo en diferentes núcleos del procesador.

Laravel abstrae estos detalles y proporciona una API unificada para trabajar con ambos escenarios. El framework detecta automáticamente qué driver usar basándose en tu configuración.

Drivers de Concurrencia Disponibles

Driver Sync (Por defecto)

Es el más simple pero también el más limitado. Ejecuta todas las tareas secuencialmente, una tras otra.

use Illuminate\Support\Concurrent;

$results = Concurrent::run([
    'user' => fn () => User::find(1),
    'posts' => fn () => Post::where('user_id', 1)->get(),
    'comments' => fn () => Comment::where('user_id', 1)->get(),
]);

// Todos se ejecutan secuencialmente
dd($results);

Driver Process

Utiliza procesos de PHP para ejecutar tareas en paralelo real. Es la opción más poderosa para concurrencia verdadera.

// config/concurrency.php
'default' => env('CONCURRENCY_DRIVER', 'process'),

'drivers' => [
    'process' => [
        'driver' => 'process',
    ],
]

Driver Fibers (PHP 8.1+)

Proporciona una forma más eficiente de manejar concurrencia usando Fibers, con menos sobrecarga que los procesos.

// config/concurrency.php
'drivers' => [
    'fiber' => [
        'driver' => 'fiber',
    ],
]

Implementación Básica de Concurrencia

Ejecutar Tareas Paralelas Simples

El método Concurrent::run() es la forma más directa de ejecutar múltiples tareas en paralelo:

<?php

namespace App\Http\Controllers;

use Illuminate\Support\Concurrent;
use App\Models\User;
use App\Models\Post;

class DashboardController extends Controller
{
    public function index()
    {
        $results = Concurrent::run([
            'user' => fn () => User::find(auth()->id()),
            'recent_posts' => fn () => Post::latest()
                ->limit(10)
                ->get(),
            'total_likes' => fn () => Post::where('user_id', auth()->id())
                ->sum('likes_count'),
            'followers' => fn () => User::find(auth()->id())
                ->followers()
                ->count(),
        ]);

        return view('dashboard', $results);
    }
}

En este ejemplo, todas cuatro operaciones se ejecutan en paralelo, no secuencialmente. El tiempo total será aproximadamente el tiempo de la operación más lenta, no la suma de todas.

Usando try-catch en Tareas Paralelas

Es crucial manejar excepciones correctamente en concurrencia:

use Illuminate\Support\Concurrent;
use Illuminate\Concurrency\Manager;

$results = Concurrent::run([
    'primary_data' => function () {
        try {
            return $this->fetchFromPrimaryAPI();
        } catch (\Exception $e) {
            return ['error' => $e->getMessage()];
        }
    },
    'secondary_data' => function () {
        try {
            return $this->fetchFromSecondaryAPI();
        } catch (\Exception $e) {
            return ['error' => $e->getMessage()];
        }
    },
]);

// Procesa los resultados sabiendo que puede haber errores
foreach ($results as $key => $result) {
    if (isset($result['error'])) {
        Log::warning("Task $key failed: {$result['error']}");
    }
}

Casos de Uso Prácticos

Caso 1: Enriquecer Datos de Múltiples Fuentes

Imagina que necesitas construir un perfil de usuario desde múltiples APIs:

<?php

namespace App\Services;

use Illuminate\Support\Concurrent;
use Illuminate\Support\Facades\Http;

class UserProfileService
{
    public function enrichUserProfile($userId)
    {
        $results = Concurrent::run([
            'github' => fn () => $this->fetchGitHubProfile($userId),
            'twitter' => fn () => $this->fetchTwitterProfile($userId),
            'linkedin' => fn () => $this->fetchLinkedInProfile($userId),
            'activity' => fn () => $this->fetchUserActivity($userId),
        ]);

        return new UserProfile(
            github: $results['github'],
            twitter: $results['twitter'],
            linkedin: $results['linkedin'],
            activity: $results['activity'],
        );
    }

    private function fetchGitHubProfile($userId)
    {
        return Http::get("https://api.github.com/users/$userId")->json();
    }

    private function fetchTwitterProfile($userId)
    {
        return Http::withToken(config('services.twitter.bearer'))
            ->get("https://api.twitter.com/2/users/$userId")
            ->json();
    }

    private function fetchLinkedInProfile($userId)
    {
        return Http::withToken(config('services.linkedin.token'))
            ->get("https://api.linkedin.com/v2/me")
            ->json();
    }

    private function fetchUserActivity($userId)
    {
        return auth()->user()
            ->activities()
            ->latest()
            ->limit(50)
            ->get();
    }
}

Caso 2: Procesamiento de Lotes de Datos

<?php

namespace App\Jobs;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Support\Concurrent;
use App\Models\Product;

class ProcessProductAnalytics implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable;

    public function handle()
    {
        $products = Product::whereNull('analyzed_at')->get();

        // Procesa en lotes de 10 productos en paralelo
        $products->chunk(10)->each(function ($chunk) {
            $results = Concurrent::run(
                $chunk->mapWithKeys(fn ($product) => [
                    "product_{$product->id}" => fn () => $this->analyzeProduct($product),
                ])->toArray()
            );

            // Guarda resultados
            foreach ($results as $key => $analysis) {
                $productId = explode('_', $key)[1];
                Product::find($productId)->update([
                    'analysis' => $analysis,
                    'analyzed_at' => now(),
                ]);
            }
        });
    }

    private function analyzeProduct(Product $product)
    {
        return [
            'sentiment' => $this->analyzeSentiment($product->description),
            'keywords' => $this->extractKeywords($product->description),
            'category' => $this->categorizeProduct($product),
            'price_comparison' => $this->comparePrices($product),
        ];
    }

    private function analyzeSentiment($text)
    {
        // Implementar análisis de sentimiento
        return 'positive';
    }

    private function extractKeywords($text)
    {
        // Extraer palabras clave
        return explode(' ', $text);
    }

    private function categorizeProduct(Product $product)
    {
        // Categorizar producto
        return 'electronics';
    }

    private function comparePrices(Product $product)
    {
        // Comparar precios con competidores
        return 100;
    }
}

Caso 3: Reportes Complejos

<?php

namespace App\Services;

use Illuminate\Support\Concurrent;
use App\Models\Order;
use App\Models\Customer;
use App\Models\Product;

class ReportService
{
    public function generateMonthlyReport($year, $month)
    {
        $results = Concurrent::run([
            'sales' => fn () => $this->calculateSales($year, $month),
            'top_products' => fn () => $this->getTopProducts($year, $month),
            'customer_stats' => fn () => $this->getCustomerStatistics($year, $month),
            'revenue_by_category' => fn () => $this->getRevenueByCategory($year, $month),
            'growth_metrics' => fn () => $this->calculateGrowthMetrics($year, $month),
        ]);

        return $results;
    }

    private function calculateSales($year, $month)
    {
        return Order::whereYear('created_at', $year)
            ->whereMonth('created_at', $month)
            ->sum('total');
    }

    private function getTopProducts($year, $month)
    {
        return Product::whereHas('orders', function ($query) use ($year, $month) {
            $query->whereYear('created_at', $year)
                ->whereMonth('created_at', $month);
        })
        ->withCount(['orders' => function ($query) use ($year, $month) {
            $query->whereYear('created_at', $year)
                ->whereMonth('created_at', $month);
        }])
        ->orderByDesc('orders_count')
        ->limit(10)
        ->get();
    }

    private function getCustomerStatistics($year, $month)
    {
        return [
            'new' => Customer::whereYear('created_at', $year)
                ->whereMonth('created_at', $month)
                ->count(),
            'active' => Customer::whereHas('orders', function ($query) use ($year, $month) {
                $query->whereYear('created_at', $year)
                    ->whereMonth('created_at', $month);
            })->count(),
        ];
    }

    private function getRevenueByCategory($year, $month)
    {
        return Product::join('orders', 'products.id', '=', 'orders.product_id')
            ->whereYear('orders.created_at', $year)
            ->whereMonth('orders.created_at', $month)
            ->groupBy('products.category')
            ->selectRaw('products.category, SUM(orders.total) as revenue')
            ->get();
    }

    private function calculateGrowthMetrics($year, $month)
    {
        $currentMonth = Order::whereYear('created_at', $year)
            ->whereMonth('created_at', $month)
            ->sum('total');

        $previousMonth = Order::whereYear('created_at', $year)
            ->whereMonth('created_at', $month - 1)
            ->sum('total');

        return [
            'growth_percentage' => (($currentMonth - $previousMonth) / $previousMonth) * 100,
            'current' => $currentMonth,
            'previous' => $previousMonth,
        ];
    }
}

Mejores Prácticas y Consideraciones

1. Evita Compartir Estado

Cada tarea debe ser independiente. No compartas variables mutables entre tareas:

// ❌ MAL - Estado compartido
$counter = 0;
Concurrent::run([
    'task1' => fn () => $counter++, // Problema
    'task2' => fn () => $counter++, // Problema
]);

// ✅ BIEN - Cada tarea es independiente
Concurrent::run([
    'result1' => fn () => $this->processData($data1),
    'result2' => fn () => $this->processData($data2),
]);

2. Usa Tipos Adecuadamente

Laravel 13 mejora el soporte para tipos enum en el gestor de concurrencia:

enum TaskType
{
    case Database;
    case API;
    case Cache;
}

$results = Concurrent::run([
    TaskType::Database->value => fn () => DB::query(),
    TaskType::API->value => fn () => Http::get(),
    TaskType::Cache->value => fn () => Cache::get(),
]);

3. Considera la Sobrecarga

La concurrencia no siempre es más rápida. Para operaciones muy rápidas, el overhead de crear procesos o fibers puede superar los beneficios:

// ✅ Bueno para concurrencia
- Llamadas a APIs externas
- Operaciones de base de datos pesadas
- Procesamiento de imágenes
- Análisis de datos

// ❌ No recomendado para concurrencia
- Operaciones simples de cache
- Búsquedas en memoria
- Cálculos matemáticos simples

4. Monitorea y Optimiza

$start = microtime(true);

$results = Concurrent::run([
    'task1' => fn () => $this->heavyOperation1(),
    'task2' => fn () => $this->heavyOperation2(),
]);

$elapsed = microtime(true) - $start;

Log::info("Concurrent tasks completed in {$elapsed}s");

Integración con Queues

Para tareas más complejas, integra concurrencia con el sistema de colas:

<?php

namespace App\Jobs;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Support\Concurrent;
use App\Models\Report;

class GenerateReportConcurrently implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable;

    public function __construct(public Report $report)
    {
    }

    public function handle()
    {
        $results = Concurrent::run([
            'section1' => fn () => $this->generateSection1(),
            'section2' => fn () => $this->generateSection2(),
            'section3' => fn () => $this->generateSection3(),
        ]);

        $this->report->update([
            'content' => $results,
            'generated_at' => now(),
        ]);
    }

    private function generateSection1() { }
    private function generateSection2() { }
    private function generateSection3() { }
}

Debugging y Troubleshooting

Problema: Las tareas se ejecutan secuencialmente

Verifica tu configuración de