PHP غير متزامن

قبل عشر سنوات ، كان لدينا مجموعة LAMP كلاسيكية: Linux و Apache و MySQL و PHP ، والتي عملت في الوضع البطيء لـ mod_php. لقد تغير العالم ، ومعه أهمية السرعة. ظهر PHP-FPM ، مما سمح بزيادة أداء الحلول بشكل كبير في PHP ، وعدم إعادة الكتابة بسرعة إلى شيء أسرع.

في موازاة ذلك ، تم تطوير مكتبة ReactPHP باستخدام مفهوم Event Loop لمعالجة الإشارات من نظام التشغيل وتقديم نتائج للعمليات غير المتزامنة. تطوير فكرة ReactPHP - AMPHP. تستخدم هذه المكتبة نفس حلقة الأحداث ، ولكنها تدعم coroutines ، على عكس ReactPHP. تسمح لك بكتابة كود غير متزامن يبدو متزامن. ربما يكون هذا هو أحدث إطار لتطوير التطبيقات غير المتزامنة في PHP.



لكن السرعة مطلوبة أكثر فأكثر ، والأدوات ليست كافية بالفعل ، لذا فإن فكرة البرمجة غير المتزامنة في PHP هي إحدى الطرق لتسريع معالجة الاستعلام واستخدام الموارد بشكل أفضل.

هذا ما سيتحدث عنه أنطون شابوفتا (zloyusr) مطور في Onliner. خبرة أكثر من 10 سنوات: لقد بدأت بتطبيقات سطح المكتب في C / C ++ ، ثم انتقلت إلى تطوير الويب في PHP. يكتب مشاريع "منزلية" في C # و Python 3 ، وفي PHP يقوم بتجربة DDD و CQRS و Event Sourcing و Async Multitasking.

تستند هذه المقالة إلى نص تقرير أنتون حول PHP Russia 2019 . سنفهم فيه عمليات الحجب وغير الحجب في PHP ، وسندرس بنية حلقة الحدث والأولويات غير المتزامنة ، مثل Promise و coroutines ، من الداخل. أخيرًا ، سنكتشف ما ينتظرنا في ext-async و AMPHP 3 و PHP 8.


نقدم بعض التعاريف. لفترة طويلة حاولت العثور على تعريف دقيق لعمليات غير متزامنة وغير متزامنة ، لكنني لم أجد وكتبت لي.
Asynchrony هو قدرة النظام البرمجي على عدم منع سلسلة التنفيذ الرئيسية.
العملية غير المتزامنة هي عملية لا تمنع تدفق تنفيذ البرنامج حتى تكتمل.

يبدو الأمر بسيطًا ، ولكن عليك أولاً أن تفهم العمليات التي تمنع تدفق التنفيذ.

عمليات الحظر


PHP هي لغة مترجم. يقرأ سطر التعليمات البرمجية سطراً ، ويترجم إلى تعليماته وينفذها. في أي سطر من المثال أدناه سيتم حظر الرمز؟

public function update(User $user)
{
    try {
        $sql = 'UPDATE users SET ...';
        return $this->connection->execute($sql, $user->data());
    } catch (\PDOException $error) {
        log($error->getMessage());
    }

    return 0;
}

إذا كان لنا اتصال بقاعدة البيانات عن طريق شركة تنمية نفط عمان، سيتم حظر موضوع التنفيذ على سلسلة الاستعلام إلى SQL خادم: return $this->connection->execute($sql, $user->data());.

هذا لأن PHP لا يعرف المدة التي سيعالج فيها خادم SQL هذا الاستعلام وما إذا كان سيتم تنفيذه على الإطلاق. ينتظر استجابة من الخادم ولم يتم تشغيل البرنامج طوال هذا الوقت.

تمنع PHP أيضًا تدفق التنفيذ على جميع عمليات الإدخال / الإخراج.

  • نظام الملفات : fwrite، file_get_contents.
  • قواعد البيانات : PDOConnection، RedisClient. تعمل جميع ملحقات ربط قاعدة البيانات تقريبًا في وضع الحظر افتراضيًا.
  • العمليات : exec، system، proc_open. هذه عمليات حظر ، حيث أن كل العمل مع العمليات يتم من خلال مكالمات النظام.
  • العمل مع المدخل / المخرج المعياري : readline، echo، print.

