php

Understanding Concurrency in PHP

honeybadger_staff

Honeybadger Staff

Posted on June 28, 2023

Understanding Concurrency in PHP

This article was originally written by Michael Barasa on the Honeybadger Developer Blog.

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.
Enter fullscreen mode Exit fullscreen mode

We can also display errors and exceptions:

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

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)

?>
Enter fullscreen mode Exit fullscreen mode

The above simpleGenerator() function will output the following:

The generator begins
Yielded 0
Yielded 1
Yielded 2
The generator ends
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
?>
Enter fullscreen mode Exit fullscreen mode

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
?>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
    }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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();
      }
    );
  }
);
Enter fullscreen mode Exit fullscreen mode

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"),
  ];
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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
  }
));
Enter fullscreen mode Exit fullscreen mode

Finally, we started the server using the following code:

yield $server->start();
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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.

💖 💪 🙅 🚩
honeybadger_staff
Honeybadger Staff

Posted on June 28, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related