├── .gitignore ├── LICENSE ├── README.md ├── composer.json └── src ├── Guidv4.php ├── Thread.php └── ThreadInterface.php /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | *.log 3 | /.idea -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 AlexeY 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MultiThreading in php from Redis + php-cli 2 | [![Latest Stable Version](https://poser.pugx.org/sidorkinalex/multiphp/v)](//packagist.org/packages/sidorkinalex/multiphp) [![Total Downloads](https://poser.pugx.org/sidorkinalex/multiphp/downloads)](//packagist.org/packages/sidorkinalex/multiphp) [![Latest Unstable Version](https://poser.pugx.org/sidorkinalex/multiphp/v/unstable)](//packagist.org/packages/sidorkinalex/multiphp) [![License](https://poser.pugx.org/sidorkinalex/multiphp/license)](//packagist.org/packages/sidorkinalex/multiphp) 3 | 4 | this package is designed to quickly run background php-cli scripts with the ability to wait for the result of their execution in the main thread. 5 | 6 | ## Inslall and including in project 7 | 8 | ### Install 9 | You can install the package via the compositor by running the command: 10 | 11 | ``` 12 | composer require sidorkinalex/multiphp 13 | ``` 14 | 15 | Or you can download the current version from github and connect 3 files to the core of your project 16 | 17 | ``` 18 | require_once 'src/Guidv4.php'; 19 | require_once 'src/Thread.php'; 20 | require_once 'src/ThreadInterface.php'; 21 | ``` 22 | 23 | ### Include 24 | 25 | To connect in your project, you must create a class that inherits from the Thread class and override the following variables: 26 | 27 | ``` 28 | namespace App; 29 | 30 | use SidorkinAlex\Multiphp\Thread; 31 | 32 | class CustomThread extends Thread 33 | { 34 | public static $php_worker_path = "/var/www/html/exec.php"; //path to the script that starts the thread(with the initialized core of your project) 35 | public static $redis_host = "127.0.0.1";// can be a host, or the path to a unix domain socket from Redis 36 | public static $redis_port = 6379; // Redis port 37 | public static $redis_timeout = 0.0; // timeout from Redis value in seconds (optional, default is 0.0 meaning unlimited) 38 | public static $redis_reserved = null; //should be null if $retry_interval is specified from Redis 39 | public static $redis_retry_interval = 0; //retry interval in milliseconds from Redis. 40 | public static $cache_timeout = "1200"; // seconds lifetime of stream data in the Redis database 41 | 42 | } 43 | ``` 44 | you also need to create an entry point to the application to run through the console(php-cli) 45 | in the current example, this is /var/www/html/exec.php 46 | 47 | 48 | in the file for execution from the console, you need to put the code to start the execution of the stream, as the parameter will pass the key to get the stream data. 49 | 50 | ``` 51 | include 'vendor/autoload.php'; 52 | $key=$argv[1]; // 53 | SidorkinAlex\Multiphp\Thread::shell_start($key); 54 | ``` 55 | include 'vendor/autoload.php'; If the package is installed via the composer or if downloaded from github, connect the 3 files listed above. 56 | 57 | $argv[1] a unique thread key that is generated when the thread is started and passed as a parameter to the php-cli script. Depending on the framework, this variable may change. 58 | 59 | 60 | SidorkinAlex\Multiphp\Thread::shell_start($key); calling a static method that starts the execution of the function passed to the thread. 61 | 62 | 63 | ### Examples install from Symfony 64 | 65 | https://github.com/SidorkinAlex/symfony-website-skeleton-multithreading_php/pull/1/files 66 | 67 | 68 | ## Example code 69 | 70 | ### Example 1 (parallel execution) 71 | 72 | ``` 73 | 74 | $paramsFromThread = 3; 75 | $test = new CThread($paramsFromThread,function ($n){ 76 | for ($i = 0; $i<$n; $i++){ 77 | $pid=getmypid(); 78 | file_put_contents('test1.log', $i." my pid is {$pid} \n", FILE_APPEND); 79 | sleep(3); 80 | } 81 | return 'test1'; 82 | }); 83 | $test->start(); 84 | 85 | $test2 = new CThread($paramsFromThread,function ($n){ 86 | for ($i = 0; $i<$n; $i++){ 87 | $pid=getmypid(); 88 | file_put_contents('test2.log', $i." my pid is {$pid} \n", FILE_APPEND); 89 | sleep(3); 90 | } 91 | return 'test2'; 92 | }); 93 | $test2->start(); 94 | $result1 = $test->getCyclicalResult(); 95 | $result2 = $test2->getCyclicalResult(); 96 | ``` 97 | 98 | In the example, we see the creation of two threads, which are passed the function of iterating through arrays with a son at the end of each step for 3 seconds. 99 | if we would execute them sequentially, the script execution time would be 18 seconds, when running them in parallel threads, the script execution time is 9 seconds. 100 | 101 | ### Example 2 (background thread) 102 | 103 | ``` 104 | public function testpars(Request $request): Response 105 | { 106 | $userIds=$request->request->get('users'); 107 | if(!is_array($userIds)){ 108 | throw new \Exception('post[users] is not array'); 109 | } 110 | $EmailSendlerThread = new CThread($users,function ($users){ 111 | foreach ($users as $user_id){ 112 | 113 | $transport = \Symfony\Component\Mailer\Transport::fromDsn('smtp://localhost'); 114 | $mailer = new \Symfony\Component\Mailer\Mailer($transport); 115 | 116 | $userObj = new \App\Service\User(); 117 | $userObj->retrieve($user_id); 118 | if(!empty($userObj->email)){ 119 | $email = (new Email()) 120 | ->from(\App\Service\Email::getSelfEmail()) 121 | ->to($userObj->email) 122 | //->cc('cc@example.com') 123 | //->bcc('bcc@example.com') 124 | //->replyTo('fabien@example.com') 125 | //->priority(Email::PRIORITY_HIGH) 126 | ->subject(\App\Service\Email::get_first_email_subject()) 127 | ->text(\App\Service\Email::get_first_email_text()) 128 | ->html(\App\Service\Email::get_first_email_html()); 129 | 130 | $mailer->send($email); 131 | } 132 | } 133 | }); 134 | $EmailSendlerThread->start(); 135 | 136 | return new JsonResponse(['status' =>"ok"]); 137 | } 138 | ``` 139 | 140 | Example 2 shows the code that implements the start of sending emails. the execution of the testpars method is completed by running the $EmailSendlerThread thread and does not wait for its execution. 141 | 142 | Therefore, the response to such a request will be very fast, the main thread will not wait for the $EmailSendlerThread thread to finish, but will simply return {"status" : "ok"} to the initiator. 143 | 144 | 145 | # Многопоточность на PHP с помошбю Redis + php-cli 146 | 147 | Этот пакет предназначен для быстрого запуска фоновых php-cli скриптов с возможностью ожидания результата их выполнения в основном потоке. 148 | 149 | ## Установка и включение в проект 150 | 151 | ### Установить 152 | Вы можете установить пакет через компоновщик, выполнив команду: 153 | ``` 154 | composer require sidorkinalex/multiphp 155 | ``` 156 | 157 | Или вы можете загрузить текущую версию с github и подключить 3 файла к ядру вашего проекта 158 | 159 | ``` 160 | require_once 'src/Guidv4.php'; 161 | require_once 'src/Thread.php'; 162 | require_once 'src/ThreadInterface.php'; 163 | ``` 164 | 165 | ### Подключение к проекту 166 | 167 | Чтобы включить его в свой проект, необходимо создать класс, который наследуется от класса Thread, и переопределить следующие переменные: 168 | 169 | 170 | ``` 171 | namespace App; 172 | 173 | use SidorkinAlex\Multiphp\Thread; 174 | 175 | class CustomThread extends Thread 176 | { 177 | public static $php_worker_path = "/var/www/html/exec.php"; //path to the script that starts the thread(with the initialized core of your project) 178 | public static $redis_host = "127.0.0.1";// can be a host, or the path to a unix domain socket from Redis 179 | public static $redis_port = 6379; // Redis port 180 | public static $redis_timeout = 0.0; // timeout from Redis value in seconds (optional, default is 0.0 meaning unlimited) 181 | public static $redis_reserved = null; //should be null if $retry_interval is specified from Redis 182 | public static $redis_retry_interval = 0; //retry interval in milliseconds from Redis. 183 | public static $cache_timeout = "1200"; // seconds lifetime of stream data in the Redis database 184 | 185 | } 186 | ``` 187 | вам также необходимо создать точку входа в приложение для запуска через консоль(php-cli) 188 | в текущем примере это /var/www/html/exec.php 189 | 190 | 191 | в файл для выполнения из консоли нужно поместить код для запуска выполнения потока, так как параметр передаст ключ для получения данных потока. 192 | 193 | ``` 194 | include 'vendor/autoload.php'; 195 | $key=$argv[1]; // 196 | SidorkinAlex\Multiphp\Thread::shell_start($key); 197 | ``` 198 | 199 | include 'vendor/autoload.php'; Если пакет установлен через composer. Eсли загружен с github, подключите 3 файла, перечисленные выше. 200 | 201 | $argv[1] уникальный ключ потока, который генерируется при запуске потока и передается в качестве параметра скрипту php-cli. В зависимости от структуры эта переменная может изменяться. 202 | 203 | 204 | SidorkinAlex\Multi php\Thread::she'will_start($key); вызов статического метода, который запускает выполнение функции, переданной потоку. 205 | 206 | ### Примеры установки для Symfony 207 | 208 | https://github.com/SidorkinAlex/symfony-website-skeleton-multithreading_php/pull/1/files 209 | 210 | 211 | ## Примеры кода 212 | 213 | 214 | ### Пример 1 (паралельное выполнение) 215 | 216 | ``` 217 | 218 | $paramsFromThread = 3; 219 | $test = new CThread($paramsFromThread,function ($n){ 220 | for ($i = 0; $i<$n; $i++){ 221 | $pid=getmypid(); 222 | file_put_contents('test1.log', $i." my pid is {$pid} \n", FILE_APPEND); 223 | sleep(3); 224 | } 225 | return 'test1'; 226 | }); 227 | $test->start(); 228 | 229 | $test2 = new CThread($paramsFromThread,function ($n){ 230 | for ($i = 0; $i<$n; $i++){ 231 | $pid=getmypid(); 232 | file_put_contents('test2.log', $i." my pid is {$pid} \n", FILE_APPEND); 233 | sleep(3); 234 | } 235 | return 'test2'; 236 | }); 237 | $test2->start(); 238 | $result1 = $test->getCyclicalResult(); 239 | $result2 = $test2->getCyclicalResult(); 240 | ``` 241 | 242 | В примере мы видим создание двух потоков, в котрые передается вункция перебора массивов с сном вконце каждого шага на 3 секунды. 243 | если мы бы выполняли их последовательно, то время выполнения скрипта было бы 18 секунд, при запуске их паралельными потоками время выполнения скрипта составляет 9 секунд. 244 | 245 | ### Пример 2 (фоновый поток) 246 | ``` 247 | public function testpars(Request $request): Response 248 | { 249 | $userIds=$request->request->get('users'); 250 | if(!is_array($userIds)){ 251 | throw new \Exception('post[users] is not array'); 252 | } 253 | $EmailSendlerThread = new CThread($users,function ($users){ 254 | foreach ($users as $user_id){ 255 | 256 | $transport = \Symfony\Component\Mailer\Transport::fromDsn('smtp://localhost'); 257 | $mailer = new \Symfony\Component\Mailer\Mailer($transport); 258 | 259 | $userObj = new \App\Service\User(); 260 | $userObj->retrieve($user_id); 261 | if(!empty($userObj->email)){ 262 | $email = (new Email()) 263 | ->from(\App\Service\Email::getSelfEmail()) 264 | ->to($userObj->email) 265 | //->cc('cc@example.com') 266 | //->bcc('bcc@example.com') 267 | //->replyTo('fabien@example.com') 268 | //->priority(Email::PRIORITY_HIGH) 269 | ->subject(\App\Service\Email::get_first_email_subject()) 270 | ->text(\App\Service\Email::get_first_email_text()) 271 | ->html(\App\Service\Email::get_first_email_html()); 272 | 273 | $mailer->send($email); 274 | } 275 | } 276 | }); 277 | $EmailSendlerThread->start(); 278 | 279 | return new JsonResponse(['status' =>"ok"]); 280 | } 281 | ``` 282 | В примере 2 представлен код который реализует запуск отправки писем. выполнение метода testpars завершается запуском потока $EmailSendlerThread и не ждет его выполнения. 283 | 284 | Поэтому ответ на подобный запрос будет очень быстрым основной поток не будет ждать завершения работы потока $EmailSendlerThread а просто вернет {"status" : "ok"} инициатору. 285 | 286 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sidorkinalex/multiphp", 3 | "type": "library", 4 | "description": "multithreading in php by means of php-cli and Redis", 5 | "keywords": ["multithreading", "multithreading php", "cli", "php-cli"], 6 | "homepage": "https://github.com/SidorkinAlex/multithreading_php", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Sidorkin Alex", 11 | "email": "sid88alex@yandex.ru", 12 | "homepage": "https://github.com/SidorkinAlex", 13 | "role": "Developer" 14 | } 15 | ], 16 | "require": { 17 | "jeremeamia/superclosure": "^2.4", 18 | "php": ">=7.1" 19 | }, 20 | "autoload": { 21 | "psr-4": { 22 | "SidorkinAlex\\Multiphp\\": "src" 23 | } 24 | }, 25 | "minimum-stability": "dev", 26 | "prefer-stable": true 27 | } 28 | -------------------------------------------------------------------------------- /src/Guidv4.php: -------------------------------------------------------------------------------- 1 | function = $serializer->serialize($function); 41 | $this->functionParams = $params; 42 | $this->id = Guidv4::create_guidv4(); 43 | } 44 | 45 | /** 46 | * launching a new cli script to execute a task in the created thread 47 | * запуск нового cli скрипта для выполнения задания в созданном потоке 48 | */ 49 | public function start() 50 | { 51 | $key = $this->saveToRedis(); 52 | shell_exec("php {$this::$php_worker_path} '$key' > /dev/null & "); 53 | } 54 | 55 | /** 56 | * creating a connection to Redis 57 | * создание подключения к Redis 58 | * @return \Redis 59 | */ 60 | protected function redisConnect(): \Redis 61 | { 62 | $redis = new \Redis(); 63 | $redis->connect( 64 | self::$redis_host, 65 | self::$redis_port, 66 | self::$redis_timeout, 67 | self::$redis_reserved, 68 | self::$redis_retry_interval); 69 | return $redis; 70 | } 71 | 72 | /** 73 | * runs an anonymous function that was passed to the class 74 | * запускает анонимную функцию, которая была передана классу 75 | */ 76 | protected function exec() 77 | { 78 | $this->pid = getmypid(); 79 | $serializer = new Serializer(); 80 | $function = $serializer->unserialize($this->function); 81 | if ($this->functionParams !== null) { 82 | $result = $function($this->functionParams); 83 | } else { 84 | $result = $function(); 85 | } 86 | if (!is_null($result)) { 87 | $this->result = $result; 88 | $this->saveToRedis(); 89 | } 90 | $this->finalise(); 91 | $this->publishEnd(); 92 | } 93 | 94 | /** 95 | * a static method to be called in the cli script that will act as a thread. 96 | * статический метод, который нужно вызывать в cli скрипте, который будет выполнять роль потока. 97 | * @param $key 98 | * @throws \Exception 99 | */ 100 | public static function shell_start($key) 101 | { 102 | $redis = new \Redis(); 103 | $redis->connect( 104 | self::$redis_host, 105 | self::$redis_port, 106 | self::$redis_timeout, 107 | self::$redis_reserved, 108 | self::$redis_retry_interval); 109 | $var = $redis->get($key); 110 | $obj = unserialize($var); 111 | if ($obj instanceof Thread) { 112 | $obj->exec(); 113 | } else { 114 | throw new \Exception('$obj is not Thread object, $obj->exec() not started'); 115 | } 116 | } 117 | 118 | /** 119 | * saving class data in Redis 120 | * сохранение данных класса в Redis 121 | * @return string 122 | */ 123 | protected function saveToRedis(): string 124 | { 125 | $redis = $this->redisConnect(); 126 | $serializer = new Serializer(); 127 | $par = serialize($this); 128 | $key = self::SAVE_BASE_NAME . $this->id; 129 | $redis->set($key, $par, self::$cache_timeout); 130 | return $key; 131 | } 132 | 133 | /** 134 | * getting the result of executing a stream from Redis. 135 | * получение результата выполнения потока из Redis. 136 | * @return 137 | */ 138 | protected function getResultFromRedis() 139 | { 140 | $redis = $this->redisConnect(); 141 | $key = self::SAVE_BASE_NAME . $this->id; 142 | $obj = $redis->get($key); 143 | $par = unserialize($obj); 144 | return $par->result; 145 | } 146 | 147 | /** 148 | * sending a message to the channel about the completion of the thread execution 149 | * отправка сообщения в канал о завершении выполнения потока 150 | */ 151 | protected function publishEnd() 152 | { 153 | $redis = $this->redisConnect(); 154 | $redis->publish(self::CHANNEL_BASE_NAME . $this->id, 'end'); 155 | } 156 | 157 | /** 158 | * creating an entry in redis about thread shutdown 159 | * создание записи в Redis о завершении работы потока 160 | */ 161 | protected function finalise() 162 | { 163 | $redis = $this->redisConnect(); 164 | $key = self::FINAL_BASE_NAME . $this->id; 165 | $redis->set($key, "true", self::$cache_timeout); 166 | } 167 | 168 | /** 169 | * getting the result of the stream cyclically (high reliability, but more resource consumption) 170 | * получение результата потока циклично (высокая надежность, но большее потребление ресурсов) 171 | * @param int $waitingTimeThreadCompletion milliseconds 172 | * maximum thread waiting time if you specify 0 then there is no waiting time limit 173 | * максимальное время ожитания потока если указать 0 то ограничение по времени ожидания отсутствует 174 | * @param int $cyclicalSleepTime milliseconds 175 | * time step in milliseconds through which the completion of the stream is checked 176 | *временной шаг в миллисекундах через который происходит проверка завершения потока 177 | * @return 178 | */ 179 | public function getCyclicalResult(int $waitingTimeThreadCompletion = 0, int $cyclicalSleepTime = 100) 180 | { 181 | if ($waitingTimeThreadCompletion === 0) { 182 | $this->waitingCyclicalFinish($cyclicalSleepTime); 183 | } else { 184 | $cyclicalCount = ceil($waitingTimeThreadCompletion / $cyclicalSleepTime); 185 | 186 | $this->waitingCyclicalCountFinish($cyclicalSleepTime, $cyclicalCount); 187 | } 188 | return $this->getResultFromRedis(); 189 | } 190 | 191 | /**loop check by key in radish whether the stream is complete 192 | * циклическая проверка по ключу в редисе завершен ли поток 193 | * @param int $cyclicalSleepTime milliseconds 194 | */ 195 | protected function waitingCyclicalFinish(int $cyclicalSleepTime) 196 | { 197 | ini_set('max_execution_time', '300'); 198 | $redis = $this->redisConnect(); 199 | $key = self::FINAL_BASE_NAME . $this->id; 200 | while (true) { 201 | if ($redis->get($key) == 'true') { 202 | break; 203 | } 204 | usleep($cyclicalSleepTime); 205 | } 206 | } 207 | 208 | /** 209 | * loop check by key in radish whether the stream with loop limitation is completed 210 | * циклическая проверка по ключу в редисе завершен ли поток с ограничением циклов 211 | * @param int $cyclicalSleepTime milliseconds 212 | * @param int $cyclicalCount count 213 | */ 214 | protected function waitingCyclicalCountFinish(int $cyclicalSleepTime, int $cyclicalCount) 215 | { 216 | $redis = $this->redisConnect(); 217 | $key = self::FINAL_BASE_NAME . $this->id; 218 | $i = 0; 219 | 220 | while ($i <= $cyclicalCount) { 221 | if ($redis->get($key) == 'true') { 222 | break; 223 | } 224 | usleep($cyclicalSleepTime); 225 | } 226 | } 227 | 228 | } 229 | -------------------------------------------------------------------------------- /src/ThreadInterface.php: -------------------------------------------------------------------------------- 1 |