بالإضافة إلى ذلك ، تم حظر التنفيذ على المؤقتات : sleep، usleep. هذه هي العمليات التي نطلب فيها بوضوح من الخيط أن ينام لفترة من الوقت. سوف يكون PHP خاملاً طوال هذا الوقت.

عميل SQL غير متزامن


لكن PHP الحديثة هي لغة للأغراض العامة ، وليس فقط للويب مثل PHP / FI في عام 1997. لذلك ، يمكننا كتابة عميل SQL غير متزامن من البداية. المهمة ليست الأكثر تافهة ، ولكن قابلة للحل.

public function execAsync(string $query, array $params = [])
{
    $socket = stream_socket_client('127.0.0.1:3306', ...);

    stream_set_blocking($socket, false);

    $data = $this->packBinarySQL($query, $params);
    
    socket_write($socket, $data, strlen($data));
}

ماذا يفعل مثل هذا العميل؟ إنه يتصل بخادم SQL الخاص بنا ، ويضع المقبس في وضع غير قابل للحظر ، ويحزم الطلب بتنسيق ثنائي يفهمه خادم SQL ، ويكتب البيانات إلى المقبس.

بما أن المقبس في وضع عدم الحجب ، فإن عملية الكتابة من PHP تكون سريعة.

ولكن ماذا سيعود نتيجة لهذه العملية؟ نحن لا نعرف ما سيرد عليه خادم SQL. قد يستغرق الأمر وقتًا طويلاً لإكمال الطلب أو عدمه على الإطلاق. ولكن يجب إعادة شيء ما؟ إذا استخدمنا PDO وقمنا باستدعاء updateالاستعلام على خادم SQL ، فسيتم إرجاعنا affected rows- تم تغيير عدد الصفوف بواسطة هذا الاستعلام. لا يمكننا إعادته حتى الآن ، لذلك نعد فقط بالعودة.

وعد


هذا مفهوم من عالم البرمجة غير المتزامنة.
الوعد هو كائن مجمّع فوق نتيجة عملية غير متزامنة. علاوة على ذلك ، لا تزال نتيجة العملية غير معروفة لنا.
لسوء الحظ ، لا يوجد معيار Promise واحد ، ولا يمكن نقل المعايير مباشرة من عالم JavaScript إلى PHP.

كيف يعمل الوعد


نظرًا لعدم وجود نتيجة حتى الآن ، يمكننا فقط إنشاء بعضها callbacks.



عند توفر البيانات ، من الضروري تنفيذ رد اتصال onResolve.



في حالة حدوث خطأ ، سيتم تنفيذ رد اتصال onRejectلمعالجة الخطأ.



تبدو واجهة Promise شيئًا مثل هذا.

interface Promise
{
    const
        STATUS_PENDING = 0,
        STATUS_RESOLVED = 1,
        STATUS_REJECTED = 2
    ;

    public function onResolve(callable $callback);
    public function onReject(callable $callback);
    public function resolve($data);
    public function reject(\Throwable $error);
}

الوعد له حالة وطرق لضبط الاسترجاعات وملء ( resolve) الوعد بالبيانات أو الخطأ ( reject). ولكن هناك اختلافات واختلافات. قد يتم استدعاء الأساليب بشكل مختلف ، أو بدلاً من طرق منفصلة لإنشاء عمليات رد الاتصال ، وقد يكون resolveهناك rejectبعض الطرق ، كما في AMPHP ، على سبيل المثال.

في كثير من الأحيان تقنيات لملء وعد resolveو rejectتأخذ بها في كائن منفصل والمؤجلة - وظيفة غير متزامن دولة التخزين. يمكن اعتباره نوعًا من مصنع وعد. مرة واحدة: مؤجل واحد يعطي وعدًا.



كيفية تطبيق هذا في عميل SQL إذا قررنا كتابته بأنفسنا؟

