Пакет sync: мьютексы, пулы и одноразовая инициализация
Пакет sync: мьютексы, пулы и одноразовая инициализация
Каналы - не единственный способ синхронизации. Пакет sync даёт низкоуровневые примитивы.
sync.Mutex
type SafeCounter struct {
mu sync.Mutex
v map[string]int
}
func (c *SafeCounter) Inc(key string) {
c.mu.Lock()
defer c.mu.Unlock()
c.v[key]++
}
func (c *SafeCounter) Value(key string) int {
c.mu.Lock()
defer c.mu.Unlock()
return c.v[key]
}
<?php
declare(strict_types=1);
namespace App\Concurrency;
use Symfony\Component\Lock\LockFactory;
final readonly class SafeCounter
{
public function __construct(
private LockFactory $lockFactory,
private CounterStorageInterface $storage,
) {}
public function inc(string $key): void
{
$lock = $this->lockFactory->createLock(\sprintf('counter:%s', $key));
$lock->acquire(true); // блокирующий
try {
$this->storage->increment($key);
} finally {
$lock->release();
}
}
}
В PHP-FPM in-process mutex не нужен: каждый request - отдельный процесс, shared memory между запросами нет. Реальная задача - синхронизация между процессами/воркерами/серверами. Промышленный референс -
symfony/lockс file/Redis/Postgres backend (distributed lock). Скорость - миллисекунды, а не наносекунды, как уsync.Mutex.
Конфигурация Redis-lock:
framework:
lock:
counter: '%env(REDIS_URL)%' # redis://redis:6379
Альтернатива - БД-уровень через Postgres advisory lock или SELECT ... FOR UPDATE в транзакции.
sync.RWMutex
Когда чтений много, а записей мало - RWMutex эффективнее.
type Cache struct {
mu sync.RWMutex
data map[string]string
}
func (c *Cache) Get(key string) (string, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
val, ok := c.data[key]
return val, ok
}
func (c *Cache) Set(key, value string) {
c.mu.Lock()
defer c.mu.Unlock()
c.data[key] = value
}
<?php
declare(strict_types=1);
namespace App\Concurrency;
use Symfony\Component\Cache\Adapter\RedisAdapter;
use Symfony\Component\Lock\LockFactory;
use Symfony\Contracts\Cache\ItemInterface;
final readonly class DistributedCache
{
public function __construct(
private RedisAdapter $cache,
private LockFactory $lockFactory,
) {}
public function get(string $key): ?string
{
return $this->cache->get($key, static fn (ItemInterface $item): ?string => null);
}
public function set(string $key, string $value): void
{
$lock = $this->lockFactory->createLock(\sprintf('cache:%s', $key));
$lock->acquire(true);
try {
$item = $this->cache->getItem($key);
$item->set($value);
$this->cache->save($item);
} finally {
$lock->release();
}
}
}
symfony/lockне различает read/write локи - это всегда эксклюзивная блокировка. В монопроцессном PHP-FPM RWMutex и не нужен: shared memory нет. Если задача в распределённой системе и read-heavy - используют cache layer (symfony/cacheс Redis), писатели идут через lock, читатели читают из кеша без блокировки.
sync.Once
Гарантирует, что функция вызовется ровно один раз.
var (
instance *Database
once sync.Once
)
func GetDB() *Database {
once.Do(func() {
instance = connectToDatabase()
})
return instance
}
<?php
declare(strict_types=1);
namespace App\Concurrency;
use Doctrine\DBAL\Connection;
// сервис shared по умолчанию - инициализируется один раз на процесс
final readonly class DatabaseProvider
{
public function __construct(private Connection $connection) {}
public function get(): Connection
{
return $this->connection;
}
}
В PHP-FPM «однократная инициализация» автоматическая на уровне DI-контейнера: Symfony создаёт singleton-сервис один раз в lifecycle процесса. Эквивалент
sync.Once- service definition с дефолтнымshared: true(по умолчанию). Между запросами FPM состояние не разделяет, кеш bootcache решает холодный старт.
Для lazy-init внутри одного request - private property + ??=:
<?php
declare(strict_types=1);
namespace App\Concurrency;
final class LazyResource
{
private ?\PDO $pdo = null;
public function __construct(private readonly string $dsn) {}
public function pdo(): \PDO
{
return $this->pdo ??= new \PDO($this->dsn);
}
}
sync.Map
Оптимизированный для конкурентного доступа map (как устроен изнутри). Полезен когда ключи стабильны или при disjoint access.
var cache sync.Map
cache.Store("key", "value")
val, ok := cache.Load("key")
if ok {
fmt.Println(val.(string))
}
cache.Range(func(key, value any) bool {
fmt.Printf("%v: %v\n", key, value)
return true // false чтобы остановить
})
sync.Pool
Повторное использование объектов для снижения нагрузки на GC.
var bufferPool = sync.Pool{
New: func() any {
return new(bytes.Buffer)
},
}
func processRequest() {
buf := bufferPool.Get().(*bytes.Buffer)
defer func() {
buf.Reset()
bufferPool.Put(buf)
}()
buf.WriteString("processing...")
// используем buf
}
<?php
declare(strict_types=1);
namespace App\Concurrency;
final class ConnectionPool
{
/** @var list<\PDO> */
private array $available = [];
public function __construct(
private readonly string $dsn,
private readonly int $maxSize = 10,
) {}
public function acquire(): \PDO
{
return \array_pop($this->available) ?? new \PDO($this->dsn);
}
public function release(\PDO $pdo): void
{
if (\count($this->available) < $this->maxSize) {
$this->available[] = $pdo;
}
}
}
Прямого аналога
sync.Poolв PHP нет: между запросами FPM зачистит память, внутри одного request объект-аллокации редко становятся узким местом. Если и нужен «пул объектов», то для тяжёлых ресурсов (DB-соединения, gRPC клиенты) - это решается на уровне DI-контейнера (singleton-сервисы) или connection pooling в Doctrine. Для длинноживущих воркеров (Swoole / ReactPHP / Symfony Messenger) можно делать пул вручную через массив.
sync.Cond (редко нужен)
var (
mu sync.Mutex
cond = sync.NewCond(&mu)
ready bool
)
// Ожидающая горутина
go func() {
mu.Lock()
for !ready {
cond.Wait() // атомарно: Unlock → sleep → Lock
}
fmt.Println("ready!")
mu.Unlock()
}()
// Сигнализирующая горутина
mu.Lock()
ready = true
cond.Signal() // или cond.Broadcast() для всех
mu.Unlock()