Многопоточная работа с блокировками

Дата обновления перевода: 2023-01-16

Многопоточная работа с блокировками

Когда выполнение программы распараллелено, часть кода, изменяющая общие ресурсы не должна быть доступна одновременно нескольким процессам. Компонент Symfony Lock предоставляет механизм блокировок, чтобы убедиться, что только один процесс запускает критический участок кода в любой момент времени для предотвращения состояния гонки (race condition).

Пример, показывающий типичное использование блокировки:

1
2
3
4
5
6
7
8
9
$lock = $lockFactory->createLock('pdf-invoice-generation');
if (!$lock->acquire()) {
    return;
}

// критический участок кода
$service->method();

$lock->release();

Установка

В приложениях с Symfony Flex , запустите эту команду для установки компонента Lock:

1
$ composer require symfony/lock

Конфигурация

По умолчанию Symfony предоставляет Semaphore , когда возможно, или Flock в других случаях. Вы можете настроить это поведение используя ключ lock так:

  • YAML
  • XML
  • PHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# config/packages/lock.yaml
framework:
    lock: ~
    lock: 'flock'
    lock: 'flock:///path/to/file'
    lock: 'semaphore'
    lock: 'memcached://m1.docker'
    lock: ['memcached://m1.docker', 'memcached://m2.docker']
    lock: 'redis://r1.docker'
    lock: ['redis://r1.docker', 'redis://r2.docker']
    lock: 'zookeeper://z1.docker'
    lock: 'zookeeper://z1.docker,z2.docker'
    lock: 'zookeeper://localhost01,localhost02:2181'
    lock: 'sqlite:///%kernel.project_dir%/var/lock.db'
    lock: 'mysql:host=127.0.0.1;dbname=app'
    lock: 'pgsql:host=127.0.0.1;dbname=app'
    lock: 'pgsql+advisory:host=127.0.0.1;dbname=app'
    lock: 'sqlsrv:server=127.0.0.1;Database=app'
    lock: 'oci:host=127.0.0.1;dbname=app'
    lock: 'mongodb://127.0.0.1/app?collection=lock'
    lock: '%env(LOCK_DSN)%'

    # named locks
    lock:
        invoice: ['semaphore', 'redis://r2.docker']
        report: 'semaphore'

6.1

Поддержка CSV (например, zookeeper://localhost01,localhost02:2181) в DSN ZookeeperStore была представлена в Symfony 6.1.

Блокировка источника

Для блокировки ресурса по умолчанию, автоподключите Lock используя LockInterface (id сервиса lock):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// src/Controller/PdfController.php
namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Lock\LockFactory;

class PdfController extends AbstractController
{
    #[Route('/download/terms-of-use.pdf')]
    public function downloadPdf(LockFactory $factory, MyPdfGeneratorService $pdf)
    {
        $lock = $factory->createLock('pdf-creation');
        $lock->acquire(true);

        // сложные расчёты
        $myPdf = $pdf->getOrCreatePdf();

        $lock->release();

        // ...
    }
}

Caution

Тот же экземпляр LockInterface не будет блокирован, если вызвать acquire несколько раз внутри того же процесса. Когда несколько сервисов используют тот же lock, внедрите LockFactory, чтобы создавать новый экземпляр блокировщика для каждого сервиса.

Блокирование динамического ресурса

Иногда приложение может разделить ресурс на небольшие части, чтобы блокировать только малую часть процесса, и позволять остальным обрабатываться. В предудущем примере $pdf->getOrCreatePdf() блокировалось для всех, а теперь давайте посмотрим, как заблокировать $pdf->getOrCreatePdf($version) только для процессов, которым нужен доступ к той же $version:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// src/Controller/PdfController.php
namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Lock\LockFactory;

class PdfController extends AbstractController
{
    #[Route('/download/{version}/terms-of-use.pdf')]
    public function downloadPdf($version, LockFactory $lockFactory, MyPdfGeneratorService $pdf)
    {
        $lock = $lockFactory->createLock('pdf-creation-'.$version);
        $lock->acquire(true);

        // сложные рассчёты
        $myPdf = $pdf->getOrCreatePdf($version);

        $lock->release();

        // ...
    }
}

Именованные блокировки

Если приложению нужен другой тип хранилища для разных блокировок, Symfony предоставляет именованную блокировку :

  • YAML
  • XML
  • PHP
1
2
3
4
5
# config/packages/lock.yaml
framework:
    lock:
        invoice: ['semaphore', 'redis://r2.docker']
        report: 'semaphore'

Псевдоним автомонтирования создаётся для каждой именованной блокировки с именем, используя версия camel case её имени с суффиксом LockFactory.

Например, блокировка invoice может быть внедрена путём именования аргумента $invoiceLockFactory и добавления подсказки LockFactory:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// src/Controller/PdfController.php
namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Lock\LockFactory;

class PdfController extends AbstractController
{
    #[Route('/download/terms-of-use.pdf')]
    public function downloadPdf(LockFactory $invoiceLockFactory, MyPdfGeneratorService $pdf)
    {
        // ...
    }
}