عميل SQL غير متزامن


أولاً ، أنشأنا مؤجل ، وقمنا بكل العمل باستخدام مآخذ ، وكتبنا البيانات وأعدنا الوعد - كل شيء بسيط.

public function execAsync(string $query, array $params = [])
{
    $deferred = new Deferred;

    $socket = stream_socket_client('127.0.0.1:3306', ...);
    stream_set_blocking($socket, false);

    $data = $this->packBinarySQL($query, $params);
    socket_write($socket, $data, strlen($data));

    return $deferred->promise();
}

عندما نحصل على وعد ، يمكننا على سبيل المثال:

  • تعيين رد الاتصال والحصول على تلك affected rowsالتي تعود إلينا PDOConnection؛
  • معالجة الخطأ ، إضافة إلى السجل ؛
  • أعد محاولة الاستعلام إذا استجاب خادم SQL بخطأ.

$promise = $this->execAsync($sql, $user->data());

$promise->onResolve(function (int $rows) {
    echo "Affected rows: {$rows}";
});

$promise->onReject(function (\Throwable $error) {
    log($error->getMessage());
});

يبقى السؤال: قمنا بتعيين رد الاتصال ، ومن سيتصل resolveو reject؟

حلقة الحدث


هناك مفهوم حلقة الحدث - حلقة الحدث . وهو قادر على معالجة الرسائل في بيئة غير متزامنة. بالنسبة للإدخال / الإخراج غير المتزامن ، ستكون هذه رسائل من نظام التشغيل جاهز للقراءة أو الكتابة.

كيف تعمل.

  • يخبر العميل Event Loop أنه مهتم بنوع من مأخذ التوصيل.
  • تستقصي حلقة الحدث نظام التشغيل من خلال استدعاء النظام stream_select: هل المقبس جاهز ، هل جميع البيانات المكتوبة ، هي البيانات القادمة من الجانب الآخر.
  • إذا أفاد نظام التشغيل أن المقبس ليس جاهزًا ، فقد تم حظره ، فإن تكرار حلقة الحدث يكرر الحلقة.
  • عندما يخطر نظام التشغيل أن المقبس جاهز ، ترجع Event Loop التحكم إلى العميل وتمكن ( resolveأو reject) وعد.



نعبر عن هذا المفهوم في الكود: خذ أبسط الحالات ، أزل معالجة الأخطاء والفروق الدقيقة الأخرى ، بحيث تبقى حلقة واحدة لا نهائية. في كل تكرار ، سوف يقوم باستطلاع نظام التشغيل حول مآخذ التوصيل الجاهزة للقراءة أو الكتابة ، والاتصال باستدعاء لمقبس معين.

public static function run()
{
    while (true) {
        stream_select($readSockets, $writeSockets, null, 0);
        
        foreach ($readSockets as $i => $socket) {
            call_user_func(self::readCallbacks[$i], $socket);
        }

        // Do same for write sockets
    }
}

نكمل عميل SQL لدينا. نبلغ Event Loop أنه بمجرد وصول البيانات من خادم SQL إلى المقبس الذي نعمل معه ، نحتاج إلى إحالة Defired إلى الحالة "منتهية" ونقل البيانات من المقبس إلى Promise.

public function execAsync(string $query, array $params = [])
{
    $deferred = new Deferred;
    ...
    Loop::onReadable($socket, function ($socket) use ($deferred) {
        $deferred->resolve(socket_read($socket));
    });

    return $deferred->promise();
}

الحدث حلقة قادرة على التعامل دينا I / O و يعمل مع مآخذ . ماذا يمكن أن يفعل؟

  • JavaScript setTimeout setInterval — . N . Event Loop .
  • Event Loop . process control, .

Event Loop


كتابة حلقة الأحداث الخاصة بك ليست ممكنة فحسب ، بل ضرورية أيضًا. إذا كنت ترغب في العمل مع PHP غير المتزامن ، فمن المهم كتابة التنفيذ البسيط الخاص بك لفهم كيفية عمل ذلك. لكن في الإنتاج ، بالطبع ، لن نستخدمه ، لكننا سنأخذ تطبيقات جاهزة: مستقرة وخالية من الأخطاء ومثبتة في العمل.

