فقط توی توابعی که با async def ساخته شدن میتونی از await استفاده کنی.
اگه از یه کتابخونه سومشخص استفاده میکنی که با یه چیزی (مثل دیتابیس، API، سیستم فایل و غیره) ارتباط داره و از await پشتیبانی نمیکنه (که الان برای بیشتر کتابخونههای دیتابیس اینجوریه)، اون وقت توابع عملیات مسیرت رو عادی، فقط با def تعریف کن، اینجوری:
اگه برنامهات (به هر دلیلی) لازم نیست با چیز دیگهای ارتباط برقرار کنه و منتظر جوابش بمونه، از async def استفاده کن.
اگه نمیدونی چیکار کنی، از def معمولی استفاده کن.
توجه: میتونی توی توابع عملیات مسیرت هر چقدر که لازم داری def و async def رو قاطی کنی و هر کدوم رو با بهترین گزینه برات تعریف کنی. FastAPI خودش کار درست رو باهاشون انجام میده.
به هر حال، توی هر کدوم از موقعیتهای بالا، FastAPI هنوز ناهمزمان کار میکنه و خیلی خیلی سریع هست.
ولی با دنبال کردن مراحل بالا، میتونه یه سری بهینهسازی عملکرد هم بکنه.
کد ناهمزمان یعنی زبون 💬 یه راهی داره که به کامپیوتر / برنامه 🤖 بگه توی یه جای کد، باید منتظر بمونه تا یه چیز دیگه یه جای دیگه تموم بشه. فرض کن اون یه چیز دیگه اسمش "فایل-آروم" 📝 باشه.
پس، توی اون مدت، کامپیوتر میتونه بره یه کار دیگه بکنه، تا وقتی "فایل-آروم" 📝 تموم بشه.
بعدش کامپیوتر / برنامه 🤖 هر وقت فرصتی داشته باشه برمیگرده، چون دوباره منتظره، یا هر وقت همه کاری که اون لحظه داشته تموم کرده. و میبینه آیا کارایی که منتظرشون بوده تموم شدن یا نه، و هر کاری که باید بکنه رو انجام میده.
بعد، اون 🤖 اولین کاری که تموم شده (مثلاً "فایل-آروم" 📝 ما) رو برمیداره و هر کاری که باید باهاش بکنه رو ادامه میده.
این "منتظر یه چیز دیگه بودن" معمولاً به عملیات I/O اشاره داره که نسبتاً "آروم" هستن (نسبت به سرعت پردازنده و حافظه RAM)، مثل منتظر موندن برای:
دادههایی که از کلاینت از طریق شبکه فرستاده میشن
دادههایی که برنامهات فرستاده تا از طریق شبکه به کلاینت برسه
محتوای یه فایل توی دیسک که سیستم بخوندش و به برنامهات بده
محتوایی که برنامهات به سیستم داده تا توی دیسک بنویسه
یه عملیات API از راه دور
یه عملیات دیتابیس که تموم بشه
یه کوئری دیتابیس که نتایجش برگرده
و غیره.
چون زمان اجرا بیشتر صرف انتظار برای عملیات I/O میشه، بهشون میگن عملیات "I/O bound".
بهش "ناهمزمان" میگن چون کامپیوتر / برنامه لازم نیست با کار آروم "همزمان" باشه، منتظر لحظه دقیق تموم شدن کار بمونه، در حالی که هیچ کاری نمیکنه، تا نتیجه رو بگیره و کارش رو ادامه بده.
به جاش، چون یه سیستم "ناهمزمان" هست، وقتی کار تموم شد، میتونه یه کم توی صف منتظر بمونه (چند میکروثانیه) تا کامپیوتر / برنامه هر کاری که رفته بکنه رو تموم کنه، و بعد برگرده نتیجه رو بگیره و باهاش کار کنه.
برای "همزمان" (برخلاف "ناهمزمان") معمولاً از اصطلاح "ترتیبی" هم استفاده میکنن، چون کامپیوتر / برنامه همه مراحل رو به ترتیب دنبال میکنه قبل از اینکه بره سراغ یه کار دیگه، حتی اگه اون مراحل شامل انتظار باشن.
تصور کن تو توی این داستان کامپیوتر / برنامه 🤖 هستی.
وقتی توی صف هستی، فقط بیکاری 😴، منتظر نوبتت هستی، کار خیلی "مفیدی" نمیکنی. ولی صف سریع پیش میره چون صندوقدار فقط سفارش میگیره (آمادشون نمیکنه)، پس این خوبه.
بعد، وقتی نوبتت میشه، کار "مفید" واقعی میکنی، منو رو پردازش میکنی، تصمیم میگیری چی میخوای، انتخاب عشقت رو میگیری، پول میدی، چک میکنی اسکناس یا کارت درست رو دادی، چک میکنی درست حساب شده، چک میکنی سفارش آیتمای درست رو داره و غیره.
ولی بعد، گرچه هنوز برگرهات رو نداری، کارت با صندوقدار "موقتاً متوقف" ⏸ میشه، چون باید منتظر بمونی 🕙 تا برگرهات آماده بشن.
ولی وقتی از پیشخون دور میشی و با شماره نوبتت سر میز میشینی، میتونی توجهت رو 🔀 به عشقت بدی و "کار" ⏯ 🤓 رو اون بکنی. بعدش دوباره داری یه چیز خیلی "مفید" انجام میدی، مثل لاس زدن با عشقت 😍.
بعد صندوقدار 💁 با گذاشتن شمارهات رو نمایشگر پیشخون میگه "من با درست کردن برگرها تموم کردم"، ولی تو مثل دیوونهها وقتی شمارهات رو نمایشگر میاد فوری نمیپری. میدونی کسی برگرهات رو نمیدزده چون شماره نوبتت رو داری، و اونا هم مال خودشون رو دارن.
پس منتظر میمونی تا عشقت داستانش رو تموم کنه (کار فعلی ⏯ / وظیفهای که داره پردازش میشه 🤓)، آروم لبخند میزنی و میگی که میری برگرها رو بیاری ⏸.
بعد میری پیشخون 🔀، به کار اولیه که حالا تموم شده ⏯، برگرها رو میگیری، تشکر میکنی و میبرشون سر میز. این مرحله / وظیفه تعامل با پیشخون رو تموم میکنه ⏹. این به نوبه خودش یه وظیفه جدید، "خوردن برگرها" 🔀 ⏯، میسازه، ولی اون قبلی که "گرفتن برگرها" بود تموم شده ⏹.
توی صف وایمیستی در حالی که چند تا (مثلاً 8 تا) صندوقدار که همزمان آشپز هم هستن سفارش آدمای جلوی تو رو میگیرن.
همه قبل تو منتظرن برگرهاشون آماده بشه قبل از اینکه پیشخون رو ترک کنن، چون هر کدوم از 8 تا صندوقدار میره و برگر رو همون موقع درست میکنه قبل از اینکه سفارش بعدی رو بگیره.
بالاخره نوبت تو میشه، سفارش دو تا برگر خیلی شیک برای خودت و عشقت میدی.
پول رو میدی 💸.
صندوقدار میره آشپزخونه.
منتظر میمونی، جلوی پیشخون وایستادی 🕙، که کسی قبل از تو برگرهات رو نگیره، چون شماره نوبت نیست.
چون تو و عشقت مشغول این هستین که نذارین کسی جلوتون بیاد و هر وقت برگرها رسیدن اونا رو بگیره، نمیتونی به عشقت توجه کنی. 😞
این کار "همزمان" هست، تو با صندوقدار/آشپز 👨🍳 "همزمان" هستی. باید منتظر بمونی 🕙 و درست همون لحظه که صندوقدار/آشپز 👨🍳 برگرها رو تموم میکنه و بهت میده اونجا باشی، وگرنه ممکنه یکی دیگه اونا رو بگیره.
بعد صندوقدار/آشپزت 👨🍳 بالاخره بعد از یه مدت طولانی انتظار 🕙 جلوی پیشخون با برگرهات برمیگرده.
برگرهات رو میگیری و با عشقت میری سر میز.
فقط میخورینشون، و تمومه. ⏹
حرف زدن یا لاس زدن زیاد نبود چون بیشتر وقت صرف انتظار 🕙 جلوی پیشخون شد. 😞
توی این سناریوی برگرهای موازی، تو یه کامپیوتر / برنامه 🤖 با دو تا پردازنده (تو و عشقت) هستی، هر دو منتظر 🕙 و توجهشون ⏯ رو برای مدت طولانی "انتظار جلوی پیشخون" 🕙 گذاشتن.
فستفود 8 تا پردازنده (صندوقدار/آشپز) داره. در حالی که فستفود برگرهای همزمان شاید فقط 2 تا داشته (یه صندوقدار و یه آشپز).
ولی با این حال، تجربه نهایی بهترین نیست. 😞
این معادل موازی داستان برگرها بود. 🍔
برای یه مثال "واقعیتر" از زندگی، یه بانک رو تصور کن.
تا همین چند وقت پیش، بیشتر بانکها چند تا صندوقدار 👨💼👨💼👨💼👨💼 داشتن و یه صف بزرگ 🕙🕙🕙🕙🕙🕙🕙🕙.
همه صندوقدارها کار رو با یه مشتری بعد از اون یکی 👨💼⏯ انجام میدادن.
و باید توی صف 🕙 مدت زیادی منتظر بمونی وگرنه نوبتت رو از دست میدی.
احتمالاً نمیخوای عشقت 😍 رو با خودت ببری بانک 🏦 برای کارای روزمره.
توی این سناریوی "برگرهای فستفود با عشقت"، چون کلی انتظار 🕙 هست، خیلی منطقیتره که یه سیستم همزمان ⏸🔀⏯ داشته باشی.
این برای بیشتر برنامههای وب هم صدق میکنه.
خیلی خیلی کاربر، ولی سرورت منتظر 🕙 اتصال نهچندان خوبشون هست تا درخواستهاشون رو بفرستن.
و بعد دوباره منتظر 🕙 که جوابها برگردن.
این "انتظار" 🕙 توی میکروثانیهها اندازهگیری میشه، ولی با این حال، جمعش که بکنی آخرش کلی انتظار میشه.
برای همین استفاده از کد ناهمزمان ⏸🔀⏯ برای APIهای وب خیلی منطقیه.
این نوع ناهمزمانی چیزیه که NodeJS رو محبوب کرد (گرچه NodeJS موازی نیست) و نقطه قوت Go بهعنوان یه زبون برنامهنویسیه.
و همون سطح عملکردی هست که با FastAPI میگیری.
و چون میتونی همزمانی و موازیسازی رو همزمان داشته باشی، عملکرد بالاتری از بیشتر فریمورکهای تستشده NodeJS میگیری و همتراز با Go، که یه زبون کامپایلشده نزدیک به C هست (همه اینا به لطف Starlette).
همزمانی با موازیسازی فرق داره. و توی سناریوهای خاص که کلی انتظار دارن بهتره. به همین خاطر، معمولاً برای توسعه برنامههای وب خیلی از موازیسازی بهتره. ولی نه برای همهچیز.
برای اینکه یه تعادل بذاریم، این داستان کوتاه رو تصور کن:
باید یه خونه بزرگ و کثیف رو تمیز کنی.
آره، کل داستان همینه.
هیچ انتظاری 🕙 اونجا نیست، فقط کلی کار برای انجام دادن توی جاهای مختلف خونه.
میتونی مثل مثال برگرها نوبت بذاری، اول پذیرایی، بعد آشپزخونه، ولی چون منتظر چیزی نیستی 🕙، فقط داری تمیز میکنی و تمیز میکنی، نوبتها هیچ تأثیری نداره.
با نوبت یا بدون نوبت (همزمانی) همون قدر طول میکشه تا تمومش کنی و همون مقدار کار رو کردی.
ولی توی این موقعیت، اگه بتونی اون 8 تا صندوقدار/آشپز/حالا-تمیزکار رو بیاری، و هر کدومشون (بهعلاوه خودت) یه قسمت از خونه رو تمیز کنن، میتونی همه کار رو موازی انجام بدی، با کمک اضافی، و خیلی زودتر تمومش کنی.
توی این سناریو، هر کدوم از تمیزکارها (از جمله خودت) یه پردازندهست که کار خودش رو میکنه.
و چون بیشتر زمان اجرا صرف کار واقعی میشه (به جای انتظار)، و کار توی کامپیوتر با CPU انجام میشه، به این مشکلات میگن "CPU bound".
مثالهای رایج عملیات CPU bound چیزایی هستن که نیاز به پردازش ریاضی پیچیده دارن.
مثلاً:
پردازش صدا یا تصویر.
بینایی کامپیوتری: یه تصویر از میلیونها پیکسل تشکیل شده، هر پیکسل 3 تا مقدار / رنگ داره، پردازشش معمولاً نیاز داره چیزی رو رو اون پیکسلها همزمان حساب کنی.
یادگیری ماشین: معمولاً کلی ضرب "ماتریس" و "بردار" لازم داره. یه جدول بزرگ پر از عدد رو تصور کن که همهشون رو همزمان ضرب میکنی.
یادگیری عمیق: این یه زیرشاخه از یادگیری ماشینه، پس همون قضیه صدق میکنه. فقط این که یه جدول عدد برای ضرب کردن نیست، بلکه یه مجموعه بزرگ از اونا هست، و توی خیلی موارد از یه پردازنده خاص برای ساخت و / یا استفاده از این مدلها استفاده میکنی.
با FastAPI میتونی از همزمانی که برای توسعه وب خیلی رایجه (همون جذابیت اصلی NodeJS) استفاده کنی.
ولی میتونی از فواید موازیسازی و چندپردازشی (اجرای چند پروسه بهصورت موازی) برای کارای CPU bound مثل سیستمهای یادگیری ماشین هم بهره ببری.
این، بهعلاوه این واقعیت ساده که پایتون زبون اصلی برای علم داده، یادگیری ماشین و بهخصوص یادگیری عمیقه، باعث میشه FastAPI یه انتخاب خیلی خوب برای APIها و برنامههای وب علم داده / یادگیری ماشین باشه (بین خیلی چیزای دیگه).
برای دیدن اینکه چطور توی محیط واقعی به این موازیسازی برسی، بخش استقرار رو ببین.
نسخههای مدرن پایتون یه راه خیلی ساده و قابلفهم برای تعریف کد ناهمزمان دارن. این باعث میشه مثل کد "ترتیبی" معمولی به نظر بیاد و توی لحظههای درست "انتظار" رو برات انجام بده.
وقتی یه عملیاتی هست که قبل از دادن نتیجهها نیاز به انتظار داره و از این قابلیتهای جدید پایتون پشتیبانی میکنه، میتونی اینجوری کدنویسیش کنی:
burgers=awaitget_burgers(2)
نکته کلیدی اینجا await هست. به پایتون میگه که باید ⏸ منتظر بمونه تا get_burgers(2) کارش 🕙 تموم بشه قبل از اینکه نتیجهها رو توی burgers ذخیره کنه. با این، پایتون میدونه که میتونه بره یه کار دیگه 🔀 ⏯ توی این مدت بکنه (مثل گرفتن یه درخواست دیگه).
برای اینکه await کار کنه، باید توی یه تابع باشه که از این ناهمزمانی پشتیبانی کنه. برای این کار، فقط با async def تعریفش میکنی:
asyncdefget_burgers(number:int):# یه سری کار ناهمزمان برای ساختن برگرها انجام بدهreturnburgers
...به جای def:
# این ناهمزمان نیستdefget_sequential_burgers(number:int):# یه سری کار ترتیبی برای ساختن برگرها انجام بدهreturnburgers
با async def، پایتون میدونه که توی اون تابع باید حواسش به عبارتهای await باشه، و میتونه اجرای اون تابع رو "موقتاً متوقف" ⏸ کنه و بره یه کار دیگه 🔀 قبل از برگشتن بکنه.
وقتی میخوای یه تابع async def رو صدا کنی، باید "منتظرش" بمونی. پس این کار نمیکنه:
# این کار نمیکنه، چون get_burgers با async def تعریف شدهburgers=get_burgers(2)
پس، اگه از یه کتابخونه استفاده میکنی که بهت میگه میتونی با await صداش کنی، باید توابع عملیات مسیرت که ازش استفاده میکنن رو با async def بسازی، مثل:
شاید متوجه شده باشی که await فقط توی توابعی که با async def تعریف شدن میتونه استفاده بشه.
ولی در عین حال، توابعی که با async def تعریف شدن باید "منتظر"شون بمونی. پس توابع با async def فقط توی توابعی که با async def تعریف شدن میتونن صدا زده بشن.
حالا، قضیه مرغ و تخممرغ چیه، چطور اولین تابع async رو صدا میکنی؟
اگه با FastAPI کار میکنی، لازم نیست نگران این باشی، چون اون "اولین" تابع، تابع عملیات مسیرت هست، و FastAPI میدونه چطور کار درست رو بکنه.
ولی اگه بخوای بدون FastAPI از async / await استفاده کنی، اینم ممکنه.
Starlette (و FastAPI) بر پایه AnyIO هستن، که باعث میشه با کتابخونه استاندارد پایتون asyncio و Trio سازگار باشه.
بهخصوص، میتونی مستقیماً از AnyIO برای موارد استفاده پیشرفته همزمانی که نیاز به الگوهای پیچیدهتر توی کد خودت دارن استفاده کنی.
و حتی اگه از FastAPI استفاده نکنی، میتونی برنامههای ناهمزمان خودت رو با AnyIO بنویسی تا خیلی سازگار باشه و فوایدش رو بگیری (مثل همزمانی ساختاریافته).
من یه کتابخونه دیگه روی AnyIO ساختم، یه لایه نازک روش، تا یه کم annotationهای نوع رو بهتر کنم و تکمیل خودکار بهتر، خطاهای درونخطی و غیره بگیرم. یه مقدمه و آموزش ساده هم داره که بهت کمک میکنه بفهمی و کد ناهمزمان خودت رو بنویسی: Asyncer. اگه بخوای کد ناهمزمان رو با کد معمولی (بلاککننده/همزمان) ترکیب کنی خیلی بهدردت میخوره.
کروتین فقط یه اصطلاح خیلی شیک برای چیزیه که یه تابع async def برمیگردونه. پایتون میدونه که این یه چیزی مثل تابع هست، میتونه شروع بشه و یه جایی تموم بشه، ولی ممکنه داخلش هم موقف ⏸ بشه، هر وقت یه await توش باشه.
ولی همه این قابلیت استفاده از کد ناهمزمان با async و await خیلی وقتا خلاصه میشه به استفاده از "کروتینها". این قابل مقایسه با ویژگی اصلی Go، یعنی "Goroutineها" هست.
وقتی یه تابع عملیات مسیر رو با def معمولی به جای async def تعریف میکنی، توی یه استخر نخ خارجی اجرا میشه که بعدش منتظرش میمونن، به جای اینکه مستقیم صداش کنن (چون سرور رو بلاک میکنه).
اگه از یه فریمورک ناهمزمان دیگه میای که به روش بالا کار نمیکنه و عادت داری توابع عملیات مسیر ساده فقط محاسباتی رو با def معمولی برای یه سود کوچیک عملکرد (حدود 100 نانوثانیه) تعریف کنی، توجه کن که توی FastAPI اثرش کاملاً برعکسه. توی این موارد، بهتره از async def استفاده کنی مگه اینکه توابع عملیات مسیرت کدی داشته باشن که عملیات I/O بلاککننده انجام بده.
با این حال، توی هر دو موقعیت، احتمالش زیاده که FastAPI هنوز سریعتر از فریمورک قبلیات باشه (یا حداقل قابل مقایسه باهاش).
میتونی چند تا وابستگی و زیروابستگی داشته باشی که همدیگه رو نیاز دارن (بهعنوان پارامترهای تعریف تابع)، بعضیهاشون ممکنه با async def ساخته بشن و بعضیها با def معمولی. بازم کار میکنه، و اونایی که با def معمولی ساخته شدن توی یه نخ خارجی (از استخر نخ) صدا زده میشن به جای اینکه "منتظرشون" بمونن.
هر تابع کاربردی دیگهای که مستقیم خودت صداش میکنی میتونه با def معمولی یا async def ساخته بشه و FastAPI رو نحوه صدازدنش تأثیر نمیذاره.
این برخلاف توابعی هست که FastAPI برات صداشون میکنه: توابع عملیات مسیر و وابستگیها.
اگه تابع کاربردیت یه تابع معمولی با def باشه، مستقیم صداش میکنن (همونطور که توی کدت نوشتی)، نه توی استخر نخ، اگه تابع با async def ساخته شده باشه، باید وقتی توی کدت صداش میکنی awaitش کنی.