Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

العقود الآجلة وبناء جملة Async (Futures and the Async Syntax)

العناصر الأساسية للبرمجة غير المتزامنة (asynchronous programming) في Rust هي العقود الآجلة (futures) والكلمات المفتاحية async و await الخاصة بـ Rust.

الـ Future هو قيمة قد لا تكون جاهزة الآن ولكنها ستصبح جاهزة في مرحلة ما في المستقبل. (يظهر هذا المفهوم نفسه في العديد من اللغات، أحيانًا تحت أسماء أخرى مثل المهمة (task) أو الوعد (promise)). توفر Rust سمة (trait) تسمى Future كبنة بناء بحيث يمكن تنفيذ عمليات async مختلفة بهياكل بيانات مختلفة ولكن بواجهة مشتركة. في Rust، الـ futures هي أنواع تنفذ الـ Future trait. يحتفظ كل future بمعلوماته الخاصة حول التقدم الذي تم إحرازه وماذا يعني أن يكون “جاهزًا”.

يمكنك تطبيق الكلمة المفتاحية async على الكتل (blocks) والدوال (functions) لتحديد إمكانية مقاطعتها واستئنافها. داخل كتلة async أو دالة async، يمكنك استخدام الكلمة المفتاحية await لانتظار future (أي انتظار أن يصبح جاهزًا). أي نقطة تقوم فيها بانتظار future باستخدام await داخل كتلة أو دالة async هي نقطة محتملة لتوقف تلك الكتلة أو الدالة مؤقتًا واستئنافها. تسمى عملية التحقق من الـ future لمعرفة ما إذا كانت قيمته متاحة بعد بـ الاستطلاع (polling).

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

عند كتابة async Rust، نستخدم الكلمات المفتاحية async و await معظم الوقت. يقوم Rust بترجمتها إلى كود مكافئ باستخدام الـ Future trait، تمامًا كما يترجم حلقات for إلى كود مكافئ باستخدام الـ Iterator trait. نظرًا لأن Rust يوفر الـ Future trait، يمكنك أيضًا تنفيذه لأنواع البيانات الخاصة بك عند الحاجة. العديد من الدوال التي سنراها في هذا الفصل تعيد أنواعًا لها تنفيذاتها الخاصة لـ Future. سنعود إلى تعريف الـ trait في نهاية الفصل ونتعمق في كيفية عمله، لكن هذا القدر من التفاصيل كافٍ لمواصلة المضي قدمًا.

قد يبدو كل هذا مجردًا بعض الشيء، لذا دعنا نكتب أول برنامج async لنا: أداة بسيطة لكشط الويب (web scraper). سنمرر عنواني URL من سطر الأوامر، ونجلبهما معًا بالتزامن (concurrently)، ونعيد نتيجة أيهما ينتهي أولاً. سيحتوي هذا المثال على قدر لا بأس به من بناء الجملة الجديد، لكن لا تقلق - سنشرح كل ما تحتاج إلى معرفته أثناء المضي قدمًا.

أول برنامج Async لنا

للحفاظ على تركيز هذا الفصل على تعلم async بدلاً من التعامل مع أجزاء النظام البيئي (ecosystem)، قمنا بإنشاء حزمة (crate) تسمى trpl (وهي اختصار لـ “The Rust Programming Language”). تقوم بإعادة تصدير (re-export) جميع الأنواع والـ traits والدوال التي ستحتاجها، بشكل أساسي من حزم futures و tokio. حزمة futures هي الموطن الرسمي لتجارب Rust لأكواد async، وهي في الواقع المكان الذي تم فيه تصميم الـ Future trait في الأصل. Tokio هو وقت التشغيل غير المتزامن (async runtime) الأكثر استخدامًا في Rust اليوم، خاصة لتطبيقات الويب. هناك أوقات تشغيل رائعة أخرى، وقد تكون أكثر ملاءمة لأغراضك. نحن نستخدم حزمة tokio داخليًا لـ trpl لأنها مختبرة جيدًا ومستخدمة على نطاق واسع.

