Understanding Concurrency in PHP
Honeybadger Staff
Posted on June 28, 2023
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.
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.
Posted on June 28, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.