پردازش موازی(Parallel Processing) چیه؟ طبق تعریف ویکیپدیا به استفاده ی همزمان بیشتر از یک پردازنده برای اجرای برنامه. فرض کنید میخواهیم هزار ایمیل را راس یک ساعت خاص برای کاربران ارسال کنیم. قاعدتا ارسال این ایمیل ها بصورت ترتیبی نیازمند زمان زیادی است و در یک زمان مشخص برای همه ی کاربران ارسال نمی شود. اینجاست که پردازش موازی به داد ما میرسد و می توانیم از تمامی منابع سیستم برای ارسال ایمیل ها استفاده کرده و کمترین تاخیر را در ارسال ها داشته باشیم.
زبان های برنامه نویسی مثل C, Java و Erlang ذاتا از چندنخی پشتیبانی می کنند. متاسفانه پی اچ پی اینگونه نیست. پی اچ پی رو بخاطر فقدان پشتیبانی از چندنخی سرزنش نکنید و به یاد داشته باشید که پی اچ پی یک زبان اسکریپتی سطح بالاست و هدف اصلی آن سرو کردن درخواست های stateless پروتکل HTTP بود. ولی با گذشت زمان که محبوب تر و تواناتر شد دولوپرها برنامه های پیچیده ای را با پی اچ توسعه دادند. و بدین ترتیب با گذشت زمان قابلیت های پیشرفته ای مثل پردازش موازی از پی اچ پی درخواست می شد.
برای انجام این کار در پی اچ پی 3 راهکار داریم:
چندنخی(Multi-Thread):
همانطور که قبلا هم اشاره کردم پی اچ پی بصورت ذاتی از چندنخی پشتیبانی نمیکند. اما اکستنشن pthreads در پی اچ پی این امکان را در اختیار شما قرار می دهد. شخصا خیلی با pthreads کار نکرده ام چون فکر میکنم چندنخی در پی اچ پی اشتباه است. شما می توانید کد thread safe خود را داشته باشید ولی زمانیکه از دیگر اکستنشن های پی اچ پی استفاده می کنید میتوانید مطمئن باشید که کد همچنان thread safe است. متاسفانه اکثر اکستنشن های مشهور thread safe نیستند.
چندپردازشی(Multi-Process):
برای دستیابی به پردازش موازی می توانیم چندین فرایند را بصورت همزمان در پی اچ پی اجرا کنیم. 3 راه برای این کار وجود دارد
روش Fork:
پی اچ پی چند فانکشن برای کنترل فرایندها مهیا کرده که به شما اجازه می دهند تا فرایندهای فرزند را بوجود آورید. زمانیکه شما pcntl_fork را صدا میزنید یک فرایند فرزند را بوجود میاورید که دقیقا یک کپی از فرایند والدش است. و این به این معنی است که فرایند فرزند می تواند تمامی منابع و متغیرهای والدش را مثل کانکشن MySQL را به ارث ببرد. این ماژول در CLI و FastCGI در دسترس است. و در نظر داشته باشید که دسترسی فرایندهای فرزند به منابع اشتراکی والد خطرناک است، برای مثال بستن کانکشن MySQL بدون اینکه والد در جریان باشد.
کد نمونه برای نمایش استفاده از process forking
$workload = "some work load" $processId = pcntl_fork(); if ($processId < 0){ die('Fork failed!'); } else if ($processId == 0) { // child starts working here trim($workload); } else { // parent waits for child pcntl_wait($status); }
روش Execute Command:
استفاده از فانکشن های زیر که باعث بوجود آمدن فرایند های کامل و مجزا می شود.
در نظر داشته باشید که اجرای این دستورات یک عمل ارزان تلقی نشود مخصوصا زمانیکه شما از این دستورات در حلقه استفاده می کنید. زمانیکه شما هزاران فرایند را روی یک سرور به اجرا در میاورید سرور برای سوئیچ کردن بین آنها خیلی مشغول میشود و عملکرد کلی سیستم پایین می آید.
روش Piping:
اگر به دو روش قبلی توجه کرده باشید متوجه یک مشکل خواهید شد. شما نمی توانید بین فرایندهایی که در حال اجرا هستند هماهنگی بوجود آورید و در واقع عدم ارتباط بین فرایندها فقدانی است که باید برطرف گردد.
روش Piping این اجازه را به ما می دهد که دو فرایند برای همدیگر دستوراتی را ارسال کنند و خیلی شبیه دستور پایپ در لینوکس عمل میکند. تابع کلیدی که یک فرایند جدید ایجاد میکند و یک هندل را برای کار با فرایند در اختیار ما می گذارد تابع proc_open است. برای نحوه ی چگونگی کار piping نمونه کدی را آورده ایم. کلاینت کلمه ی Hello را برای Worker ارسال می کند و Worker بعد از دریافت کلمه ی Hello کلمه ی World را به آن می چسباند و به کلاینت پس می دهد.
// pipe_client.php, uses proc_open() function to create the // worker process, and send instructions to it $descriptorspec = array( 0 => array("pipe", "r"), // stdin for worker 1 => array("pipe", "w"), // stdout for worker ); $worker = proc_open("php pipe_worker.php", $descriptorspec, $pipes); if ($worker) { fwrite($pipes[0], "hello"); while (!feof($pipes[1])) { echo fgets($pipes[1]). "\n" } proc_close($worker); } // pipe_worker.php, all it does is to read instructions // from STDIN, and write response to STDOUT $line = fread(STDIN,4096); fwrite(STDOUT, "$line world");
پردازش موازی توزیع شده(Distributed Parallel Processing):
تا اینجا نگاهی به مواردی مثل نخ ها، fork کردن فرایند، ایجاد فرایند جدید و برقراری ارتباط بین فرایندها با Piping انداختیم. این روش ها زمانی قابل استفاده هستند که روی یک سرور هستیم. در اینحالت شما با افزایش پردازنده و هسته های داخل سرور به کار خود Scale می دهید ولی اگر به چیزی فراتر نیاز دارید باید به سمت پردازش موازی توزیع شده بروید.
در جهان توزیع شده برنامه نویسی تحت شبکه اجتناب ناپذیر است، خوشبختانه فریمورک و کتابخانه های منبع باز کاملی موجود هستند که شما را از برنامه نویسی سوکت در پی اچ پی بی نیاز می کنند. اینجاست که Gearman و ZeroMQ پا به عرصه وجود گذاشتند.
ابزار Gearman:
این فریمورک توسط خالق Memcached یعنی Danga توسعه داده شد. توجه داشته باشید برای اجرای کد زیر باید (Gearman deamon (GearmanD نصب شده باشد در اینجا localhost:4731 و همچنین اکستنشن Gearman نیز برای پی اچ پی نصب باشد. کد زیر همان مثال Hello World ی است که در بالا داشتیم.
// gearman_client.php $gmclient= new GearmanClient(); $gmclient->addServer(); //localhost // the function ->do blocks, wait for worker response // to do tasks in non-blocking mode, use doBackground function // Also not 'getWorld' is a job name, and 'Hello' // is the job payload $result = $gmclient->do("getWorld", "Hello "); echo "$result\n" // gearman_worker.php $gmworker= new GearmanWorker(); $gmworker->addServer(); //localhost // register the job 'getWorld' handler 'getWorldFn' // which is defined below as a function $gmworker->addFunction("getWorld", "getWorldFn"); // loops here wait for jobs, and let handler deal with it while($gmworker->work()) {} function getWorldFn($job) { return $job->workload() . "World!"
ابزار ZeroMQ:
طبق لینکدین یکی از مهارتهای فنی سریع در حال رشد ZeroMQ است، اگر هنوز نمی دانید چیست بهتر است آنرا بررسی کنید. در واقع یک گلوله ی نقره ای نیست که بصورت جادویی همه چیز را برای شما حل کند اما ساخت سیستم های توزیع شده را برای شما خیلی ساده می کند.
برای اجرای کد زیر باید ZeroMQ C library نصب شده باشد و همچنین اکستنشن ZeroMQ نیز برای پی اچ پی نصب باشد. کد زیر همان مثال Hello World ی است که در بالا داشتیم.
//zmp_client.php //creates context $context = new ZMQContext(); //create DEALER socket http://api.zeromq.org/2-1:zmq-socket#toc6 $socket = new ZMQSocket($context, ZMQ::SOCKET_DEALER); //client connects $socket->connect('tcp://127.0.0.1:15000'); //send 100 Hellos for ($i = 0; $i < 100; $i++){ $socket->send("$i - Hello"); echo $socket->recv() . "\n" sleep(1); } //zmp_server.php //creates context $context = new ZMQContext(); //create ROUTER socket, http://api.zeromq.org/2-1:zmq-socket#toc7 $socket = new ZMQSocket($context, ZMQ::SOCKET_ROUTER); //worker binds $socket->bind('tcp://*:15000'); //poll the socket, like event dispatcher $poll = new ZMQPoll(); $poll->add($socket, ZMQ::POLL_IN); $readable = $writeable = array(); while(true) { $events = $poll->poll($readable, $writeable, 1000); foreach($readable as $s) { //When there is incoming message, deal with it $message = $socket->recvmulti(); $socket->sendmulti(array($message[0], $message[1] . " World!")); } }
پیشنهاد میکنم کد client را قبل از راه اندازی worker اجرا کنید و ببینید چه اتفاقی می افتد. و سپس worker را راه اندازی کنید و ببینید چه اتفاقی می افتد. در حالیکه client حلقه ی 100 تایی را تکرار می کند worker را برای چند ثانیه متوقف کنید و دوباره شروع کنید و ببینید چه اتفاقی می افتد، چند بار این کار را تکرار کنید.
به این فکر کنید که شما با 30 خط کد چه دستاورد بزرگی دارید آن هم بدون هندل کردن یک خطا و همچنین به یاد داشته باشید هیچ message broker مثل RabbitMQ یا Gearman در کار نبوده است.
پیروز باشید...