في بعض الحالات، تقوم trpl أيضًا بإعادة تسمية أو تغليف واجهات برمجة التطبيقات (APIs) الأصلية لإبقائك مركزًا على التفاصيل ذات الصلة بهذا الفصل. إذا كنت تريد فهم ما تفعله الحزمة، فنحن نشجعك على مراجعة كود المصدر الخاص بها. ستتمكن من رؤية الحزمة التي يأتي منها كل re-export، وقد تركنا تعليقات مكثفة تشرح ما تفعله الحزمة.

أنشئ مشروعًا ثنائيًا (binary project) جديدًا باسم hello-async وأضف حزمة trpl كاعتمادية (dependency):

$ cargo new hello-async
$ cd hello-async
$ cargo add trpl

الآن يمكننا استخدام الأجزاء المختلفة التي توفرها trpl لكتابة أول برنامج async لنا. سنقوم ببناء أداة سطر أوامر بسيطة تجلب صفحتين ويب، وتسحب عنصر <title> من كل منهما، وتطبع عنوان الصفحة التي تنهي تلك العملية بالكامل أولاً.

تعريف دالة page_title

لنبدأ بكتابة دالة تأخذ عنوان URL لصفحة كـ parameter، وتقوم بإنشاء طلب (request) إليها، وتعيد نص عنصر <title> (انظر القائمة 17-1).

extern crate trpl; // required for mdbook test

fn main() {
    // TODO: we'll add this next!
}

use trpl::Html;

async fn page_title(url: &str) -> Option<String> {
    let response = trpl::get(url).await;
    let response_text = response.text().await;
    Html::parse(&response_text)
        .select_first("title")
        .map(|title| title.inner_html())
}

أولاً، نعرّف دالة باسم page_title ونميزها بالكلمة المفتاحية async. ثم نستخدم الدالة trpl::get لجلب أي عنوان URL يتم تمريره ونضيف الكلمة المفتاحية await لانتظار الاستجابة (response). للحصول على نص الـ response نطلب دالة text الخاصة بها وننتظرها مرة أخرى باستخدام الكلمة المفتاحية await. كلتا الخطوتين غير متزامنتين. بالنسبة لدالة get يتعين علينا انتظار الخادم لإرسال الجزء الأول من استجابته، والذي سيتضمن ترويسات HTTP (HTTP headers) وملفات تعريف الارتباط (cookies) وما إلى ذلك ويمكن تسليمها بشكل منفصل عن جسم الاستجابة (response body). خاصة إذا كان الجسم كبيرًا جدًا، فقد يستغرق وصوله بالكامل بعض الوقت. نظرًا لأنه يتعين علينا انتظار وصول الاستجابة بالكامل، فإن دالة text هي أيضًا async.

يتعين علينا انتظار كلا الـ futures صراحةً، لأن الـ futures في Rust كسولة (lazy): فهي لا تفعل أي شيء حتى تطلب منها ذلك باستخدام الكلمة المفتاحية await. (في الواقع، سيعرض Rust تحذيرًا من المترجم إذا لم تستخدم future). قد يذكرك هذا بمناقشة المكررات (iterators) في قسم “معالجة سلسلة من العناصر باستخدام المكررات” في الفصل 13. لا تفعل الـ iterators شيئًا ما لم تستدعِ دالة next الخاصة بها - سواء بشكل مباشر أو باستخدام حلقات for أو دوال مثل map التي تستخدم next داخليًا. وبالمثل، لا تفعل الـ futures شيئًا ما لم تطلب منها ذلك صراحةً. يسمح هذا الكسل لـ Rust بتجنب تشغيل كود async حتى تبرز الحاجة إليه فعليًا.

ملاحظة: هذا يختلف عن السلوك الذي رأيناه عند استخدام thread::spawn في قسم “إنشاء خيط جديد باستخدام spawn” في الفصل 16، حيث بدأت الـ closure التي مررناها إلى خيط (thread) آخر في العمل على الفور. كما أنه يختلف عن كيفية تعامل العديد من اللغات الأخرى مع async. ولكنه مهم لـ Rust لتكون قادرة على تقديم ضمانات الأداء الخاصة بها، تمامًا كما هو الحال مع الـ iterators.