هناك ثلاثة تطبيقات رئيسية.

رد PHP . أقدم مشروع ، بدأ في PHP 5.3. الآن الحد الأدنى المطلوب من إصدار PHP هو 5.3.8. يقوم المشروع بتنفيذ معيار الوعود / A من عالم JavaScript.

AMPHP . هذا هو التطبيق الذي أفضل استخدامه. الحد الأدنى من المتطلبات هو PHP 7.0 ، وبما أن الإصدار التالي هو بالفعل 7.3. يستخدم coroutines على رأس وعد.

سوول. هذا إطار صيني مثير للاهتمام يحاول فيه المطورون نقل بعض المفاهيم من عالم Go إلى PHP. الوثائق باللغة الإنجليزية غير مكتملة ، معظمها على GitHub باللغة الصينية. إذا كنت تعرف اللغة ، فتابع ، لكني خائفة حتى الآن من العمل.



رد PHP


دعونا نرى كيف سيبدو العميل باستخدام ReactPHP لـ MySQL.

$connection = (new ConnectionFactory)->createLazyConnection();

$promise = $connection->query('UPDATE users SET ...');
$promise->then(
    function (QueryResult $command) {
        echo count($command->resultRows) . ' row(s) in set.';
    },
    function (Exception $error) {
        echo 'Error: ' . $error->getMessage();
    });

كل شيء تقريبًا هو نفسه كما كتبنا: ننشئ onnectionوننفذ الطلب. يمكننا ضبط رد الاتصال لمعالجة النتائج (العودة affected rows):

    function (QueryResult $command) {
        echo count($command->resultRows) . ' row(s) in set.';
    },

واستدعاء لمعالجة الخطأ:

    function (Exception $error) {
        echo 'Error: ' . $error->getMessage();
    });

من عمليات الاسترجاعات هذه ، يمكنك بناء سلاسل طويلة ، لأن كل نتيجة thenفي ReactPHP تُعيد أيضًا Promise.

$promise
    ->then(function ($data) {
        return new Promise(...);
    })
    ->then(function ($data) {
        ...
    }, function ($error) {
        log($error);
    })
    ...

هذا هو حل لمشكلة تسمى رد الاتصال الجحيم. لسوء الحظ ، في تطبيق ReactPHP ، يؤدي هذا إلى مشكلة "Promise hell" ، عندما تكون هناك حاجة لاستدعاء 10-11 لاستدعاء RabbitMQ بشكل صحيح . من الصعب العمل مع هذا الرمز وإصلاحه. أدركت بسرعة أن هذا لم يكن ملكي وتحولت إلى AMPHP.

أمفب


هذا المشروع أصغر من ReactPHP ويعزز مفهومًا مختلفًا - coroutines . إذا نظرت إلى العمل مع MySQL في AMPHP ، يمكنك أن ترى أن هذا يماثل تقريبًا العمل PDOConnectionفي PHP.

$pool = Mysql\pool("host=127.0.0.1 port=3306 db=test");

try {
    $result = yield $pool->query("UPDATE users SET ...");

    echo $result->affectedRows . ' row(s) in set.';
} catch (\Throwable $error) {
    echo 'Error: ' . $error->getMessage();
}

هنا نقوم بإنشاء تجمع وتوصيل وتنفيذ الطلب. يمكننا التعامل مع الأخطاء من خلال الأخطاء المعتادة try...catch، ولا نحتاج إلى رد الاتصال.

ولكن قبل المكالمة غير المتزامنة ، تظهر الكلمة الرئيسية - هنا yield.

مولدات كهرباء


الكلمة الرئيسية yieldتحول وظيفتنا إلى مولد.

function generator($counter = 1)
{
    yield $counter++;

    echo "A";

    yield $counter;

    echo "B";

    yield ++$counter;
}

بمجرد أن يواجه مترجم PHP yieldوظائف في الجسم ، فإنه يدرك أنه وظيفة مولد. بدلاً من التنفيذ ، يتم إنشاء كائن فئة عند استدعاؤه Generator.

