به نظر می رسد توسعه دهندگان PHP به ندرت از موازی سازی استفاده می کنند. جذابیت سادگی برنامهنویسی همزمان و تک رشتهای قطعاً زیاد است، اما گاهی اوقات استفاده از کمی همزمانی میتواند باعث بهبود عملکرد ارزشمندی شود. و ما می خواهیم آن را بررسی کنیم.
در این مقاله نگاهی خواهیم داشت به اینکه چگونه می توان با اکستنشن pthreads در PHP به threading دست یافت. این به یک نسخه ZTS (Zend Thread Safety) از PHP 7.x به همراه pthreads v3 نصب شده نیاز دارد.
چه زمانی نباید از thread استفاده کرد؟
قبل از اینکه به ادامه مطلب برویم، ابتدا می خواهم توضیح دهم که چه زمانی نباید (و همچنین نمی توانید) از پسوند pthreads استفاده کنید.
در pthreads v3، توصیه این بود که pthread ها نباید در یک محیط وب سرور (یعنی در فرآیند FCGI) استفاده شوند. از pthreads v3، این توصیه اجرا شده است، بنابراین اکنون نمی توانید از آن در یک محیط وب سرور استفاده کنید. دو دلیل برجسته برای این امر عبارتند از:
- استفاده از چندین رشته در چنین محیطی (که باعث مشکلات IO، از جمله مشکلات دیگر می شود) ایمن نیست.
- مقیاس خوبی ندارد. به عنوان مثال، فرض کنید یک اسکریپت PHP دارید که یک رشته جدید برای رسیدگی به برخی کارها ایجاد می کند، و آن اسکریپت بر اساس هر درخواست اجرا می شود. این بدان معنی است که برای هر درخواست، برنامه شما یک رشته جدید ایجاد می کند (این یک مدل رشته 1:1 است - یک رشته به یک درخواست). اگر برنامه شما 1000 درخواست در ثانیه ارائه می دهد، پس در حال ایجاد 1000 موضوع در ثانیه است! وجود این تعداد رشته در حال اجرا بر روی یک ماشین به سرعت آن را اذیت می کند و مشکل تنها با افزایش نرخ درخواست تشدید می شود.
به همین دلیل است که threading راه حل خوبی در چنین محیطی نیست. اگر به دنبال threading بهعنوان راهحلی برای کارهای مسدودکننده IO (مانند انجام درخواستهای HTTP) هستید،می توانید از برنامهنویسی ناهمزمان استفاده کنید که میتواند از طریق چارچوبهایی مانند Amp به دست آید.
.
با توجه به این موضوع، بیایید مستقیماً به مسائل برویم!
هندل تسک های یکباره Handling one-off tasks
گاهی اوقات، شما می خواهید تسک های یکباره را به روش چند رشته ای انجام دهید (مانند انجام برخی از تسک های IO-bound). در چنین مواردی، کلاس Thread
ممکن است برای ایجاد یک رشته جدید و اجرای واحد از کار در آن رشته جداگانه استفاده شود.
مثلا:
$task = new class extends Thread {
private $response;
public function run()
{
$content = file_get_contents("http://google.com");
preg_match("~<title>(.+)</title>~", $content, $matches);
$this->response = $matches[1];
}
};
$task->start() && $task->join();
var_dump($task->response); // string(6) "Google"
در بالا متد run
واحد کار ما است که در داخل thread جدید اجرا می شود. هنگام فراخوانی Thread::start
، نیز thread جدید ایجاد می شود و متد run
فراخوانی می شود. سپس رشته spwned را به رشته اصلی (از طریق Thread::join
) وصل می کنیم، که تا زمانی که رشته جداگانه اجرای آن تمام شود مسدود می شود. این تضمین میکند که اجرای کار قبل از اینکه بخواهیم نتیجه را به دست آوریم (ذخیره شده در task->response$
) به پایان رسیده است.
ممکن است خوب نباشد که مسئولیت یک کلاس با منطق مرتبط با رشته (از جمله نیاز به تعریف متد run
) آلوده شود. ما میتوانیم چنین کلاسهایی را با گسترش کلاس Threaded
جدا کنیم، جایی که میتوان آنها را در داخل رشتههای دیگر اجرا کرد:
class Task extends Threaded
{
public $response;
public function someWork()
{
$content = file_get_contents('http://google.com');
preg_match('~<title>(.+)</title>~', $content, $matches);
$this->response = $matches[1];
}
}
$task = new Task;
$thread = new class($task) extends Thread {
private $task;
public function __construct(Threaded $task)
{
$this->task = $task;
}
public function run()
{
$this->task->someWork();
}
};
$thread->start() && $thread->join();
var_dump($task->response);
هر کلاسی که باید در داخل یک رشته مجزا اجرا شود باید کلاس Threaded
را به نحوی گسترش دهد. این به این دلیل است که توانایی های لازم را برای اجرای درون رشته های مختلف و همچنین امنیت ضمنی و رابط های مفید (برای مواردی مانند همگام سازی منابع) فراهم می کند.
بیایید نگاهی گذرا به سلسلهمراتب کلاسهایی که توسط pthreadها در معرض دید قرار میگیرند بیاندازیم:
Threaded (implements Traversable, Collectable)
Thread
Worker
Volatile
Pool
ما قبلاً اصول اولیه کلاسهای Thread
و Threaded
را دیده و یاد گرفتهایم، بنابراین اکنون بیایید به سه مورد باقیمانده (Worker
، Volatile
و Pool
) نگاهی بیندازیم.
thread های بازیافت
چرخاندن یک رشته جدید برای هر task که باید موازی شود گران است. این به این دلیل است که یک معماری هیچ اشتراکی باید توسط pthread ها به کار گرفته شود تا بتوان به thread در داخل PHP دست یافت. این بدان معناست که کل زمینه اجرای نمونه فعلی مفسر PHP (شامل هر کلاس، رابط، صفت و تابع) باید برای هر رشته ایجاد شده کپی شود. از آنجایی که این کار تاثیر قابل توجهی بر عملکرد دارد، یک thread همیشه باید در صورت امکان دوباره استفاده شود. thread ها ممکن است به دو صورت مورد استفاده مجدد قرار گیرند: با Worker
ها یا با Pool
ها.
کلاس Worker
برای اجرای یک سری وظایف به صورت همزمان در داخل یک رشته دیگر استفاده می شود. این کار با ایجاد یک نمونه Worker
جدید (که یک رشته جدید ایجاد می کند) و سپس قرار دادن وظایف در آن رشته جداگانه (از طریق Worker::stack
) انجام می شود.
در اینجا یک مثال سریع آورده شده است:
class Task extends Threaded
{
private $value;
public function __construct(int $i)
{
$this->value = $i;
}
public function run()
{
usleep(250000);
echo "Task: {$this->value}\n";
}
}
$worker = new Worker();
$worker->start();
for ($i = 0; $i < 15; ++$i) {
$worker->stack(new Task($i));
}
while ($worker->collect());
$worker->shutdown();
در بالا 15 تسک را از طریق Worker::stack
بر روی شی worker$
جدید قرار می دهد و سپس آنها را به ترتیب انباشته شده پردازش می کند. روش Worker::collect
، همانطور که در بالا مشاهده می شود، برای پاکسازی وظایف پس از اتمام اجرای آنها استفاده می شود. با استفاده از آن در داخل یک حلقه while، رشته اصلی را مسدود می کنیم تا زمانی که اجرای همه کارهای stack ای به پایان برسد و قبل از اینکه Worker:: shutdown
را راه اندازی کنیم، پاکسازی شوند. خاموش کردن پیش از موعد Worker
(یعنی در حالی که هنوز کارهایی برای اجرا وجود دارد) همچنان رشته اصلی را تا زمانی که اجرای همه کارها به پایان برسد مسدود می کند.استک ها به سادگی جمع آوری نمی شوند از زباله. (که باعث نشت حافظه می شود).
کلاس Worker
چند روش دیگر مربوط به استک وظیفه خود را ارائه می دهد، از جمله Worker::unstack
برای حذف قدیمی ترین آیتم انباشته شده و Worker::getStacked
برای تعداد آیتم های موجود در استک اجرا. استک Worker فقط وظایفی را که قرار است اجرا شوند را در خود جای می دهد. هنگامی که یک وظیفه در استک اجرا شد، حذف می شود و سپس روی یک استک جداگانه (داخلی) قرار می گیرد تا زباله جمع آوری شود (با استفاده از Worker::collect
).
راه دیگر برای استفاده مجدد از thread در هنگام اجرای بسیاری از وظایف، استفاده از Thread Pool
(از طریق کلاس Pool
) است. Thread Pools توسط گروهی از Workers
نیرو می گیرد تا وظایف را به طور همزمان اجرا کنند، جایی که ضریب همزمانی (تعداد رشته هایی که pool
روی آنها اجرا می شود) در هنگام ایجاد pool
مشخص می شود.
بیایید مثال بالا را برای استفاده از مجموعه ای از Worker
به جای آن تطبیق دهیم:
class Task extends Threaded
{
private $value;
public function __construct(int $i)
{
$this->value = $i;
}
public function run()
{
usleep(250000);
echo "Task: {$this->value}\n";
}
}
$pool = new Pool(4);
for ($i = 0; $i < 15; ++$i) {
$pool->submit(new Task($i));
}
while ($pool->collect());
$pool->shutdown();
چند تفاوت قابل توجه بین استفاده از pool
در مقابل Worker وجود دارد. اولاً، poolها نیازی به راهاندازی دستی ندارند، آنها به محض اینکه در دسترس قرار میگیرند، شروع به اجرای وظایف میکنند. ثانیا، ما وظایف را به pool ارسال می کنیم، نه اینکه آنها را روی هم قرار دهیم. همچنین، کلاس Pool Threaded را گسترش نمی دهد و بنابراین ممکن است به رشته های دیگر منتقل نشود (برخلاف Worker).
به عنوان یک عمل خوب، Workerها و poolها باید همیشه پس از اتمام کارهایشان جمع آوری شوند و به صورت دستی تعطیل شوند. Threadهایی که از طریق کلاس Thread
ایجاد می شوند نیز باید به رشته ایجاد کننده متصل شوند.
pthreads and (im)mutability
آخرین کلاسی که باید پوشش داد Volatile
است.یک افزوده جدید به pthreads v3 می باشد. تغییر ناپذیری به یک مفهوم مهم در pthread ها تبدیل شده است، زیرا بدون آن، عملکرد به شدت کاهش می یابد. بنابراین، بهطور پیشفرض، ویژگیهای کلاسهای Threaded
که خود آبجکت های Threaded
هستند، اکنون غیرقابل تغییر هستند و بنابراین نمیتوان آنها را پس از تخصیص اولیه مجدداً تخصیص داد. تغییرپذیری صریح برای چنین ویژگی هایی اکنون خوب است و هنوز هم می توان با استفاده از کلاس جدید Volatile
انجام داد.
بیایید نگاهی گذرا به یک مثال برای نشان دادن محدودیتهای تغییرناپذیری جدید بیندازیم:
class Task extends Threaded // a Threaded class
{
public function __construct()
{
$this->data = new Threaded();
// $this->data is not overwritable, since it is a Threaded property of a Threaded class
}
}
$task = new class(new Task()) extends Thread { // a Threaded class, since Thread extends Threaded
public function __construct($tm)
{
$this->threadedMember = $tm;
var_dump($this->threadedMember->data); // object(Threaded)#3 (0) {}
$this->threadedMember = new StdClass(); // invalid, since the property is a Threaded member of a Threaded class
}
};
از سوی دیگر، ویژگیهای رشتهای کلاسهای Volatile
قابل تغییر هستند:
class Task extends Volatile
{
public function __construct()
{
$this->data = new Threaded();
$this->data = new StdClass(); // valid, since we are in a volatile class
}
}
$task = new class(new Task()) extends Thread {
public function __construct($vm)
{
$this->volatileMember = $vm;
var_dump($this->volatileMember->data); // object(stdClass)#4 (0) {}
// still invalid, since Volatile extends Threaded, so the property is still a Threaded member of a Threaded class
$this->volatileMember = new StdClass();
}
};
می بینیم که کلاس Volatile
تغییرناپذیری اعمال شده توسط کلاس Threaded
والد خود را overrides می کند تا خصوصیات Threaded قابل تخصیص مجدد باشد (و همچنین ()unset
).
فقط آخرین موضوع اساسی که وجود دارد که با توجه به تغییرپذیری و کلاس Volatile
، آرایه ها باید پوشش داده شود. آرایههای موجود در pthreadها هنگامی که به ویژگی کلاس Threaded
تخصیص داده میشوند، بهطور خودکار به آبجکت های Volatile
منتقل میشوند. این به این دلیل است که دستکاری یک آرایه از چندین زمینه در PHP ایمن نیست.
بیایید دوباره نگاهی گذرا به یک مثال بیندازیم تا بهتر بفهمیم:
$array = [1,2,3];
$task = new class($array) extends Thread {
private $data;
public function __construct(array $array)
{
$this->data = $array;
}
public function run()
{
$this->data[3] = 4;
$this->data[] = 5;
print_r($this->data);
}
};
$task->start() && $task->join();
/* Output:
Volatile Object
(
[0] => 1
[1] => 2
[2] => 3
[3] => 4
[4] => 5
)
*/
میتوانیم ببینیم که با آبجکت های Volatile
میتوان بهگونهای رفتار کرد که انگار آرایه هستند، زیرا آنها از عملیات مبتنی بر آرایه (همانطور که در بالا نشان داده شده است) با عملگر زیر مجموعه ([]
) پشتیبانی میکنند. با این حال، کلاسهای Volatile
توسط توابع رایج مبتنی بر آرایه مانند array_pop
و array_shift
پشتیبانی نمیشوند. در عوض، کلاس Threaded چنین عملیاتی را به عنوان متدهای داخلی در اختیار ما قرار می دهد.
به عنوان مثال :
$data = new class extends Volatile {
public $a = 1;
public $b = 2;
public $c = 3;
};
var_dump($data);
var_dump($data->pop());
var_dump($data->shift());
var_dump($data);
/* Output:
object(class@anonymous)#1 (3) {
["a"]=> int(1)
["b"]=> int(2)
["c"]=> int(3)
}
int(3)
int(1)
object(class@anonymous)#1 (1) {
["b"]=> int(2)
}
*/
سایر عملیات های پشتیبانی شده عبارتند از Threaded::chunk
و Threaded::merge
.
هماهنگ سازی
موضوع نهایی که در این مقاله به آن خواهیم پرداخت، همگام سازی در pthread ها است. همگام سازی تکنیکی برای فعال کردن دسترسی کنترل شده به منابع مشترک است.
به عنوان مثال، بیایید یک شمارنده ساده را پیاده سازی کنیم:
$counter = new class extends Thread {
public $i = 0;
public function run()
{
for ($i = 0; $i < 10; ++$i) {
++$this->i;
}
}
};
$counter->start();
for ($i = 0; $i < 10; ++$i) {
++$counter->i;
}
$counter->join();
var_dump($counter->i); // outputs a number from between 10 and 20
بدون استفاده از همگام سازی، خروجی قطعی نیست. نوشتن چندین رشته در یک متغیر بدون دسترسی کنترل شده باعث از بین رفتن بهروزرسانیها شده است.
بیایید این را با اضافه کردن همگام سازی اصلاح کنیم تا خروجی صحیح 20 را دریافت کنیم:
$counter = new class extends Thread {
public $i = 0;
public function run()
{
$this->synchronized(function () {
for ($i = 0; $i < 10; ++$i) {
++$this->i;
}
});
}
};
$counter->start();
$counter->synchronized(function ($counter) {
for ($i = 0; $i < 10; ++$i) {
++$counter->i;
}
}, $counter);
$counter->join();
var_dump($counter->i); // int(20)
بلوک های همگام سازی شده کد همچنین می توانند با استفاده از Threaded::wait
و Threaded::notify
(همراه با Threaded::notifyOne
) با یکدیگر همکاری کنند.
در اینجا یک افزایش پلکانی از دو حلقه همزمان همزمان وجود دارد:
$counter = new class extends Thread {
public $cond = 1;
public function run()
{
$this->synchronized(function () {
for ($i = 0; $i < 10; ++$i) {
var_dump($i);
$this->notify();
if ($this->cond === 1) {
$this->cond = 2;
$this->wait();
}
}
});
}
};
$counter->start();
$counter->synchronized(function ($counter) {
if ($counter->cond !== 2) {
$counter->wait(); // wait for the other to start first
}
for ($i = 10; $i < 20; ++$i) {
var_dump($i);
$counter->notify();
if ($counter->cond === 2) {
$counter->cond = 1;
$counter->wait();
}
}
}, $counter);
$counter->join();
/* Output:
int(0)
int(10)
int(1)
int(11)
int(2)
int(12)
int(3)
int(13)
int(4)
int(14)
int(5)
int(15)
int(6)
int(16)
int(7)
int(17)
int(8)
int(18)
int(9)
int(19)
*/
ممکن است متوجه شرایط اضافی شده باشید که در اطراف فراخوانی Threaded::wait
قرار داده شده است. این شرایط بسیار مهم هستند زیرا تنها زمانی اجازه میدهند که همگامسازی شدن زمانی از سر گرفته شود که اعلان دریافت کرده باشد و شرایط مشخص شده درست باشد. این مهم است زیرا ممکن است اعلانها از مکانهایی غیر از Threaded::notify
دریافت شوند. بنابراین، اگر Threaded::wait
در شرایط قرار نمی گرفتند، ما در معرض کال های بیدارسازی جعلی(purious wakeup) هستیم که منجر به کدهای غیرقابل پیشبینی میشود.
نتیجه
ما شاهد بسته بندی پنج کلاس pthread با آن هستیم (Threaded
، Thread
، Worker
، Volatile
و Pool
)، از جمله پوشش زمانی که هر یک از کلاس ها استفاده می شوند. ما همچنین به مفهوم تغییرناپذیری جدید در pthread ها و همچنین داشتن یک تور سریع از ویژگی همگام سازی که پشتیبانی می کند نگاه کرده ایم. با پوشش این اصول اساسی، اکنون میتوانیم به بررسی استفاده از thread در برخی موارد استفاده در دنیای واقعی بپردازیم.
در همین حال، اگر ایده های کاربردی در مورد pthread ها دارید، دریغ نکنید که آنها را در قسمت نظرات قرار دهید!
اگر می خواهید PHP را به صورت حرفه ای یادبگیرید و موضوع مقاله امروز را به صورت ویدیویی ببینید می توانید یوتیوب ما را نیز سابسکرایب کنید.