بمجرد حصولنا على response_text يمكننا تحليلها إلى مثيل من نوع Html باستخدام Html::parse. بدلاً من سلسلة نصية خام (raw string)، أصبح لدينا الآن نوع بيانات يمكننا استخدامه للتعامل مع HTML كهيكل بيانات أغنى. على وجه الخصوص، يمكننا استخدام دالة select_first للعثور على أول مثيل لمحدد CSS (CSS selector) معين. من خلال تمرير السلسلة النصية "title" سنحصل على أول عنصر <title> في المستند، إذا وجد. نظرًا لأنه قد لا يكون هناك أي عنصر مطابق، فإن select_first تعيد Option<ElementRef>. أخيرًا، نستخدم دالة Option::map التي تتيح لنا التعامل مع العنصر الموجود في الـ Option إذا كان حاضرًا، وعدم فعل شيء إذا لم يكن كذلك. (يمكننا أيضًا استخدام تعبير match هنا، لكن map أكثر تعبيرًا (idiomatic)). في جسم الدالة التي نوفرها لـ map نستدعي inner_html على الـ title للحصول على محتواه، وهو String. عندما ينتهي كل شيء، يكون لدينا Option<String>.

لاحظ أن الكلمة المفتاحية await في Rust تأتي بعد التعبير الذي تنتظره، وليس قبله. أي أنها كلمة مفتاحية لاحقة (postfix). قد يختلف هذا عما اعتدت عليه إذا كنت قد استخدمت async في لغات أخرى، ولكن في Rust يجعل ذلك سلاسل الدوال (chains of methods) أجمل بكثير في التعامل معها. ونتيجة لذلك، يمكننا تغيير جسم page_title لربط استدعاءات دالتي trpl::get و text معًا مع وجود await بينهما، كما هو موضح في القائمة 17-2.

extern crate trpl; // required for mdbook test

use trpl::Html;

fn main() {
    // TODO: we'll add this next!
}

async fn page_title(url: &str) -> Option<String> {
    let response_text = trpl::get(url).await.text().await;
    Html::parse(&response_text)
        .select_first("title")
        .map(|title| title.inner_html())
}

بهذا، نكون قد كتبنا بنجاح أول دالة async لنا! قبل أن نضيف بعض الكود في main لاستدعائها، دعنا نتحدث قليلاً عن ما كتبناه وما يعنيه.

عندما يرى Rust كتلة (block) مميزة بالكلمة المفتاحية async فإنه يترجمها إلى نوع بيانات فريد ومجهول ينفذ الـ Future trait. وعندما يرى Rust دالة (function) مميزة بـ async فإنه يترجمها إلى دالة غير متزامنة (non-async function) يكون جسمها عبارة عن كتلة async. نوع الإرجاع لدالة async هو نوع البيانات المجهول الذي ينشئه المترجم لتلك الكتلة async.

وبالتالي، فإن كتابة async fn تعادل كتابة دالة تعيد future لنوع الإرجاع. بالنسبة للمترجم، فإن تعريف دالة مثل async fn page_title في القائمة 17-1 يعادل تقريبًا دالة غير متزامنة معرفة بهذا الشكل:

#![allow(unused)]
fn main() {
extern crate trpl; // required for mdbook test
use std::future::Future;
use trpl::Html;

fn page_title(url: &str) -> impl Future<Output = Option<String>> {
    async move {
        let text = trpl::get(url).await.text().await;
        Html::parse(&text)
            .select_first("title")
            .map(|title| title.inner_html())
    }
}
}

دعنا نمر عبر كل جزء من النسخة المحولة:

  • تستخدم بناء جملة impl Trait الذي ناقشناه سابقًا في الفصل 10 في قسم “السمات كمعاملات”.
  • القيمة المعادة تنفذ الـ Future trait مع نوع مرتبط (associated type) يسمى Output. لاحظ أن نوع الـ Output هو Option<String> وهو نفس نوع الإرجاع الأصلي من نسخة async fn لدالة page_title.
  • يتم تغليف كل الكود المستدعى في جسم الدالة الأصلية في كتلة async move. تذكر أن الكتل هي تعبيرات (expressions). هذه الكتلة بالكامل هي التعبير المعاد من الدالة.
  • تنتج كتلة async هذه قيمة من نوع Option<String> كما تم وصفه للتو. وتلك القيمة تطابق نوع الـ Output في نوع الإرجاع. هذا تمامًا مثل الكتل الأخرى التي رأيتها.
  • جسم الدالة الجديد هو كتلة async move بسبب كيفية استخدامه لـ parameter الـ url. (سنتحدث أكثر عن الفرق بين async و async move لاحقًا في الفصل).