ترث المولدات واجهة المكرر.

$generator = generator(1);

foreach ($generator as $value) {
    echo $value;
}

while ($generator->valid()) {
    echo $generator->current();

    $generator->next();
}

وفقا لذلك، فمن الممكن لتشغيل دورات foreachو whileوغيرها. ولكن المثير للاهتمام أن المكرر لديه طرق currentو next. دعونا نذهب من خلالهم خطوة بخطوة.

إدارة وظيفتنا generator($counter = 1). نسمي طريقة المولد current(). سيتم إرجاع قيمة المتغير $counter++.

بمجرد تنفيذ المولد next()، سيذهب الرمز إلى المكالمة التالية داخل المولد yield. yieldسيتم تنفيذ الجزء الكامل من التعليمات البرمجية بين الاثنين ، وهذا رائع. بالاستمرار في تدوير المولد ، نحصل على النتيجة.

Coroutines


لكن للمولد وظيفة أكثر إثارة للاهتمام - يمكننا إرسال البيانات إلى المولد من الخارج. في هذه الحالة ، هذا ليس مولدًا تمامًا ، ولكنه كوروتين أو كوروتين.

function printer() {  
    while (true) {     
        echo yield;       
    }                             
}                                

$print = printer();
$print->send('Hello');
$print->send(' PHPRussia');
$print->send(' 2019');
$print->send('!');

في هذا القسم من الرمز ، من المثير للاهتمام أنه while (true)لن يمنع تدفق التنفيذ ، ولكن سيتم تنفيذه مرة واحدة. أرسلنا البيانات إلى Corutin وتلقى 'Hello'. أرسل المزيد - وردت 'PHPRussia'. المبدأ واضح.

بالإضافة إلى إرسال البيانات إلى المولد ، يمكنك إرسال الأخطاء ومعالجتها من الداخل ، وهو أمر مناسب.

function printer() {
    try {
        echo yield;
    } catch (\Throwable $e) {
        echo $e->getMessage();
    }
}

printer()->throw(new \Exception('Ooops...'));

كي تختصر. Corutin هو أحد مكونات البرنامج الذي يدعم التوقف والتنفيذ المستمر مع الحفاظ على الحالة الحالية . يتذكر Corutin مكدس مكالماته والبيانات الموجودة داخله ويمكنه استخدامها في المستقبل.

المولدات والوعد


دعونا نلقي نظرة على واجهات المولد و Promise.

class Generator
{
    public function send($data);
    public function throw(\Throwable $error);
}

class Promise
{
    public function resolve($data);
    public function reject(\Throwable $error);
}

تبدو متشابهة ، باستثناء أسماء الطرق المختلفة. يمكننا إرسال البيانات وإلقاء خطأ لكل من المولد و Promise.

كيف نستطتيع ان نستعمل هذا؟ لنكتب دالة.

