Symfony
sponsored by SensioLabs
Menu
  • About
  • Documentation
  • Screencasts
  • Cloud
  • Certification
  • Community
  • Businesses
  • News
  • Download
  1. Home
  2. Documentation
  3. Dealing with Concurrency with Locks
  • Documentation
  • Book
  • Reference
  • Bundles
  • Cloud
Search by Algolia

Table of Contents

  • Installing
  • Configuring
  • Locking a Resource
  • Locking a Dynamic Resource
  • Naming Locks
  • Blocking Store

Dealing with Concurrency with Locks

Edit this page

Dealing with Concurrency with Locks

When a program runs concurrently, some part of code which modify shared resources should not be accessed by multiple processes at the same time. Symfony's Lock component provides a locking mechanism to ensure that only one process is running the critical section of code at any point of time to prevent race conditions from happening.

The following example shows a typical usage of the lock:

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

// critical section of code
$service->method();

$lock->release();

Installing

In applications using Symfony Flex, run this command to install the Lock component:

1
$ composer require symfony/lock

Configuring

By default, Symfony provides a Semaphore when available, or a Flock otherwise. You can configure this behavior by using the lock key like:

  • 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
# 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: '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'
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
<!-- config/packages/lock.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:framework="http://symfony.com/schema/dic/symfony"
    xsi:schemaLocation="http://symfony.com/schema/dic/services
        https://symfony.com/schema/dic/services/services-1.0.xsd
        http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd">

    <framework:config>
        <framework:lock>
            <framework:resource>flock</framework:resource>

            <framework:resource>flock:///path/to/file</framework:resource>

            <framework:resource>semaphore</framework:resource>

            <framework:resource>memcached://m1.docker</framework:resource>

            <framework:resource>memcached://m1.docker</framework:resource>
            <framework:resource>memcached://m2.docker</framework:resource>

            <framework:resource>redis://r1.docker</framework:resource>

            <framework:resource>redis://r1.docker</framework:resource>
            <framework:resource>redis://r2.docker</framework:resource>

            <framework:resource>zookeeper://z1.docker</framework:resource>

            <framework:resource>zookeeper://z1.docker,z2.docker</framework:resource>

            <framework:resource>sqlite:///%kernel.project_dir%/var/lock.db</framework:resource>

            <framework:resource>mysql:host=127.0.0.1;dbname=app</framework:resource>

            <framework:resource>pgsql:host=127.0.0.1;dbname=app</framework:resource>

            <framework:resource>pgsql+advisory:host=127.0.0.1;dbname=app</framework:resource>

            <framework:resource>sqlsrv:server=127.0.0.1;Database=app</framework:resource>

            <framework:resource>oci:host=127.0.0.1;dbname=app</framework:resource>

            <framework:resource>mongodb://127.0.0.1/app?collection=lock</framework:resource>

            <framework:resource>%env(LOCK_DSN)%</framework:resource>

            <!-- named locks -->
            <framework:resource name="invoice">semaphore</framework:resource>
            <framework:resource name="invoice">redis://r2.docker</framework:resource>
            <framework:resource name="report">semaphore</framework:resource>
        </framework:lock>
    </framework:config>
</container>
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
27
28
// config/packages/lock.php
use Symfony\Config\FrameworkConfig;

return static function (FrameworkConfig $framework) {
    $framework->lock()
        ->resource('default', ['flock'])
        ->resource('default', ['flock:///path/to/file'])
        ->resource('default', ['semaphore'])
        ->resource('default', ['memcached://m1.docker'])
        ->resource('default', ['memcached://m1.docker', 'memcached://m2.docker'])
        ->resource('default', ['redis://r1.docker'])
        ->resource('default', ['redis://r1.docker', 'redis://r2.docker'])
        ->resource('default', ['zookeeper://z1.docker'])
        ->resource('default', ['zookeeper://z1.docker,z2.docker'])
        ->resource('default', ['sqlite:///%kernel.project_dir%/var/lock.db'])
        ->resource('default', ['mysql:host=127.0.0.1;dbname=app'])
        ->resource('default', ['pgsql:host=127.0.0.1;dbname=app'])
        ->resource('default', ['pgsql+advisory:host=127.0.0.1;dbname=app'])
        ->resource('default', ['sqlsrv:server=127.0.0.1;Database=app'])
        ->resource('default', ['oci:host=127.0.0.1;dbname=app'])
        ->resource('default', ['mongodb://127.0.0.1/app?collection=lock'])
        ->resource('default', [env('LOCK_DSN')])

        // named locks
        ->resource('invoice', ['semaphore', 'redis://r2.docker'])
        ->resource('report', ['semaphore'])
    ;
};

Locking a Resource

