The term concurrency refers to a situation where two or more activities occur at the same time. In software, concurrency is the ability to execute multiple tasks or processes simultaneously. Therefore, concurrency has a substantial impact on overall software performance.

By default, PHP executes tasks or code on a single thread. This means that one process must complete before the next task is performed. Running code on a single thread allows developers to avoid the complexity associated with parallel programming.

However, as the program grows, the limitations of running code on a single thread become more evident. For starters, an organization relying on this technology may have difficulties dealing with sudden surges in traffic.

Limitations of Single-Threaded Operations

Single-threaded operations fail to make proper use of the available system resources. For instance, they run on one CPU core rather than taking advantage of multi-core architectures.

Single-threaded operations are also quite time-consuming. Due to the blocking architecture, one process needs to be completed before the next task is handled (i.e., sequential execution). Developers may also experience challenges dealing with processes that are dependent on one another.

Running code on a single thread also poses a serious security risk. Companies have to deal with complex issues, such as memory leaks and data loss.

Third-Party Concurrency Libraries

PHP lacks top-level abstractions for implementing and managing concurrency. Nevertheless, developers can perform multiple tasks using third-party libraries, such as Amp and ReactPHP.

ReactPHP is categorized as a low-level dependency for event-driven programming. It features an event loop that supports low-level utilities, such as HTTP client/server, async DNS resolver, streams abstraction, and network client/servers.

ReactPHP's event-driven architecture allows it to handle thousands of long-running operations and concurrent connections. A significant advantage of React PHP is that it’s non-blocking by default. It uses worker classes to handle different processes. Additionally, ReactPHP can also be incorporated into other third-party libraries, database systems, and network services.

Like ReactPHP, Amp uses an event-driven architecture for multitasking. It provides synchronous APIs and non-blocking I/O to handle long-running processes.

In this tutorial, we will learn how to manage concurrency in PHP using ReactPHP and Amp.

Before going further, it's important to understand coroutines, promises, and generators due to their influence on handling concurrency. These concepts are discussed below.

Promises

According to PHP documentation, a promise is a value returned from an asynchronous operation. Although it's not present at a specific time, it will become available in the future. The value returned by a promise can either be the expected response or an error.

A promise is demonstrated in the sudo code below:

$networkrequest = $networkfactory->createRequest('POST','htttp://dummyurl')
//using a promise to send request
$promise = $client->sendAsyncRequest($networkrequest);

echo "Waiting for response"
// The message will be displayed after the network request but before the response is returned.

We can also display errors and exceptions:

try {
  $result = $promise->await();
} catch (\Exception $exception) {
  echo $exception->message();
}

Generators

A generator acts as a normal function, but rather than returning a particular value, it yields as much data as it needs to.

Generators use the keyword yield, which allows them to save state. This feature allows the function to resume from where it was paused. The return keyword can only be used to stop a generator's execution.

In PHP, the generator class also implements the Iterator interface. When looping through a set of data, PHP will call the generator whenever a value is required.

Generators allow developers to save valuable memory space and processing time. For example, there's no need to create and save arrays in memory, which can otherwise cause the application to exceed the allocated memory limit.

We define a generator as follows:

<?php
function simpleGenerator() {
  echo "The generator begins"; 

  for ($a = 0; $a < 3; ++$a) {
    yield $a;
    echo "Yielded $a";
  }

  echo "The generator ends"; 
}

foreach (simpleGenerator() as $v)

?>

The above simpleGenerator() function will output the following:

The generator begins
Yielded 0
Yielded 1
Yielded 2
The generator ends

Coroutines

Coroutines allow a program to be sub-divided into smaller sections, which can then be executed much faster.

PHP runs code on a single thread, which consumes a lot of time, especially when long-running processes are involved. Fortunately, we can use promises and generators to write asynchronous code.

Generators allow us to pause an operation and wait for a particular promise to be completed. The coroutine can then resume once the promise is resolved.

If the promise is successful, the generator yields the result, which is then displayed to the user. In case of a failure, the coroutine will throw an exception.