function recoil(\Generator $generator)
{
    $promise = $generator->current();

    $promise->onResolve(function($data) use ($generator) {
        $generator->send($data);
        recoil($generator);
    };

    $promise->onReject(function ($error) use ($generator) {
        $generator->throw($error);
        recoil($generator);
    });
}

تأخذ الدالة على القيمة الحالية للمولد: $promise = $generator->current();.

بالغت قليلا. نعم ، يجب أن نتحقق من أن القيمة الحالية التي يتم إرجاعها إلينا هي نوع من instanceofالوعد. إذا كان الأمر كذلك ، فيمكننا أن نطلب منه معاودة الاتصال. يرسل البيانات داخليًا إلى المولد عند نجاح Promise ويبدأ الوظيفة بشكل متكرر recoil.

    $promise->onResolve(function($data) use ($generator) {
        $generator->send($data);
        recoil($generator);
    };

يمكن القيام بنفس الشيء مع الأخطاء. إذا فشل Promise ، على سبيل المثال ، قال خادم SQL: "عدد كبير جدًا من الاتصالات" ، فيمكننا إلقاء الخطأ داخل المولد والانتقال إلى الخطوة التالية.

كل هذا ينقلنا إلى المفهوم المهم لتعدد المهام التعاوني.

تعدد المهام التعاونية


هذا هو نوع من تعدد المهام ، حيث يتم تنفيذ المهمة التالية فقط بعد أن تعلن المهمة الحالية صراحة أنها مستعدة لإعطاء المعالج وقتًا لمهام أخرى.

نادرًا ما صادفت شيئًا بسيطًا ، مثل العمل مع قاعدة بيانات واحدة فقط. في أغلب الأحيان ، في عملية تحديث المستخدم ، تحتاج إلى تحديث البيانات في قاعدة البيانات ، في فهرس البحث ، ثم تنظيف ذاكرة التخزين المؤقت أو تحديثها ، ثم إرسال 15 رسالة أخرى إلى RabbitMQ. في PHP ، يبدو كل هذا.



نقوم بتنفيذ العمليات واحدًا تلو الآخر: قمنا بتحديث قاعدة البيانات والفهرس ثم ذاكرة التخزين المؤقت. ولكن بشكل افتراضي ، يقوم PHP بحظر مثل هذه العمليات (I / O) ، لذلك إذا نظرت عن كثب ، في الواقع ، كل شيء كذلك.



على الأجزاء المظلمة حجبنا. يأخذون معظم الوقت.

إذا عملنا في وضع غير متزامن ، فإن هذه الأجزاء ليست هناك ، فإن الجدول الزمني للتنفيذ متقطع.



يمكنك لصقها كلها معًا وصنع القطع واحدة تلو الأخرى.



ما كل هذا؟ إذا نظرت إلى حجم المخطط الزمني ، في البداية يستغرق الكثير من الوقت ، ولكن بمجرد أن نلصقه معًا ، يتسارع التطبيق.

لطالما استخدم مفهوم حلقة الأحداث وتعدد المهام التعاوني في تطبيقات مختلفة: Nginx و Node.js و Memcached و Redis. كلهم يستخدمون داخل حلقة الحدث ويتم بناؤها على نفس المبدأ.

منذ أن بدأنا نتحدث عن خوادم الويب Nginx و Node.js ، لنتذكر كيف تتم معالجة الطلبات في PHP.

معالجة الطلب في PHP


يرسل المتصفح طلبًا ، ويصل إلى خادم HTTP الذي يوجد خلفه مجموعة من تدفقات FPM. يبدأ أحد سلاسل العمليات هذا الطلب في العمل ، ويربط كودنا ويبدأ في تنفيذه.



عند وصول الطلب التالي ، سوف يلتقطه مؤشر ترابط FPM آخر ، ويربط الكود وسيتم تنفيذه.

هناك مزايا لخطة العمل هذه .

  • معالجة الأخطاء البسيطة . إذا حدث خطأ ما وسقط أحد الطلبات ، فنحن لسنا بحاجة إلى القيام بأي شيء - سيأتي الطلب التالي ، وهذا لن يؤثر على عمله.
  • لا نفكر في الذاكرة . لسنا بحاجة لتنظيف أو مراقبة الذاكرة. في الطلب التالي ، سيتم مسح كل الذاكرة.

هذا مخطط رائع يعمل في PHP منذ البداية ولا يزال يعمل بنجاح. ولكن هناك أيضًا عيوب .

  • الحد من عدد العمليات . إذا كان لدينا 50 سلسلة رسائل في الدقيقة على الخادم ، فبمجرد وصول الطلب 51 ، سينتظر حتى يصبح أحد سلاسل المحادثات مجانيًا.
  • تكاليف تبديل السياق . يقوم نظام التشغيل بتبديل الطلبات بين تدفقات FPM. هذه العملية على مستوى المعالج تسمى تبديل السياق. إنها مكلفة وتدير عددًا كبيرًا من الإجراءات. من الضروري حفظ جميع التسجيلات ، مكدس الاتصال ، كل شيء موجود في المعالج ، ثم التبديل إلى عملية أخرى ، وتحميل تسجيلاته ومكدس المكالمات الخاص به ، وتنفيذ شيء هناك مرة أخرى ، والتبديل مرة أخرى ، والحفظ مرة أخرى ... لفترة طويلة.

دعنا نتعامل مع السؤال بشكل مختلف - سنكتب خادم HTTP في PHP نفسه.

خادم HTTP غير متزامن




يمكن إنجازه. لقد تعلمنا بالفعل كيفية العمل مع المقابس في وضع عدم الحظر ، واتصال HTTP هو نفس المقبس. كيف ستبدو وتعمل؟

هذا مثال على بدء خوادم HTTP في إطار عمل AMPHP.

Loop::run(function () {
    $app = new Application();
    $app->bootstrap();

    $sockets = [Socket\listen('0.0.0.0:80')];

    $server = new Server($sockets, new CallableRequestHandler(
        function (Request $request) use ($app) {
            $response = yield $app->dispatch($request);

            return new Response(Status::OK, [], $response);
        })
    );

    yield $server->start();
});

كل شيء بسيط للغاية: قم بتحميل Applicationوإنشاء تجمع مأخذ توصيل (واحد أو أكثر).

بعد ذلك ، نبدأ الخادم الخاص بنا ، ونقوم بتعيينه Handler، والذي سيتم تنفيذه في كل طلب وإرسال الطلب إلى موقعنا Applicationللحصول على رد.

آخر شيء فعله هو بدء تشغيل الخادم yield $server->start();.

في ReactPHP ستبدو متشابهة تقريبًا ، ولكن لن يكون هناك سوى 150 رد للخيارات المختلفة ، وهو أمر غير ملائم للغاية.

مشاكل


هناك العديد من المشكلات المتعلقة بالتزامن في PHP.

عدم وجود معايير . كل إطار عمل: Swoole أو ReactPHP أو AMPHP ، ينفذ واجهة Promise الخاصة به ، وهي غير متوافقة.

يمكن أن يتفاعل AMPHP نظريًا مع Promise من ReactPHP ، ولكن هناك تحذير. إذا لم يكن رمز ReactPHP مكتوبًا بشكل جيد للغاية ، وفي مكان ما يستدعي أو ينشئ ضمنيًا حلقة حدث ، فعندئذٍ يتبين أن حلقتين من الأحداث ستدوران في الداخل.

جافا سكريبت لديها معيار جيد للوعود / A + ينفذ Guzzle. سيكون من اللطيف أن تتبعه الأطر. لكن حتى الآن ليس هذا.

تسريبات الذاكرة. عندما نعمل في PHP في وضع FPM المعتاد ، قد لا نفكر في الذاكرة. حتى إذا نسي مطورو بعض الإضافات كتابة رمز جيد ، ونسي تشغيله من خلال Valgrind ، وفي مكان ما داخل الذاكرة يتدفق ، فلا بأس - سوف يتم مسح الطلب التالي ويبدأ من جديد. ولكن في الوضع غير المتزامن ، لا يمكنك تحمل ذلك ، لأننا عاجلاً أم آجلاً سوف نسقط ببساطة OutOfMemoryException.

من الممكن إصلاحها ، لكنها صعبة ومؤلمة. في بعض الحالات ، يساعد Xdebug ، في حالات أخرى ، على تحليل الأخطاء التي تسببت فيها OutOfMemoryException.

عمليات الحظر . من الضروري عدم حظر حلقة الأحداث عندما نكتب رمزًا غير متزامن. يتباطأ التطبيق بمجرد عرقلة تدفق التنفيذ ، يبدأ كل كوروتينا في العمل بشكل أبطأ.

ستساعد حزمة kelunik / loop-block في العثور على مثل هذه العمليات لـ AMPHP . يقوم بتعيين المؤقت على فاصل زمني صغير جدًا. إذا لم يعمل المؤقت ، فسنحظر في مكان ما. تساعد الحزمة في العثور على أماكن الحظر ، ولكن ليس دائمًا: قد لا يتم ملاحظة الحظر في بعض الامتدادات.

دعم المكتبة: Cassandra، Influx، ClickHouse . المشكلة الرئيسية لجميع PHP غير المتزامن هو دعم المكتبات. لا يمكننا استخدام المعتادة PDOConnection، RedisClientالسائقين الآخرين لل جميع - نحتاج تطبيقات غير مؤمن. يجب أيضًا كتابتها في PHP في وضع عدم الحظر ، لأن برامج تشغيل C نادرًا ما توفر واجهات يمكن دمجها في التعليمات البرمجية غير المتزامنة.

أغرب تجربة حصلت عليها مع السائق لقاعدة بيانات كاساندرا. أنها توفر عملياتExecuteAsync، GetAsyncوآخرون ، ولكن في نفس الوقت يعيدون كائنًا Futureبطريقة واحدة يتم getحظرها. هناك فرصة للحصول على شيء ما بشكل غير متزامن ، ولكن لانتظار النتيجة ، سنستمر في حظر الحلقة بالكامل. للقيام بذلك بطريقة مختلفة ، على سبيل المثال ، من خلال الاسترجاعات ، لا يعمل. لقد كتبت حتى موكلي لـ Cassandra ، لأننا نستخدمها في عملنا.

إشارة النوع . هذه مشكلة AMPHP و corutin.

class UserRepository
{
    public function find(int $id): \Generator
    {
        $data = yield $this->db->query('SELECT ...', $id);

        return User::fill($data);
    }
}

إذا حدث في دالة yield، فإنه يصبح مولدًا. في هذه المرحلة ، لم يعد بإمكاننا تحديد أنواع بيانات الإرجاع الصحيحة.

PHP 8


ماذا ينتظرنا في PHP 8؟ سأخبرك عن افتراضاتي أو بالأحرى رغباتي ( ملاحظة المحرر: ديمتري ستوجوف يعرف ما سيظهر بالفعل في PHP 8 ).

حلقة الحدث هناك احتمال أن تظهر ، لأن العمل جار لإحضار Event Loop بشكل ما إلى النواة. إذا حدث ذلك ، فسيكون لدينا وظيفة await، مثل JavaScript أو C # ، والتي ستسمح لنا بانتظار نتيجة العملية غير المتزامنة في مكان معين. في هذه الحالة ، لن نحتاج إلى أي ملحقات ، كل شيء سيعمل بشكل غير متزامن على مستوى النواة.


class UserRepository
{
    public function find(int $id): Promise<User>
    {
        $data = await $this->db->query('SELECT ...', $id);

        return User::fill($data);
    }
}


الوراثة Go ينتظر Generics ، نحن ننتظر Generics ، والجميع ينتظر Generics.

class UserRepository
{
    public function find(int $id): Promise<User>
    {
        $data = yield $this->db->query('SELECT ...', $id);

        return User::fill($data);
    }
}

لكننا لا ننتظر Generics للمجموعات ، ولكن للإشارة إلى أن نتيجة Promise ستكون بالضبط كائن المستخدم.

لماذا كل هذا؟

للسرعة والأداء.
لغة PHP هي لغة تكون معظم العمليات فيها مرتبطة بإدخال / إخراج. نادرًا ما نكتب رمزًا مرتبطًا بشكل كبير بالحسابات في المعالج. على الأرجح ، نعمل مع مآخذ: نحن بحاجة إلى تقديم طلب إلى قاعدة البيانات ، وقراءة شيء ما ، وإرجاع استجابة ، وإرسال ملف. يسمح لك عدم التزامن بتسريع مثل هذا الرمز. إذا نظرنا إلى متوسط ​​وقت الاستجابة لـ 1000 طلب ، يمكننا التسريع بنحو 8 مرات ، وب 10000 طلب بنحو 6 طلبات!

13 مايو 2020 ، سوف نجتمع للمرة الثانية في PHP روسيا لمناقشة اللغة والمكتبات والأطر وطرق زيادة الإنتاجية ومزالق حلول الضجيج. لقد قبلنا التقارير الأربعة الأولى ، ولكن لا تزال دعوة للأوراق قادمة. قدم طلبك إذا كنت ترغب في مشاركة تجربتك مع المجتمع.

Source: https://habr.com/ru/post/undefined/


All Articles