To lock the default resource, autowire the lock factory using LockFactory:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 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);

        // heavy computation
        $myPdf = $pdf->getOrCreatePdf();

        $lock->release();

        // ...
    }
}

Caution

The same instance of LockInterface won't block when calling acquire multiple times inside the same process. When several services use the same lock, inject the LockFactory instead to create a separate lock instance for each service.

Locking a Dynamic Resource

Sometimes the application is able to cut the resource into small pieces in order to lock a small subset of processes and let others through. The previous example showed how to lock the $pdf->getOrCreatePdf() call for everybody, now let's see how to lock a $pdf->getOrCreatePdf($version) call only for processes asking for the same $version:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 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);

        // heavy computation
        $myPdf = $pdf->getOrCreatePdf($version);

        $lock->release();

        // ...
    }
}

Naming Locks

If the application needs different kind of Stores alongside each other, Symfony provides named lock:

  • YAML
  • XML
  • PHP
1
2
3
4
5
# config/packages/lock.yaml
framework:
    lock:
        invoice: ['semaphore', 'redis://r2.docker']
        report: 'semaphore'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- config/packages/lock.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:framework="http://symfony.com/schema/dic/symfony"
    xsi:schemaLocation="http://symfony.com/schema/dic/services
        https://symfony.com/schema/dic/services/services-1.0.xsd
        http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd">

    <framework:config>
        <framework:lock>
            <framework:resource name="invoice">semaphore</framework:resource>
            <framework:resource name="invoice">redis://r2.docker</framework:resource>
            <framework:resource name="report">semaphore</framework:resource>
        </framework:lock>
    </framework:config>
</container>
1
2
3
4
5
6
7
8
9
// config/packages/lock.php
use Symfony\Config\FrameworkConfig;

return static function (FrameworkConfig $framework) {
    $framework->lock()
        ->resource('invoice', ['semaphore', 'redis://r2.docker'])
        ->resource('report', ['semaphore']);
    ;
};

An autowiring alias is created for each named lock with a name using the camel case version of its name suffixed by LockFactory.

For instance, the invoice lock can be injected by naming the argument $invoiceLockFactory and type-hinting it with LockFactory:

// src/Controller/PdfController.php namespace AppController;

use SymfonyBundleFrameworkBundleControllerAbstractController; use SymfonyComponentLockLockFactory;

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

Blocking Store

If you want to use the RetryTillSaveStore for non-blocking locks, you can do it by decorating the store service:

1
2
3
4
lock.default.retry_till_save.store:
    class: Symfony\Component\Lock\Store\RetryTillSaveStore
    decorates: lock.default.store
    arguments: ['@.inner', 100, 50]
This work, including the code samples, is licensed under a Creative Commons BY-SA 3.0 license.
We stand with Ukraine.
Version:

Symfony 5.4 is backed by

Make sure your project is risk free

Make sure your project is risk free

Code consumes server resources. Blackfire tells you how

Code consumes server resources. Blackfire tells you how

↓ Our footer now uses the colors of the Ukrainian flag because Symfony stands with the people of Ukraine.

Avatar of Andrew Clark, a Symfony contributor

Thanks Andrew Clark (@tqt_andrew_clark) for being a Symfony contributor

1 commit • 2 lines changed

View all contributors that help us make Symfony

Become a Symfony contributor

Be an active part of the community and contribute ideas, code and bug fixes. Both experts and newcomers are welcome.

Learn how to contribute

Symfony™ is a trademark of Symfony SAS. All rights reserved.

  • What is Symfony?
    • Symfony at a Glance
    • Symfony Components
    • Case Studies
    • Symfony Releases
    • Security Policy
    • Logo & Screenshots
    • Trademark & Licenses
    • symfony1 Legacy
  • Learn Symfony
    • Symfony Docs
    • Symfony Book
    • Reference
    • Bundles
    • Best Practices
    • Training
    • eLearning Platform
    • Certification
  • Screencasts
    • Learn Symfony
    • Learn PHP
    • Learn JavaScript
    • Learn Drupal
    • Learn RESTful APIs
  • Community
    • SymfonyConnect
    • Support
    • How to be Involved
    • Code of Conduct
    • Events & Meetups
    • Projects using Symfony
    • Downloads Stats
    • Contributors
    • Backers
  • Blog
    • Events & Meetups
    • A week of symfony
    • Case studies
    • Cloud
    • Community
    • Conferences
    • Diversity
    • Documentation
    • Living on the edge
    • Releases
    • Security Advisories
    • SymfonyInsight
    • Twig
    • SensioLabs
  • Services
    • SensioLabs services
    • Train developers
    • Manage your project quality
    • Improve your project performance
    • Host Symfony projects
    Deployed on
Follow Symfony
Search by Algolia