Understanding Concurrency in PHP

Concurrency is a topic that every developer should understand! In this article, join Michael Barasa on a tour of PHP's concurrency features and how to use them in your web applications.

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.

What to do next:
  1. Try Honeybadger for FREE
    Honeybadger helps you find and fix errors before your users can even report them. Get set up in minutes and check monitoring off your to-do list.
    Start free trial
    Easy 5-minute setup — No credit card required
  2. 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
    Stop wasting time manually checking logs for errors!

    Try the only application health monitoring tool that allows you to track application errors, uptime, and cron jobs in one simple platform.

    • Know when critical errors occur, and which customers are affected.
    • Respond instantly when your systems go down.
    • Improve the health of your systems over time.
    • Fix problems before your customers can report them!

    As developers ourselves, we hated wasting time tracking down errors—so we built the system we always wanted.

    Honeybadger tracks everything you need and nothing you don't, creating one simple solution to keep your application running and error free so you can do what you do best—release new code. Try it free and see for yourself.

    Start free trial
    Simple 5-minute setup — No credit card required

    Learn more

    "We've looked at a lot of error management systems. Honeybadger is head and shoulders above the rest and somehow gets better with every new release."
    — Michael Smith, Cofounder & CTO of YvesBlue

    Honeybadger is trusted by top companies like:

    “Everyone is in love with Honeybadger ... the UI is spot on.”
    Molly Struve, Sr. Site Reliability Engineer, Netflix
    Start free trial
    Are you using Sentry, Rollbar, Bugsnag, or Airbrake for your monitoring? Honeybadger includes error tracking with a whole suite of amazing monitoring tools — all for probably less than you're paying now. Discover why so many companies are switching to Honeybadger here.
    Start free trial
    Stop digging through chat logs to find the bug-fix someone mentioned last month. Honeybadger's built-in issue tracker keeps discussion central to each error, so that if it pops up again you'll be able to pick up right where you left off.
    Start free trial
    “Wow — Customers are blown away that I email them so quickly after an error.”
    Chris Patton, Founder of Punchpass.com
    Start free trial