Ачааллж байна...
Түвшин 5-д тавтай морил, энэ сургалтын сүүлчийн түвшин. Өмнөх дөрвөн түвшинд бидний бичсэн бүх програм нэг онцлогтой байсан: нэг зүйлийг нэг дор хийдэг. Хичээл 3.4-ийн log боловсруулагч мөр бүрийг дарааллаар нь уншсан. Түвшин 4-ийн импортлогч мөр бүрийг дарааллаар нь шалгасан. Дараагийн мөр өмнөх мөр дуусахыг хүлээсэн. Ихэнх програмд энэ хангалттай бөгөөд хамгийн ойлгомжтой.
Гэвч програм ихэнх хугацаагаа хүлээхэд зарцуулдаг ажлууд бий. Вэб сайтаас хуудас татах, мэдээллийн сангаас хариу авах, файлын системээс том файл унших. Ийм үед процессор юу ч хийхгүй сууж, гадны систем хариулахыг хүлээнэ. Хэрэв програм 100 хуудас татах ёстой бөгөөд тус бүр нэг секунд хүлээлгэдэг бол дарааллаар нь хийхэд 100 секунд. Гэвч тэр 100 секундийн бараг бүгд хоосон хүлээлт. Хэрэв хүлээлтүүдийг давхцуулж чадвал? Энэ асуулт бол concurrency буюу зэрэгцээ ажиллагааны эхлэл.
Энэ түвшинд бид Python-ий зэрэгцээ ажиллагааны гурван гол хэрэгслийг судална: thread, process, asyncio. Түвшин нэгдсэн төсөлөөр дуусна. Харин энэ анхны хичээлд код бичихээсээ өмнө ухагдахуунуудыг тодорхой болгоно, учир нь энэ сэдэвт буруу ойлголт хамгийн их тархсан. Хоёр ялгаатай ухагдахуун, concurrency ба parallelism. Програмын хоёр төрөл, I/O-bound ба CPU-bound. Мөн тэдгээрийг таамаглалгүйгээр ялгах хэмжилтийн арга. Төгсгөлд нь урьдчилсан харц: хамгийн энгийн thread-ээр хоёр төрлийг хэмжиж, үр дүн яагаад огт өөр болохыг харна.
Гэр бүлийн оройн хоолыг ганц хүн хийж байна гэж төсөөлөөд үзээрэй. Цэс: шарсан тахиа, будаа, салат. Хамгийн гэнэн арга: тахиаг зууханд хийгээд, 40 минут зуухны өмнө зогсож хүлээх. Дараа нь будаа агшаахад 20 минут, тогооны дэргэд зогсож хүлээх. Дараа нь салат хэрчих 10 минут. Нийт 70 минут, гэвч тогооч эхний 60 минутад бараг юу ч хийгээгүй, зөвхөн хүлээсэн.
Туршлагатай тогооч өөрөөр хийнэ. Тахиаг зууханд хийгээд, хүлээх зуураа будааг тавина. Будаа буцалж байх хооронд салатаа хэрчинэ. Нийт 40 орчим минут. Тогооч ганцаараа хэвээр: нэг удаад зөвхөн нэг зүйл хэрчинэ, хоёр гараараа хоёр өөр хоол хийхгүй. Гэвч ажлуудын хүлээлтийг давхцуулсан. Энэ бол concurrency: олон ажлыг нэг дор удирдах, хүлээлтийн хугацааг ашиглах.
Одоо өөр нөхцөл. Зочин олон ирэх тул 20 салат хэрчих хэрэгтэй. Энд хүлээлт байхгүй: хэрчих бол цэвэр гарын ажил. Ганц тогооч хичнээн ухаалаг зохион байгуулсан ч 20 салатыг хурдасгах арга байхгүй, хэрчих хугацаа өөрөө үргэлжилнэ. Цорын ганц хурдасгах арга: хоёр дахь тогооч авчирч, хоёулаа зэрэг хэрчих. Энэ бол parallelism: олон ажлыг нэг дор гүйцэтгэх, олон гүйцэтгэгчээр.
Програмчлалд тогооч бол процессор, хүлээлт бол I/O, хэрчих бол тооцоолол. Хоёр ухагдахууныг холих нь энэ сэдвийн хамгийн түгээмэл алдаа: concurrency хүлээлтийг шийддэг, parallelism ажлын хэмжээг шийддэг. Буруу хэрэгслийг буруу асуудалд хэрэглэвэл ямар ч ашиг гарахгүй, Хичээлийн төгсгөлд үүнийг хэмжиж харна.
Програмын ажлыг тэр ажил юуг хүлээдгээр нь хоёр ангилна.
I/O-bound ажил: хугацааныхаа ихэнхийг гадны системийг хүлээхэд зарцуулдаг. I/O бол “input/output”, оролт гаралт: сүлжээ, диск, мэдээллийн сан, хэрэглэгчийн оролт. “Bound” нь "хязгаарлагдсан": хурдыг нь I/O хязгаарладаг. Вэб хуудас татах бол I/O-bound. Тогоочийн зууханд тахиа шарахыг хүлээх.
CPU-bound ажил: хугацааныхаа ихэнхийг процессор дээр тооцоолоход зарцуулдаг. Том жагсаалт эрэмбэлэх, зураг боловсруулах, Хичээл 3.3-ын memo-гүй fib, Түвшин 4-ийн импортлогчийн квадрат хайлт. Тогоочийн салат хэрчих.
Хоёуланг нь жижиг функцээр загварчилъя. Бодит сүлжээний хүсэлтийн оронд time.sleep() хэрэглэнэ: тэр програмыг заасан хугацаанд зогсоож, процессорыг огт ашигладаггүй. Энэ бол хүлээлтийн цэвэр загвар, ажиллуулах бүрт ижил хугацаатай, интернэтээс хамааралгүй:
python
import time
def fake_download(name, seconds=1.0):
time.sleep(seconds)
return f"{name}: татагдлаа"
def crunch(n=5_000_000):
total = 0
for i in range(n):
total += i * i
return totalfake_download нэг секунд "сүлжээг хүлээнэ". crunch таван сая удаа үржүүлж нэмнэ: цэвэр тооцоолол.
Ажил I/O-bound уу, CPU-bound уу гэдгийг таамаглах шаардлагагүй. Хоёр өөр цагийг хамтад нь хэмжвэл ажил өөрөө хэлнэ. Эхнийх нь бидний танил time.perf_counter(): wall-clock time буюу "хананы цаг", хананд өлгөсөн цаг шиг бодит өнгөрсөн хугацаа. Хоёр дахь нь time.process_time(): зөвхөн процессор таны програмын төлөө бодитоор ажилласан хугацаа. Програм sleep хийж эсвэл сүлжээ хүлээж байхад хананы цаг явна, харин процессын цаг зогсоно.
python
def measure(label, func):
wall_start = time.perf_counter()
cpu_start = time.process_time()
func()
wall = time.perf_counter() - wall_start
cpu = time.process_time() - cpu_start
print(f"{label:<20} хана: {wall:5.2f} с CPU: {cpu:5.2f} с ({cpu / wall:.0%})")
def five_downloads():
for i in range(5):
fake_download(f"хуудас-{i}")
def five_crunches():
for _ in range(5):
crunch()
measure("5 татаж авалт", five_downloads)
measure("5 тооцоолол", five_crunches):.0% формат Хичээл 3.4-өөс: бутархайг хувь болгоно. Нэг компьютер дээрх нэг ажиллуулалт:
5 татаж авалт хана: 5.00 с CPU: 0.00 с (0%)
5 тооцоолол хана: 1.62 с CPU: 1.61 с (99%)Сүүлийн багана бол ангилал. Татаж авалт таван секунд үргэлжилсэн, гэвч процессор түүнд бараг тэг секунд зарцуулсан: 0 хувь. Програм бүх хугацаанд хүлээсэн. Тооцоолол 1.62 секунд үргэлжилсэн бөгөөд процессор бараг бүх хугацаанд ажилласан: 99 хувь.
Дүрэм энгийн. CPU/хана харьцаа бага бол ажил I/O-bound: хугацаа хүлээлтэд алга болж байна, concurrency тусална. Харьцаа 100 хувьд ойр бол CPU-bound: процессор бүрэн ачаалалтай, зөвхөн илүү олон процессор, parallelism, тусална. Бодит програмууд ихэвчлэн дунд нь байдаг: хуудас татаад задлах, файл уншаад тооцоолох. Тэр үед харьцаа аль хэсэг давамгайлж байгааг хэлнэ.
Энэ бол Бүлэг 12-ын хууль шинэ нөхцөлд: хэмжихгүйгээр бүү зэрэгцүүл. "Програм удаан, thread нэмье" гэсэн шийдвэр нь "энэ мөр удаан байх" гэсэн таамаглалтай ижил. Хоёр цагийн хэмжилт хэдхэн мөр код бөгөөд аль хэрэгсэл тус болохыг урьдчилан хэлнэ.
Хоёр дахь жишээ: бодит I/O. Хичээл 3.4-ийн log генераторын үүсгэсэн том файлыг уншиж, мөр тоолохыг хэмжиж үзэхэд дүн компьютероос их хамаарна. Анх удаа уншихад диск удаан тул CPU-ийн хувь бага. Дахин уншихад үйлдлийн систем файлыг санах ойд хадгалсан, cache хийсэн, байх тул бараг цэвэр тооцоолол болж хувь өсөнө. Нэг ажил нөхцлөөсөө хамаарч ангиллаа өөрчилж болно гэдэг нь яагаад таамаглахгүй, хэмжих ёстойг дахин харуулна.
Гурав дахь жишээ: холимог ажил. Хэрэв fake_download-ийн дараа татсан "хуудсыг" crunch-аар боловсруулбал таван удаагийн нийт хугацаа 6.6 секунд орчим, CPU ойролцоогоор 1.6 секунд, харьцаа 25 хувь гарна. Хугацааны дөрөвний гурав нь хүлээлт: concurrency энэ хэсгийг багасгаж чадна, үлдсэн дөрөвний нэгд тус болохгүй.
Дараагийн хичээлд thread-ийг бүрэн судална: thread бол нэг програм дотор зэрэг явж болох тусдаа гүйцэтгэлийн шугам. Одоохондоо хамгийн энгийн хэлбэрээр хоёр ажлын төрөлд хэрэглэж, үр дүнг хэмжье:
python
import threading
def in_threads(func, count=5):
def run():
threads = [threading.Thread(target=func) for _ in range(count)]
for thread in threads:
thread.start()
for thread in threads:
thread.join()
return run
measure("5 татаж авалт, thread", in_threads(lambda: fake_download("хуудас")))
measure("5 тооцоолол, thread", in_threads(crunch))threading.Thread(target=func) нь func-ийг ажиллуулах thread үүсгэнэ, гэхдээ хараахан эхлүүлэхгүй. start() эхлүүлнэ: func шинэ thread дээр ажиллаж эхэлмэгц start шууд буцаж, дараагийн thread-ийг эхлүүлнэ. join() тухайн thread дуусахыг хүлээнэ. Таван thread-ийг бүгдийг нь эхлүүлээд, дараа нь бүгдийг нь хүлээнэ. in_threads нь Бүлэг 4-ийн closure: func ба count-ыг хадгалсан run функц буцааж, measure-д дамжуулна. Нэг ажиллуулалт, стандарт Python дээр:
5 татаж авалт, thread хана: 1.00 с CPU: 0.00 с (0%)
5 тооцоолол, thread хана: 1.65 с CPU: 1.64 с (99%)Татаж авалт таван секундээс нэг секунд болсон: таван хүлээлт давхцсан, яг туршлагатай тогоочийн адил. Тооцоолол огт хурдлаагүй, бүр бага зэрэг удаан: 1.62-оос 1.65 секунд. Таван thread үүссэн, гэвч процессорын ажлыг тэд хуваалцаж чадаагүй.
Шалтгаан нь стандарт Python-ий GIL буюу “Global Interpreter Lock”, тайлбарлагчийн нийтлэг түгжээ: нэг агшинд зөвхөн нэг thread Python код гүйцэтгэх боломжтой. Thread хүлээж байхдаа, sleep эсвэл сүлжээнд, түгжээг суллаж, бусад thread ажиллана: тиймээс I/O-bound ажилд thread гайхалтай. Харин тооцоолол хийж буй thread түгжээг ээлжлэн дамжуулна, хэзээ ч хоёр нь зэрэг тооцоолохгүй: тогооч олон болсон ч ганц хутгатай мэт. Python-ий шинэ хувилбаруудад GIL-гүй “free-threaded” build бий болж, энэ зураг өөрчлөгдөж эхэлсэн. Бид GIL, түүний шалтгаан ба өнөөгийн байдлыг, мөн CPU-bound ажлыг жинхэнэ parallelism-оор хурдасгах process-ийг энэ бүлгийн гурав дахь хичээлд нарийвчлан судална.
Одоохондоо энэ хоёр мөр хэмжилтийн гол санаа: ижил хэрэгсэл нэг төрлийн ажлыг таван дахин хурдасгаж, нөгөөд нь юу ч өгөөгүй. Ажлын төрлийг мэдэхгүйгээр хэрэгсэл сонгох нь тааварт тулгуурлах.
Зэрэгцээ ажиллагаа үнэгүй биш бөгөөд энэ түвшин тэр үнийг нууцлахгүй. Дараалсан програмд мөр бүр тодорхой дарааллаар ажилладаг: дебаг хийхэд, trace хийхэд, ойлгоход хялбар. Thread-тэй програмд хоёр thread хэзээ, ямар дарааллаар ажиллахыг та хянадаггүй, үйлдлийн систем шийднэ. Хэрэв хоёр thread нэг объектыг зэрэг өөрчилбөл үр дүн ажиллуулах бүрт өөр байж болно. Энэ төрлийн алдааг race condition буюу "уралдааны нөхцөл" гэдэг бөгөөд програмчлалын хамгийн хэцүү олддог алдаануудын нэг: мянган ажиллуулалтад нэг удаа гарч, дебаггер нэмэхэд алга болдог. Дараагийн хичээлд бид түүнийг санаатайгаар үүсгэж, хэмжиж, засна.
Тиймээс энэ түвшний ерөнхий зарчим: зэрэгцээ ажиллагаа хэмжилтээр батлагдсан хэрэгцээтэй үед л. Програм хангалттай хурдан эсвэл ажил CPU-bound бол thread нэмэх нь зөвхөн нарийн төвөгтэй байдал нэмнэ.
Хүсвэл measure функцийг өөрийн өмнөх төслүүдийн нэг ажилд хэрэглэж, тэр нь I/O-bound уу, CPU-bound уу гэдгийг тодорхойлж үзээрэй: жишээлбэл Хичээл 12.4-ийн импортлогч эсвэл Course 3-ын API-д хүсэлт илгээдэг script. Таамаглалаа эхлээд бичээрэй. Сонирхвол in_threads-ийн count-ыг 5, 10, 50 болгож татаж авалтын хэмжилтийг давтаж, хананы цаг хэрхэн өөрчлөгдөж байгааг харж болно. Мөн fake_download ба crunch хоёрыг нэгтгэсэн холимог ажлыг thread-ээр ажиллуулж, хугацааны хэдэн хувь хэмнэгдсэнийг хэмжих нь CPU харьцааны таамаглалыг шалгах сайн арга. Эдгээр бүгд санал бөгөөд дараагийн хичээлүүд үүнийг хийсэн гэж үзэхгүй.
Функцийг дамжуулахын оронд дуудах. Мессежгүй, хамгийн түгээмэл алдаа. threading.Thread(target=fake_download("хуудас")) гэж бичихэд хаалт fake_download-ийг Thread үүсэхээс өмнө, үндсэн thread дээр дуудна: нэг секунд хүлээж, string буцаана, тэр string target болно. Таван thread таван секунд, дараалсан хувилбартай ижил. target-д функцийн нэрийг хаалтгүй өг. Аргумент хэрэгтэй бол threading.Thread(target=fake_download, args=("хуудас",)) эсвэл энэ хичээлийн адил lambda. args дахь таслалыг анзаараарай: нэг элементтэй tuple.
join()-ийг мартах. Мессежгүй. join-гүй бол run функц thread-үүдийг эхлүүлээд шууд буцна, measure "хана: 0.00 с" гэж харуулна: гайхалтай хурдан мэт, гэвч ажил хараахан дуусаагүй байна. Хэмжилт буруу, үр дүнг ашиглах код ч дуусаагүй ажлын үр дүнг уншина. Thread-ийн ажлын үр дүн хэрэгтэй бол үргэлж join.
RuntimeError: threads can only be started once. “Thread-ийг зөвхөн нэг удаа эхлүүлж болно” гэсэн утгатай. Нэг Thread объектыг дахин start хийхэд гарна. Thread объект нэг удаагийн: Бүлэг 2-ын generator-ийн адил дуусмагц дахин эхлэхгүй. Шинэ ажилд шинэ Thread үүсгэ.
Ажлын төрлийг хэмжилгүй thread нэмэх. Мессежгүй, сэтгэлгээний алдаа. Тооцооллын хэмжилт харуулсан шиг CPU-bound ажилд thread хурдасгахгүй бөгөөд race condition-ий эрсдэл ба нарийн төвөгтэй байдлыг үнэгүй нэмнэ. Эхлээд хоёр цагийг хэмж.
Богино ажлын process_time-д итгэх. Мессежгүй. Windows дээр process_time нарийвчлал бүдүүн байж болно: процессын цаг ойролцоогоор 15 миллисекунд тутамд л шинэчлэгддэг тул маш богино ажил “CPU: 0.00” харуулж болно, тэр нь CPU-bound байсан ч. Ангиллыг хангалттай урт, дор хаяж хагас секундийн ажил дээр хэмж.
Concurrency бол олон ажлыг нэг дор удирдаж, хүлээлтийг давхцуулах. Parallelism бол олон ажлыг олон гүйцэтгэгчээр нэг дор гүйцэтгэх. Эхнийх нь хүлээлтийг, хоёр дахь нь ажлын хэмжээг шийддэг. I/O-bound ажил хугацаагаа гадны систем, сүлжээ, диск, мэдээллийн санг хүлээхэд зарцуулдаг. CPU-bound ажил тооцоолоход зарцуулдаг. Ангиллыг таамаглахгүй: perf_counter-ийн хананы цаг ба process_time-ийн процессорын цагийг хамт хэмжиж, харьцааг нь харна. Бага харьцаа I/O-bound, 100 хувьд ойр харьцаа CPU-bound. Урьдчилсан хэмжилтэд thread таван хүлээлтийг нэг секундэд багтаасан, харин тооцооллыг огт хурдасгаагүй. Шалтгаан нь стандарт Python-ий GIL: нэг агшинд нэг thread л Python код гүйцэтгэдэг, хүлээж буй thread түгжээг сулладаг. Concurrency race condition гэх мэт шинэ төрлийн алдааг авчирдаг тул хэмжилтээр батлагдсан хэрэгцээтэй үед л хэрэглэнэ. Хэмжихгүйгээр бүү зэрэгцүүл.
Дараагийн хичээлд thread-ийг бүрэн эзэмшинэ. threading.Thread-ийг гараар удирдахын оронд стандарт сангийн ThreadPoolExecutor ажлын thread-үүдийг автоматаар удирдаж, үр дүнг буцааж, алдааг зөв дамжуулдаг. Бид олон "хуудас"-ыг зэрэг татаж, хэдэн thread хамгийн ашигтайг хэмжиж, дараа нь race condition-ийг санаатайгаар үүсгэж, тоологч буруу тоо гаргахыг өөрийн нүдээр харж, Lock-оор засна.
Бүртгэлтэй болсноор энэ сургалтын бүх хичээлд хандах эрх авна.