Разбиране на структурата на асинхронните рамки за Python

Съдържанието на статията

Асинхронните приложения са типичен пример за това, което казват „Новото е добре забравено старо“. Е, да, самият подход се появи отдавна, когато беше необходимо да се подражава на паралелното изпълнение на задачи на едноядрени процесори и стари архитектури. Но пясъкът е лош заместител на овеса, „асинхронност“ и „паралелизъм“ са по-скоро ортогонални понятия и единият подход не решава проблема на другия. Независимо от това, асинхронността намери отлично приложение в нашето силно натоварено време на бързи интернет услуги с хиляди и стотици хиляди клиенти, които чакат услуга едновременно. Може би си струва да разберем по-добре как работи всичко.?

Защо е асинхронно?

Ако слезете почти до самото дъно, до ядрото на операционната система, тогава програмистът, честно казано, има само две възможности за работа с сокет - синхронен и асинхронен.

При синхронното като цяло всичко е ясно - дойде клиент, отвори се сокет, предадоха се данни, ако това е всичко - сокетът беше затворен. В този случай, докато не завършим локалния диалог с един клиент, не можем да го стартираме с друг. Това е принципът, използван от обикновените сървъри, които не трябва да хостват стотици и хиляди клиенти. Ако натоварването се увеличи, но не е критично, можете да създадете една или повече нишки (или дори процеси) и да обработвате връзки и в тях. Това е изпитан през годините подход, стабилно работещ, който например се използва от сървъра Apache - без изненади, данните от клиенти се обработват в строга опашка, а в случай на изпълнение на „дълги“ код - например някои изчисления или трудна заявка в базата данни - все още няма ефект върху други клиенти.

Но има проблем: сървърът не е хидра на Lernaean, той не може да създава нишки и процеси завинаги - в края на краищата има доста осезаеми ресурси, които се изразходват при всяко подобно действие и има горен праг за използването на тези ресурси. И тогава всички изведнъж се сетиха за асинхронността и системните повиквания за неблокиращ I/O. Защо да произвеждате куп сокети и нишки, да изяждате ресурси, ако можете едновременно да слушате данни от много клиенти в един сокет едновременно?

Всичко започна със системни разговори

Всъщност няма толкова много опции за системни повиквания за неблокираща работа с мрежови вход/изход (въпреки че те се различават леко от платформа до платформа). Първото, основно, би могло да се каже ветеран, е системното повикване select (), което се появи още през брадатите осемдесетте години заедно с първата версия на това, което сега се нарича POSIX сокети (т.е. сокети по разбирането на повечето съвременни сървъри системи), а след това се наричаше Berkeley sockets, Berkeley sockets.

Като цяло, по време на описанието на системното повикване select (), много малко хора се замисляха върху факта, че някой ден приложенията могат да станат ТОЛКОВО заредени. Всъщност всичко, което може да направи това обаждане, е да приеме фиксиран номер (по подразбиране не повече от 1024) файлови дескриптори, уникално описани в програмата, и да слуша събития в тях. Когато дескрипторът е готов за четене или писане, съответният метод за обратно извикване в кода ще бъде стартиран.

Защо select () все още съществува и се използва?

Е, първо, той съществува именно защото съществува, колкото и да звучи странно - select () се поддържа от почти всяка възможна и немислима софтуерна платформа, която обикновено включва мрежи. И второ, има, да кажем, „градска легенда“, че поради проста, като изпълнение на брадва, тази системна заявка за части от архитектури (които не включват широко използвани персонални компютри или дори сървъри) има феноменално време точност на обработката. -аут (до наносекунди). Може би, когато работите в космически изследвания или ядрена енергетика, това ще спаси нечий живот? Кой знае.

Тогава някой си помисли, че би било хубаво да се научи как да прави наистина мрежови приложения с голям товар по начин за възрастни и се появи системното повикване anketa (). Между другото, в Linux той съществува отдавна, но в Windows не съществува до Windows Vista. Вместо разпръснати сокети, това повикване приема за вход структура със списък с дескриптори (всъщност с произволен размер, без ограничения) и възможни събития върху тях. Тогава системата започва да циклира през тази структура и да улавя събития.

Основният недостатък на извикването на poll () (въпреки че това несъмнено беше голяма стъпка напред в сравнение с select ()) е, че обхождането на структура с дескриптори е линейно от гледна точка на алгоритмите, тоест отнема O (n). И това се отнася не само за проследяване на събития, но и за реакцията към тях и освен това е необходимо да се прехвърля информация напред-назад от пространството на ядрото към потребителското пространство.

Но по-нататък във всяка операционна система те решиха да вървят по своя път. Това не означава, че подходите са специфично различни, но въпреки това стана малко по-трудно да внедрите междуплатформена асинхронна работа с сокети във вашата програма. Под Windows се появи API за работа с така наречените IO Completion Ports, механизмът kqueue/kevent беше добавен към BSD системите и системното повикване на epoll започна да работи на Linux, започвайки с ядро ​​2.5.44. Улавянето на асинхронни събития в сокетите (колкото и тавтологично да звучи) е станало асинхронно само по себе си, тоест вместо да обхожда структури, операционната система е в състояние да сигнализира за събитие на програмата почти веднага след настъпването на събитието. Освен това по всяко време могат да се добавят и премахват гнезда за наблюдение. Това са самите технологии, които са широко използвани днес в повечето мрежови рамки.