krzysztofzylka / event-loop
Installs: 3
Dependents: 1
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 0
pkg:composer/krzysztofzylka/event-loop
Requires
- php: >=8.3
Requires (Dev)
- phpunit/phpunit: ^12.4
README
Lekka, szybka i w pełni asynchroniczna pętla zdarzeń napisana w czystym PHP 8.3+, inspirowana ReactPHP oraz Node.js.
Bazuje na stream_select(), dzięki czemu działa bez zewnętrznych rozszerzeń, a teraz dodatkowo potrafi wykonywać kosztowne zadania w osobnych procesach.
Wymagania
- PHP >= 8.3
- Opcjonalnie
ext-pcntl– wymagane do timerów procesowych (GNU/Linux, macOS)
Instalacja
composer require krzysztofzylka/event-loop
Najważniejsze funkcje
- Asynchroniczne I/O oparte na
stream_select(). - Timery jednorazowe i cykliczne z priorytetami (
HIGH,NORMAL,LOW). - Future ticks – natychmiastowe callbacki uruchamiane po bieżącej iteracji pętli.
- Obsługa strumieni odczytu i zapisu.
- Obsługa sygnałów systemowych (
SIGINT,SIGTERMitd.). - Timery procesowe – uruchamianie ciężkich zadań w osobnych procesach (jednorazowo i cyklicznie).
- Konfigurowalne sterowniki pętli (
LoopConfig) oraz tagowanie timerów i strumieni. - Brak zewnętrznych zależności – działa nawet na shared hostingu.
Szybki start
<?php require __DIR__ . '/vendor/autoload.php'; use Async\Loop; Loop::futureTick(fn() => print "Start loop\n"); Loop::addTimer(1, fn() => print "Minęła 1 sekunda\n"); Loop::addPeriodicTimer(2, fn() => print "Ping co 2 sekundy\n"); Loop::addTimer(6, fn() => Loop::stop()); Loop::run();
Wynik:
Start loop
Minęła 1 sekunda
Ping co 2 sekundy
Ping co 2 sekundy
Timery procesowe
Procesowe timery pozwalają uruchamiać kod w osobnym procesie i odebrać wynik po zakończeniu pracy – bez blokowania głównej pętli. Nadadzą się do zadań CPU-intensywnych, integracji ze starymi bibliotekami wykonującymi sleep() itp.
use Async\Loop; use Async\ProcessTimerException; use Async\Timer; Loop::addProcessTimer( 0.0, fn() => ['pid' => getmypid(), 'checksum' => array_sum(range(1, 1_000_000))], function (array $result, Timer $timer, int $pid): void { printf("Child %d finished with checksum %d\n", $pid, $result['checksum']); }, function (ProcessTimerException $exception): void { fprintf(STDERR, "Process failed: %s\n", $exception->getMessage()); } ); Loop::run();
Właściwości
Loop::addProcessTimer()– jednorazowe zadanie.Loop::addPeriodicProcessTimer()– zadanie cykliczne (uruchamia nowe dziecko przy każdej iteracji).- W callbacku możesz otrzymać wynik (
$onResult) lub błąd ($onError). - Włącza się tylko, jeśli dostępne jest
pcntl_fork()orazpcntl_waitpid()– w innym wypadku rzucany jestRuntimeException. - Dane z procesów buforowane są w katalogach tymczasowych (kolejno):
/tmp,<projekt>/tmp,<projekt>/.async-process-*.
Pełny przykład znajdziesz w examples/process_timers.php.
Konfiguracja i sterowniki
Domyślnie pętla korzysta z drivera opartego na stream_select(). Możesz jednak utworzyć własną instancję z innymi parametrami (np. zmienionym interwałem odpytywania procesów) i przekazać ją do Loop::configure():
use Async\Loop; use Async\LoopConfig; $config = LoopConfig::default() ->withProcessPollInterval(0.005) ->withIdleSleepUsec(500); Loop::configure($config); Loop::run();
Jeżeli napiszesz własny driver (np. oparty o epoll), zaimplementuj Async\Driver\LoopDriverInterface i przekaż go do konfiguracji metodą withDriver(...).
Pula procesów (prefork)
Pula procesów pozwala utrzymać stałą liczbę pracowników uruchomionych z wyprzedzeniem i zlecać im zadania bez kosztu fork() przy każdej operacji.
use Async\Loop; $pool = Loop::createProcessPool(2); $pool->submit('strtoupper', ['hello'], fn($value) => print("Upper: $value\n")); $pool->submit('strlen', ['async'], fn($value) => print("Length: $value\n")); Loop::addTimer(0.5, function () use ($pool) { $pool->shutdown(); Loop::stop(); }); Loop::run();
Każde zadanie wykonuje statyczny callable (function lub [ClassName::class, 'method']) z podanymi argumentami. Wyniki oraz błędy można obsłużyć przez przekazane callbacki. Przykład znajdziesz w examples/process_pool.php, a test obciążeniowy w benchmarks/process_pool.php.
Metryki
Pętla udostępnia podstawowe statystyki przez Loop::getMetrics() (liczba timerów, strumieni, procesów, średni czas ticku itd.):
$metrics = Loop::getMetrics(); print_r($metrics);
Metryki można wykorzystać np. do monitoringu lub prostej diagnostyki w trakcie działania aplikacji.
API klasy Loop
| Metoda | Opis |
|---|---|
Loop::addTimer(float $sec, callable $cb, Priority $priority = Priority::NORMAL) |
Jednorazowy timer |
Loop::addPeriodicTimer(float $sec, callable $cb, Priority $priority = Priority::NORMAL) |
Timer cykliczny |
Loop::addProcessTimer(float $sec, callable $process, ?callable $onResult = null, ?callable $onError = null, Priority $priority = Priority::NORMAL) |
Jednorazowe zadanie w podprocesie |
Loop::addPeriodicProcessTimer(float $sec, callable $process, ?callable $onResult = null, ?callable $onError = null, Priority $priority = Priority::NORMAL) |
Cykliczne zadanie w podprocesie |
Loop::cancelTimer(int $id) |
Anuluje timer (również procesowy) |
Loop::futureTick(callable $cb) |
Callback po zakończeniu bieżącej iteracji |
Loop::addReadStream($stream, callable $cb) |
Rejestruje strumień do odczytu |
Loop::addWriteStream($stream, callable $cb) |
Rejestruje strumień do zapisu |
Loop::addSignal(int $signal, callable $cb) |
Obsługa sygnałów systemowych |
Loop::run() |
Uruchamia pętlę zdarzeń |
Loop::stop() |
Zatrzymuje pętlę |
Loop::configure(?LoopConfig $config = null) |
Tworzy nową instancję pętli na podstawie konfiguracji |
Loop::tagTimer(int $id, string ...$tags) |
Dodaje tagi do timera |
Loop::cancelTimersByTag(string $tag) |
Anuluje wszystkie timery mające dany tag |
Loop::tagReadStream($stream, string ...$tags) |
Oznacza strumień do odczytu tagami |
Loop::tagWriteStream($stream, string ...$tags) |
Oznacza strumień do zapisu tagami |
Loop::removeStreamsByTag(string $tag) |
Usuwa wszystkie strumienie przypisane do tagu |
Priorytety timerów
use Async\Loop; use Async\Priority; Loop::addTimer(1, fn() => print "[LOW] niski\n", Priority::LOW); Loop::addTimer(1, fn() => print "[NORMAL] normalny\n", Priority::NORMAL); Loop::addTimer(1, fn() => print "[HIGH] wysoki\n", Priority::HIGH); Loop::addTimer(2, fn() => Loop::stop()); Loop::run();
Wynik:
[HIGH] wysoki
[NORMAL] normalny
[LOW] niski
Priorytet decyduje o kolejności wykonania timerów zaplanowanych na ten sam moment.
Echo serwer TCP
use Async\Loop; $server = stream_socket_server("tcp://127.0.0.1:9000", $errno, $errstr); if (!$server) { die("Błąd: $errstr ($errno)\n"); } echo "Serwer działa: 127.0.0.1:9000\n"; Loop::addReadStream($server, function ($server) { $client = @stream_socket_accept($server, 0); if ($client) { echo "[+] Nowe połączenie\n"; stream_set_blocking($client, false); Loop::addReadStream($client, function ($client) { $data = fread($client, 1024); if ($data === '' || $data === false) { Loop::removeReadStream($client); fclose($client); echo "[-] Rozłączono\n"; } else { fwrite($client, "Echo: $data"); } }); } }); Loop::addSignal(SIGINT, fn() => print("\nSIGINT — zatrzymuję serwer\n") || Loop::stop()); Loop::run();
Test:
telnet 127.0.0.1 9000
> hello
Echo: hello
Przykłady
Katalog examples/ zawiera gotowe scenariusze:
process_timers.php– demonstracja timerów procesowych (jednorazowy + cykliczny).configuration.php– uruchomienie pętli z niestandardowymLoopConfig.tags.php– praca z tagami timerów i strumieni.process_pool.php– użycie puli procesów prefork.
Uruchomienie:
php examples/process_timers.php php examples/configuration.php php examples/tags.php php examples/process_pool.php
Testy
composer install vendor/bin/phpunit
Testy dotyczące timerów procesowych wymagają dostępności pcntl.