الآن يمكننا استدعاء page_title في main.

تنفيذ دالة Async باستخدام وقت تشغيل (Executing an Async Function with a Runtime)

للبدء، سنحصل على العنوان لصفحة واحدة، كما هو موضح في القائمة 17-3. لسوء الحظ، هذا الكود لا يترجم (compile) بعد.

extern crate trpl; // required for mdbook test

use trpl::Html;

async fn main() {
    let args: Vec<String> = std::env::args().collect();
    let url = &args[1];
    match page_title(url).await {
        Some(title) => println!("The title for {url} was {title}"),
        None => println!("{url} had no title"),
    }
}

async fn page_title(url: &str) -> Option<String> {
    let response_text = trpl::get(url).await.text().await;
    Html::parse(&response_text)
        .select_first("title")
        .map(|title| title.inner_html())
}

نحن نتبع نفس النمط الذي استخدمناه للحصول على وسائط سطر الأوامر في قسم “قبول وسائط سطر الأوامر” في الفصل 12. ثم نمرر وسيط URL إلى page_title وننتظر النتيجة باستخدام await. نظرًا لأن القيمة التي ينتجها الـ future هي Option<String> فإننا نستخدم تعبير match لطباعة رسائل مختلفة بناءً على ما إذا كانت الصفحة تحتوي على <title>.

المكان الوحيد الذي يمكننا فيه استخدام الكلمة المفتاحية await هو داخل دوال أو كتل async، ولن يسمح لنا Rust بتمييز دالة main الخاصة بـ async.

error[E0752]: `main` function is not allowed to be `async`
 --> src/main.rs:6:1
  |
