Anophel-آنوفل برنامه نویسی موازی با Pthreads در PHP

برنامه نویسی موازی با Pthreads در PHP


انتشار:
0
0

به نظر می رسد توسعه دهندگان PHP به ندرت از موازی سازی استفاده می کنند. جذابیت سادگی برنامه‌نویسی همزمان و تک رشته‌ای قطعاً زیاد است، اما گاهی اوقات استفاده از کمی همزمانی می‌تواند باعث بهبود عملکرد ارزشمندی شود. و ما می خواهیم آن را بررسی کنیم.

در این مقاله نگاهی خواهیم داشت به اینکه چگونه می توان با اکستنشن pthreads در PHP به threading دست یافت. این به یک نسخه ZTS (Zend Thread Safety) از PHP 7.x به همراه pthreads v3 نصب شده نیاز دارد.

چه زمانی نباید از thread استفاده کرد؟

قبل از اینکه به ادامه مطلب برویم، ابتدا می خواهم توضیح دهم که چه زمانی نباید (و همچنین نمی توانید) از پسوند pthreads استفاده کنید.

در pthreads v3، توصیه این بود که pthread ها نباید در یک محیط وب سرور (یعنی در فرآیند FCGI) استفاده شوند. از pthreads v3، این توصیه اجرا شده است، بنابراین اکنون نمی توانید از آن در یک محیط وب سرور استفاده کنید. دو دلیل برجسته برای این امر عبارتند از:

  1. استفاده از چندین رشته در چنین محیطی (که باعث مشکلات IO، از جمله مشکلات دیگر می شود) ایمن نیست.
  2. مقیاس خوبی ندارد. به عنوان مثال، فرض کنید یک اسکریپت 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 را به صورت حرفه ای یادبگیرید و موضوع مقاله امروز را به صورت ویدیویی ببینید می توانید یوتیوب ما را نیز سابسکرایب کنید.

#موازی#برنامه_نویسی_موازی#php#پی_اچ_پی#pthreads
نظرات ارزشمند شما :