Handling Concurrency Using ReactPHP

Run the following command in your terminal to install ReactPHP. Note that you must have Composer installed to execute the command successfully.

composer require react/http react/socket

In your project folder, create an index.php file and add the following boilerplate code:

<?php
require __DIR__ . '/vendor/autoload.php'; 
// Loading the required libraries into our project.

$http = new React\Http\HttpServer(
  function (Psr\Http\Message\ServerRequestInterface $request) {
    //returning a message in case a connection is made to the server.
    return React\Http\Message\Response::plaintext("ReactPHP started\n");
  }
);

$socket = new React\Socket\SocketServer('127.0.0.1:5000'); 
// Creating a socket server
$http->listen($socket);

echo "Server running at http://127.0.0.1:5000". PHP_EOL;
?>

In the above code, we have created a simple server that prints a welcome message each time a new request is detected.

We can handle concurrent async requests using ReactPHP's event loop. The LoopInterface provided by the event loop allows the user to perform concurrent requests, as shown in the example below:

<?php
require __DIR__ . '/vendor/autoload.php'; 
// Importing classes into the project

$loop = \React\EventLoop\Factory::create();

$loop->addTimer(2, 
  function(){ 
    // Using a timer to start a task after 2 seconds
    echo "Request 1" .PHP_EOL;
  }
);

$loop->addTimer(10, 
  function(){ 
    // Using a timer to start a second task after 10 seconds
    echo "Request 2" .PHP_EOL;
  }
);

$loop->run(); 
  // Starting the event loop
?>

Handling Concurrency Using Amp

To install Amp, ensure that you are in the project folder, and then run this command in your terminal:

composer require amphp/amp

In this section, we will discuss how promises allow Amp to handle hundreds of concurrent requests. The three major states of a promise are success, failure, and pending. The success state indicates that a promise has been resolved successfully and that the appropriate response has been returned.

The pending state indicates that the promise has not been completed or resolved. We can use this opportunity to run other processes as we wait for the promise to be fulfilled.

In Amp, a promise can be implemented using the following sudo code:

getData(
  function($error, $value)){
    if($value){
      //Incase a value is detected, the promise is fulfilled
    }else{
      //Handling the error when the promise fails
    }
}

Let's learn how to use Amp to make concurrent calls to the MySQL database. Navigate to your project folder and run the following commands in your terminal:

composer require amphp/log
composer require amphp/http-server-mysql
composer require amphp/mysql
composer require amphp/http-server-router

Next, open the index.php file and replace the existing code with the following:

#!/usr/local/bin/php
<?php
require_once __DIR__ . '/vendor/autoload.php';
DEFINE('DB_HOSTNAME', 'localhost');
DEFINE('DB_USERNAME', 'root');
DEFINE('DB_NAME', 'concurrency');
DEFINE('PASSWORD', '');

use Amp\Mysql;
use Monolog\Logger;
use Amp\ByteStream\ResourceOutputStream;
use Amp\Http\Server\Request;
use Amp\Socket;
use Amp\Http\Server\Router;
use Amp\Http\Server\RequestHandler\CallableRequestHandler;
use Amp\Log\ConsoleFormatter;
use Amp\Log\StreamHandler;
use Amp\Http\Server\Response;
use Amp\Http\Server\Server;
use Amp\Http\Status;

In the code above, we imported the required Amp classes into our project. We also declared our MySQL database credentials.

The next step is to create a simple web server using Amp:

Amp\Loop::run(
  function () {
    $servers = [
      Socket\listen("127.0.0:8080"),
      Socket\listen("[::]:8080"),
    ];

    $handler = new StreamHandler(new ResourceOutputStream(\STDOUT));
    $handler->setFormatter(new ConsoleFormatter);
    $logger = new Logger('server');
    $logger->pushHandler($handle);
    $router = new Router;

    $router->addRoute('GET', '/', new CallableRequestHandler(
      function () {
        return new Response(Status::OK, ['content-type' => 'text/plain'], 'Database API');
      }
    ));

    $router->addRoute('GET', '/{data}', new CallableRequestHandler(
      function (Request $request) {
        $args = $request->getAttribute(Router::class);
        return getDatabaseData();
      }
    ));

    $server = new Server($servers, $router, $logger);

    yield $server->start();
    // To stop the server
    Amp\Loop::onSignal(SIGINT, 
      function (string $watcherId) use ($server) {
        Amp\Loop::cancel($watcherId);
        yield $server->stop();
      }
    );
  }
);