6 | async fn main() {

السبب في عدم إمكانية تمييز main بـ async هو أن كود async يحتاج إلى وقت تشغيل (runtime): حزمة Rust تدير تفاصيل تنفيذ الكود غير المتزامن. يمكن لدالة main في البرنامج أن تهيئ (initialize) وقت تشغيل، لكنها ليست وقت تشغيل بحد ذاتها. (سنرى المزيد حول سبب ذلك بعد قليل). كل برنامج Rust ينفذ كود async لديه مكان واحد على الأقل يقوم فيه بإعداد وقت تشغيل ينفذ الـ futures.

معظم اللغات التي تدعم async ترفق وقت تشغيل معها، لكن Rust لا تفعل ذلك. بدلاً من ذلك، هناك العديد من أوقات التشغيل غير المتزامنة المتاحة، كل منها يقدم مقايضات مختلفة مناسبة لحالة الاستخدام التي يستهدفها. على سبيل المثال، خادم ويب عالي الإنتاجية مع العديد من نوى المعالج (CPU cores) وكمية كبيرة من ذاكرة الوصول العشوائي (RAM) لديه احتياجات مختلفة تمامًا عن متحكم دقيق (microcontroller) بنواة واحدة وكمية صغيرة من RAM ولا توجد قدرة على تخصيص الذاكرة في الكومة (heap allocation). الحزم التي توفر أوقات التشغيل تلك غالبًا ما توفر أيضًا نسخ async من الوظائف الشائعة مثل إدخال/إخراج الملفات أو الشبكة (I/O).

هنا، وطوال بقية هذا الفصل، سنستخدم دالة block_on من حزمة trpl والتي تأخذ future كـ parameter وتحظر (blocks) الخيط الحالي حتى يكتمل تشغيل هذا الـ future. خلف الكواليس، يؤدي استدعاء block_on إلى إعداد وقت تشغيل باستخدام حزمة tokio المستخدمة لتشغيل الـ future الممرر (سلوك block_on في حزمة trpl مشابه لدوال block_on في حزم أوقات التشغيل الأخرى). بمجرد اكتمال الـ future، تعيد block_on أي قيمة أنتجها الـ future.

يمكننا تمرير الـ future المعاد من page_title مباشرة إلى block_on وبمجرد اكتماله، يمكننا إجراء مطابقة (match) على الـ Option<String> الناتج كما حاولنا أن نفعل في القائمة 17-3. ومع ذلك، بالنسبة لمعظم الأمثلة في الفصل (ومعظم أكواد async في العالم الحقيقي)، سنقوم بأكثر من مجرد استدعاء دالة async واحدة، لذا بدلاً من ذلك سنمرر كتلة async وننتظر صراحةً نتيجة استدعاء page_title كما في القائمة 17-4.

extern crate trpl; // required for mdbook test

use trpl::Html;

fn main() {
    let args: Vec<String> = std::env::args().collect();

    trpl::block_on(async {
        let url = &args[1];
        match page_title(url).await {
            Some(title) => println!("The title for {url} was {title}"),
            None => println!("{url} had no title"),
        }
    })
}

async fn page_title(url: &str) -> Option<String> {
    let response_text = trpl::get(url).await.text().await;
    Html::parse(&response_text)
        .select_first("title")
        .map(|title| title.inner_html())
}

عندما نشغل هذا الكود، نحصل على السلوك الذي توقعناه في البداية:

$ cargo run -- "https://www.rust-lang.org"
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.05s
     Running `target/debug/async_await 'https://www.rust-lang.org'`
The title for https://www.rust-lang.org was
            Rust Programming Language

أخيرًا - أصبح لدينا كود async يعمل! ولكن قبل أن نضيف الكود للتسابق بين موقعين، دعنا نوجه انتباهنا لفترة وجيزة إلى كيفية عمل الـ futures.

كل نقطة انتظار (await point) - أي كل مكان يستخدم فيه الكود الكلمة المفتاحية await - تمثل مكانًا يتم فيه إعادة التحكم إلى وقت التشغيل. لكي يعمل ذلك، يحتاج Rust إلى تتبع الحالة (state) المتضمنة في كتلة async بحيث يمكن لوقت التشغيل بدء عمل آخر ثم العودة عندما يكون جاهزًا لمحاولة دفع العمل الأول للأمام مرة أخرى. هذه آلة حالة (state machine) غير مرئية، كما لو كنت قد كتبت enum مثل هذا لحفظ الحالة الحالية عند كل نقطة انتظار:

#![allow(unused)]
fn main() {
extern crate trpl; // required for mdbook test

enum PageTitleFuture<'a> {
    Initial { url: &'a str },
    GetAwaitPoint { url: &'a str },
    TextAwaitPoint { response: trpl::Response },
}
}

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

في النهاية، يجب على شيء ما تنفيذ آلة الحالة هذه، وهذا الشيء هو وقت التشغيل. (هذا هو السبب في أنك قد تصادف ذكر المنفذين (executors) عند البحث في أوقات التشغيل: المنفذ هو جزء من وقت التشغيل المسؤول عن تنفيذ كود async).

الآن يمكنك أن ترى لماذا منعنا المترجم من جعل main نفسها دالة async في القائمة 17-3. إذا كانت main دالة async، فسيحتاج شيء آخر إلى إدارة آلة الحالة لأي future تعيده main ولكن main هي نقطة البداية للبرنامج! بدلاً من ذلك، استدعينا دالة trpl::block_on في main لإعداد وقت تشغيل وتشغيل الـ future المعاد من كتلة async حتى ينتهي.

ملاحظة: توفر بعض أوقات التشغيل وحدات ماكرو (macros) بحيث يمكنك كتابة دالة main بصيغة async. تقوم تلك الـ macros بإعادة كتابة async fn main() { ... } لتكون fn main عادية، والتي تفعل نفس الشيء الذي فعلناه يدويًا في القائمة 17-4: استدعاء دالة تشغل future حتى الاكتمال بالطريقة التي تفعلها trpl::block_on.

الآن دعنا نضع هذه الأجزاء معًا ونرى كيف يمكننا كتابة كود متزامن (concurrent code).

التسابق بين عنواني URL بالتزامن (Racing Two URLs Against Each Other Concurrently)

في القائمة 17-5، نستدعي page_title مع عنواني URL مختلفين يتم تمريرهما من سطر الأوامر ونسابق بينهما عن طريق اختيار أي future ينتهي أولاً.

extern crate trpl; // required for mdbook test

use trpl::{Either, Html};

fn main() {
    let args: Vec<String> = std::env::args().collect();

    trpl::block_on(async {
        let title_fut_1 = page_title(&args[1]);
        let title_fut_2 = page_title(&args[2]);

        let (url, maybe_title) =
            match trpl::select(title_fut_1, title_fut_2).await {
                Either::Left(left) => left,
                Either::Right(right) => right,
            };

        println!("{url} returned first");
        match maybe_title {
            Some(title) => println!("Its page title was: '{title}'"),
            None => println!("It had no title."),
        }
    })
}

async fn page_title(url: &str) -> (&str, Option<String>) {
    let response_text = trpl::get(url).await.text().await;
    let title = Html::parse(&response_text)
        .select_first("title")
        .map(|title| title.inner_html());
    (url, title)
}

نبدأ باستدعاء page_title لكل من عناوين URL التي قدمها المستخدم. نحفظ الـ futures الناتجة كـ title_fut_1 و title_fut_2. تذكر أن هذه لا تفعل أي شيء بعد، لأن الـ futures كسولة ولم ننتظرها بعد. ثم نمرر الـ futures إلى trpl::select التي تعيد قيمة تشير إلى أي من الـ futures الممررة إليها ينتهي أولاً.

ملاحظة: داخليًا، تم بناء trpl::select على دالة select أكثر عمومية معرفة في حزمة futures. يمكن لدالة select في حزمة futures القيام بالكثير من الأشياء التي لا تستطيع دالة trpl::select القيام بها، ولكنها تحتوي أيضًا على بعض التعقيدات الإضافية التي يمكننا تخطيها في الوقت الحالي.

يمكن لأي من الـ futures أن “يفوز” بشكل مشروع، لذا ليس من المنطقي إعادة Result. بدلاً من ذلك، تعيد trpl::select نوعًا لم نره من قبل، وهو trpl::Either. نوع Either يشبه إلى حد ما الـ Result في أن له حالتين. ولكن على عكس Result لا يوجد مفهوم للنجاح أو الفشل مدمج في Either. بدلاً من ذلك، يستخدم Left و Right للإشارة إلى “واحد أو الآخر”:

#![allow(unused)]
fn main() {
enum Either<A, B> {
    Left(A),
    Right(B),
}
}

تعيد دالة select القيمة Left مع مخرجات ذلك الـ future إذا فاز الوسيط الأول، و Right مع مخرجات الـ future الثاني إذا فاز ذلك الـ future. هذا يطابق الترتيب الذي تظهر به الوسائط عند استدعاء الدالة: الوسيط الأول يقع على يسار الوسيط الثاني.

نقوم أيضًا بتحديث page_title لتعيد نفس عنوان URL الممرر إليها. بهذه الطريقة، إذا كانت الصفحة التي تعود أولاً لا تحتوي على <title> يمكننا حله، فلا يزال بإمكاننا طباعة رسالة ذات معنى. مع توفر هذه المعلومات، نختتم بتحديث مخرجات println! للإشارة إلى عنوان URL الذي انتهى أولاً وما هو الـ <title> - إن وجد - لصفحة الويب عند عنوان URL هذا.

لقد قمت ببناء أداة كشط ويب صغيرة تعمل الآن! اختر عنواني URL وقم بتشغيل أداة سطر الأوامر. قد تكتشف أن بعض المواقع أسرع باستمرار من غيرها، بينما في حالات أخرى يختلف الموقع الأسرع من تشغيل لآخر. والأهم من ذلك، أنك تعلمت أساسيات التعامل مع الـ futures، لذا يمكننا الآن التعمق أكثر في ما يمكننا فعله باستخدام async.