In the code above, we first created a Socket server by declaring the IP and the Port number.

  $servers = [
    Socket\listen("127.0.0:8080"),
    Socket\listen("[::]:8080"),
  ];

We then used a log handler to print the program's output in the console:

$handler = new StreamHandler(new ResourceOutputStream(\STDOUT));
$handler->setFormatter(new ConsoleFormatter);
$logger = new Logger('server');
$logger->pushHandler($handle);

For navigation, we declared a router object that will allow us to handle different routes:

$router = new Router;

$router->addRoute('GET', '/', new CallableRequestHandler(
  function () {
    return new Response(Status::OK, ['content-type' => 'text/plain'], 'Database API');
    // Incase of a successful connection, this message is shown
  } 
));

$router->addRoute('GET', '{/data}', new CallableRequestHandler(
  function (Request $request) {
    $args = $request->getAttribute(Router::class);
    return getDatabaseData();
    // Return records from the database
  }
));

Finally, we started the server using the following code:

yield $server->start();

In Amp, the yield keyword enables other processes, such as coroutines and I/O handlers, to continue running. This is an important part of non-blocking asynchronous programming.

The next step is to handle our database logic. We will do so in the getDatabaseData function defined below:

function getDatabaseData() {
  $db = Mysql\pool(Mysql\ConnectionConfig::fromString(
    "host=".DB_HOSTNAME.";user=".DB_USERNAME.";pass=".PASSWORD.";db=".DB_NAME
  )); 

  $responsedata = "";

  $sqlStmt = yield $db->prepare("SELECT * FROM concurrency"); //SQL statement

  $result = yield $sqlStmt->execute(); 
  //Executing the SQL statemnt and storing the result.

  while (yield $result->advance()) {
    // Looping through database records
    $row = $result->getCurrent();
    $responsedata .= $row['name'] . ',';
  }

  $responseJSON = json_encode($responsedata); //Converting to JSON
  $response = new Response(Status::OK, ['content-type' => 'text/plain'], $responseJSON);
  $db->close(); // Closing database connection
  return $response; //Returning response
}

In the getDatabaseData function, we declared a database object ($db) and passed in our credentials. We also declared an empty $responsedata variable, which we will use to store database records.

Next, we added an SQL statement in the $sqlStmt object and executed it. We looped through the database response and stored the information in the $responsedata.

Finally, we changed the response to JSON, closed the database connection, and returned the data. When you navigate to localhost:8080 in your browser, you will see the retrieved database records.

Conclusion

In this tutorial, we have learned how to manage concurrency in PHP using ReactPHP and Amp. These libraries are powerful time-savers.

Developers can leverage components, such as coroutines, promises, and generators supported by both ReactPHP and Amp, to handle thousands of concurrent requests.

Get the Honeybadger newsletter

Each month we share news, best practices, and stories from the DevOps & monitoring community—exclusively for developers like you.
    author photo
    Michael Barasa

    Michael Barasa is a software developer. He loves technical writing, contributing to open source projects, and creating learning material for aspiring software engineers.

    More articles by Michael Barasa
    An advertisement for Honeybadger that reads 'Turn your logs into events.'

    "Splunk-like querying without having to sell my kidneys? nice"

    That’s a direct quote from someone who just saw Honeybadger Insights. It’s a bit like Papertrail or DataDog—but with just the good parts and a reasonable price tag.

    Best of all, Insights logging is available on our free tier as part of a comprehensive monitoring suite including error tracking, uptime monitoring, status pages, and more.

    Start logging for FREE
    Simple 5-minute setup — No credit card required