البرمجة غير المتزامنة
نريد من اللوحة أن تومض، وتراقب زرا، وتجيب على المتصفّح، وكل ذلك معا. لكنها تملك برنامجا واحدا يسير سطرا بعد سطر. في الفصل 4 حللنا مسألة صغيرة كهذه بعدّاد زمن لكل مهمة، وهو حل يتعقّد بسرعة. هنا نتعلم الأداة التي صُنعت لهذا: مهام تتنازل عن الدور حين تنتظر، فتبدو كأنها تعمل في وقت واحد.
- نشرح لماذا تعطّل الأوامر الحاجبة كل ما حولها.
- نكتب مهام بالكلمتين
asyncوawait، ونشغّلها بالدوالasyncio.runوgatherوcreate_task. - نحسب الزمن الكلي، ونتنبأ بترتيب الأحداث في المُخرَج.
- نربط مهمتين بالصنف
Event. - نبني نظاما فيه ثنائي باعث للضوء وزر وحسّاس وخادم.
- نتجنّب المصيدتين: أمر حاجب داخل مهمة، ومهمة منسية.
- نختار بين المقاطعة والمؤقّت و
asyncioبحسب المسألة.
19.1 المشكلة: انتظار يوقف كل شيء – הבעיה: חסימה (Blocking)
أمر مثل time.sleep(1) هو أمر حاجب (פקודה חוסמת): ما دام ينتظر، لا يُنفَّذ أي سطر آخر. لنضغط الزر لمدة 0.3 s في برنامج يومض كل ثانية:
import time
from machine import Pin
import board
led = Pin(board.LED, Pin.OUT)
btn = Pin(board.BTN, Pin.IN, Pin.PULL_UP)
t0 = time.ticks_ms()
for i in range(4):
led.value(1 - led.value())
time.sleep(1)
now = time.ticks_diff(time.ticks_ms(), t0) / 1000
print("%.1f s button %d" % (now, btn.value()))1.0 s button 1 2.0 s button 1 3.0 s button 1 4.0 s button 1
البرنامج يقرأ الزر عند كل ثانية كاملة فقط، والضغطة وقعت بين قراءتين. لاحظوا أن معظم زمن اللوحة يضيع هنا في انتظار لا يحتاج إلى أي حساب. والفكرة كلها في جملة واحدة: من ينتظر يترك الدور لغيره.
19.2 المهمة: async وawait – משימה: async ו-await (Coroutines)
await، وهناك تُرجع الدور.asyncio يحفظ قائمة المهام ووقت استيقاظ كل منها، ويعطي الدور للمهمة الجاهزة.| الأمر | عمله |
|---|---|
| async def f(): | دالة غير متزامنة. استدعاؤها لا يشغّلها، بل يعطينا كائنا ننتظره. |
| await x | انتظر x واترك الدور في هذه الأثناء. |
| await asyncio.sleep(s) | انتظار لا يحجب: هذه المهمة تنام، وغيرها يعمل. |
| asyncio.run(main()) | يشغّل المجدول حتى تنتهي main. |
| asyncio.gather(a, b) | يشغّل المهام معا، وينتهي حين تنتهي كلها، ويعيد قائمة بنتائجها. |
import asyncio
import time
t0 = time.ticks_ms()
def stamp():
return time.ticks_diff(time.ticks_ms(), t0) / 1000
async def work(name, n, gap):
for i in range(n):
print("%.1f %s %d" % (stamp(), name, i))
await asyncio.sleep(gap)
async def main():
await asyncio.gather(work("A", 3, 1.0), work("B", 3, 0.6))
print("%.1f done" % stamp())
asyncio.run(main())0.0 A 0 0.0 B 0 0.6 B 1 1.0 A 1 1.2 B 2 2.0 A 2 3.0 done
حين تصل A إلى await asyncio.sleep يبدأ دور B، وعند 0.6 s تستيقظ B ولا تحتاج إلى أي شيء من A. لا يوجد هنا تنفيذ متزامن حقيقي: سطر واحد فقط يعمل في كل لحظة.
| الرمز | المعنى | وحدة القياس |
|---|---|---|
| TA | مجموع زمن انتظار المهمة الأولى | s |
| TB | مجموع زمن انتظار المهمة الثانية | s |
gather؟وهذا هو السطر الأخير في مُخرَج البرنامج. أزمنة الحساب بين الانتظارين صغيرة، ولذلك نهملها.
B إلى 1.5 s. متى تنتهي المهمتان؟19.3 إنشاء مهمة: create_task – יצירת משימה (create_task)
الدالة gather تنتظر الجميع. أحيانا نريد أن نطلق مهمة ونكمل عملنا، ثم ننتظرها لاحقا أو لا ننتظرها أبدا. هذا عمل create_task. وهنا أهم درس عن الترتيب:
A لا تبدأ عند إنشائها، بل عند أول await في main.import asyncio
async def job(name):
print(name, "starts")
await asyncio.sleep(1)
print(name, "ends")
async def main():
t = asyncio.create_task(job("A"))
print("main: task created")
await asyncio.sleep(0.1)
print("main: back after 0.1 s")
await t
print("main: A finished")
asyncio.run(main())main: task created A starts main: back after 0.1 s A ends main: A finished
- الدالة
create_taskتضع المهمة في قائمة المجدول فقط. ولا تتغيّر المهمة الجارية إلا عندawait. - لذلك لا تقطع مهمةٌ مهمةً أخرى في منتصف سطر، ونستطيع أن نقرأ متغيّرا مشتركا ونكتبه بين انتظارين دون أي قفل.
- لإيقاف مهمة قبل نهايتها نستعمل
t.cancel()، وهو يرفع داخلها الاستثناءCancelledError.
await asyncio.sleep(0.1) من البرنامج، فما السطر الثاني الذي يطبعه البرنامج؟ ولماذا؟19.4 مهمة تنبّه مهمة: Event – אירוע (Event)
مهمة تراقب الزر، ومهمة أخرى تنفّذ العمل. كيف تخبرها دون أن تسأل باستمرار؟ بالحدث (אירוע): كائن فيه علامة واحدة، set() يرفعها، وwait() ينتظرها، وclear() يخفضها.
import asyncio
from machine import Pin
import board
led = Pin(board.LED, Pin.OUT)
btn = Pin(board.BTN, Pin.IN, Pin.PULL_UP)
pressed = asyncio.Event()
async def watch_button():
last = 1
while True:
now = btn.value()
if now == 0 and last == 1:
pressed.set()
last = now
await asyncio.sleep_ms(20)
async def worker():
while True:
await pressed.wait()
pressed.clear()
led.value(1 - led.value())
print("press: led", led.value())
async def main():
await asyncio.gather(watch_button(), worker())
asyncio.run(main())press: led 1 press: led 0 press: led 1 ...
pressed.clear() تبقى العلامة مرفوعة، فلا ينتظر wait() بعد ذلك، ويدور العامل دون توقف.Event: التوثيق يقول إن set() غير آمن من داخل المقاطعة، ويوجّهنا إلى الصنف asyncio.ThreadSafeFlag. والمحاكاة في هذا الكتاب لا توفّره.worker، بينما كانت مشغولة. كم مرة يُنفَّذ العمل؟19.5 ثنائي وزر وحسّاس وخادم – נורית, כפתור ושרת במקביל
ثلاث مهام، لكل واحدة إيقاعها، وعدّادان يشتركان فيهما. لا تعرف أي مهمة شيئا عن إيقاع الأخرى، وهذا هو ما كان صعبا بعدّادات الفصل 4:
import asyncio
from machine import Pin
import board
from convert import to_percent
led = Pin(board.LED, Pin.OUT)
btn = Pin(board.BTN, Pin.IN, Pin.PULL_UP)
pot = board.analog_in(board.POT)
blinks = 0
presses = 0
async def blink():
global blinks
while True:
led.value(1 - led.value())
blinks += 1
await asyncio.sleep_ms(300)
async def button():
global presses
last = 1
while True:
now = btn.value()
if now == 0 and last == 1:
presses += 1
last = now
await asyncio.sleep_ms(20)
async def report():
while True:
await asyncio.sleep(2)
level = to_percent(pot.read_u16())
print("blinks", blinks, "presses", presses, "pot", level)
async def main():
await asyncio.gather(blink(), button(), report())
asyncio.run(main())blinks 7 presses 2 pot 50 blinks 14 presses 3 pot 50 ...
والخادم مهمة أخرى. المكتبة تعطينا asyncio.start_server، وهي تستدعي دالة لكل متصفّح يتصل، وكل دالة منها مهمة مستقلة. البرنامج التالي لم يُشغَّل عند بناء الكتاب، لأن مُحاكاتنا لا توفّر هذه الدالة، وقد فحصناه بالترجمة فقط:
import asyncio
from machine import Pin
import board
import wifi
led = Pin(board.LED, Pin.OUT)
blinks = 0
async def blink():
global blinks
while True:
led.value(1 - led.value())
blinks += 1
await asyncio.sleep_ms(300)
async def handle(reader, writer):
await reader.readline()
while await reader.readline() not in (b"\r\n", b""):
pass
writer.write(b"HTTP/1.1 200 OK\r\n\r\n")
writer.write(b"blinks %d\n" % blinks)
await writer.drain()
writer.close()
await writer.wait_closed()
async def main():
wifi.connect()
await asyncio.start_server(handle, "0.0.0.0", 80)
await blink()
asyncio.run(main())- الخادم في الفصل 14 ينتظر عند
accept()ولا يفعل شيئا غيره. هنا يستمر الوميض بينما ينتظر الخادم. await reader.readline()وawait writer.drain()هما مكانا تسليم الدور عند الخادم.wifi.connectحاجبة، لكنها تعمل قبل بدء المهام الأخرى، فلا تعطّل شيئا.
async_system.py: كم وميضا نتوقع عند 2 s؟ قارنوا بالمُخرَج.19.6 مصيدتان – מלכודות (Pitfalls)
أمر حاجب داخل مهمة:
المهمة slow تستعمل هنا time.sleep بدلا من await asyncio.sleep:
import asyncio
import time
t0 = time.ticks_ms()
def stamp():
return time.ticks_diff(time.ticks_ms(), t0) / 1000
async def beat():
for i in range(6):
print("%.1f beat" % stamp())
await asyncio.sleep(0.5)
async def slow():
await asyncio.sleep(1.2)
print("%.1f slow starts" % stamp())
time.sleep(2)
print("%.1f slow ends" % stamp())
async def main():
await asyncio.gather(beat(), slow())
asyncio.run(main())0.0 beat 0.5 beat 1.0 beat 1.2 slow starts 3.2 slow ends 3.2 beat 3.7 beat 4.2 beat
المجدول لا يقطع أي مهمة: هو ينتظر أن تعيد الدور. فإذا استبدلنا السطر بـawait asyncio.sleep(2) عادت النبضات إلى إيقاعها:
0.0 beat 0.5 beat 1.0 beat 1.2 slow starts 1.5 beat 2.0 beat 2.5 beat 3.2 slow ends
requests.get من الفصلين 15 و18، وaccept()، وحلقة while True بلا await. وإذا كانت الحلقة طويلة ولا بد، نضع فيها await asyncio.sleep(0) حتى تترك الدور.مهمة منسية:
A لا تعمل إطلاقا، وB تبدأ ولا تنتهي. ولا توجد أي رسالة خطأ.import asyncio
async def job(name):
print(name, "starts")
await asyncio.sleep(1)
print(name, "ends")
async def main():
a = job("A")
asyncio.create_task(job("B"))
await asyncio.sleep(0.5)
print("main ends")
asyncio.run(main())
print("program ends")B starts main ends program ends
a = job("A"): الاستدعاء ينشئ كائنا فقط، سواء حفظناه أم لا. نحتاج إلىawaitأوcreate_task. وهذا أكثر خطأ نقع فيه في البداية.asyncio.runينتهي بنهايةmain: ولا ينتظر بقية المهام. لذلك تنتهيmainبـgatherأوawait tحين يهمّنا إكمالها.- مهمة ترفع استثناء: تموت وحدها، ويكمل الباقي. نضع ما قد يفشل في
try/exceptداخل المهمة، كما في بوت الفصل 18.
blink توقفت عن الوميض بعد أن أضفنا إليها سطر قراءة من UART بالأمر uart.readline() الذي ينتظر بيانات. ما السبب المحتمل؟19.7 متى نستعمل كل أداة؟ – מתי להשתמש בכל כלי
| الأداة | متى تعمل | تناسب | حدودها |
|---|---|---|---|
| المقاطعة (الفصل 5) | فور الحدث، وتقطع البرنامج | حدث قصير لا يجوز أن يضيع: نبضة، حافة | معالج قصير جدا، ولا انتظار داخله |
| المؤقّت (الفصل 4) | بدورة ثابتة، وتقطع البرنامج | عمل قصير منتظم: قراءة أو وميض | معالج قصير، وعدد المؤقّتات محدود |
ticks_diff في حلقة | حين تصل الحلقة إليها | مهمة أو اثنتان بسيطتان | تتعقّد مع كل مهمة جديدة |
asyncio | حين تعيد المهمة الدور | مهام كثيرة بطيئة تنتظر: شبكة، زر، شاشة | أمر حاجب واحد يعطّل الجميع |
والقاعدة العملية: زمن الاستجابة في asyncio لا يقل عن أطول مقطع بين await وawait. فإذا كان الحدث أقصر من ذلك فهو للمقاطعة، وتستطيع المقاطعة أن تسجّله في متغيّر تقرؤه مهمة لاحقا. وكثيرا ما نجمع الأدوات في برنامج واحد.
_thread، ولا نستعملها في هذا الكتاب لثلاثة أسباب: توثيقها يصفها بأنها تجريبية جدا وأن واجهتها لم تستقر بعد؛ والخيوط يمكن أن تتبدّل في أي سطر، فتحتاج البيانات المشتركة إلى أقفال وتظهر أخطاء يصعب تكرارها؛ ومُحاكاتنا لا تشغّلها، بينما كل برنامج في هذا الكتاب يُفحص بالتشغيل.أهمّ أفكار الفصل:
- المهام تتناوب على معالج واحد، ولا يتبدّل الدور إلا عند
await. - مع
gatherالزمن الكلي هو الأطول بين المهام، وليس مجموعها. create_taskيطلق مهمة لا تبدأ إلا عند أولawaitفي المهمة الجارية.Eventعلامة تنبّه مهمة، ونخفضها بـclear().- أمر حاجب واحد يوقف كل المهام، واستدعاء بلا
awaitلا يشغّل شيئا. - المقاطعة للحدث القصير، و
asyncioللانتظار الطويل.
ملخّص قوانين الفصل:
| القانون | الصيغة |
|---|---|
| الزمن الكلي بالتتابع | Tseq = TA + TB |
| الزمن الكلي مع gather | T = max(TA, TB) |
أوامر الفصل:
| الأمر | عمله |
|---|---|
| asyncio.run(main()) | يشغّل المجدول حتى تنتهي main. |
| await asyncio.sleep(s) | نوم لا يحجب؛ ومعه sleep_ms. |
| asyncio.gather(a, b) | ينتظر الجميع، ويعيد قائمة النتائج. |
| asyncio.create_task(c) | يطلق مهمة، ويعيد كائنا ننتظره أو نلغيه. |
| t.cancel() | يلغي المهمة. |
| asyncio.Event() | علامة، ولها set وwait وclear. |
| asyncio.start_server(f, host, port) | خادم يستدعي f(reader, writer) لكل اتصال. |
قاموس المصطلحات – מילון מונחים:
| بالعربية | بالعبرية | English |
|---|---|---|
| البرمجة غير المتزامنة | תכנות אסינכרוני | Asynchronous Programming |
| مهمة | משימה | Task |
| المجدول | מתזמן | Scheduler |
| أمر حاجب | פקודה חוסמת | Blocking Call |
| حدث | אירוע | Event |
main: back after 0.1 s وليس A starts، لأن المهمة A لا تأخذ الدور قبل أول await في main، وهو الآن await t.Event علامة وليس عدّادا.أسئلة للتمرين:
time.sleep(1) وawait asyncio.sleep(1)؟gather أزمنتها 1 s و4 s و2.5 s. ما الزمن الكلي؟ وما هو بالتتابع؟asyncio.run(main()) إذا كانت main تستدعي job() دون await، وليس فيها شيء آخر؟LED كل 500 ms وLED_EXT كل 130 ms، بدالة واحدة.import asyncio
from machine import Pin
import board
async def blink(pin, ms):
led = Pin(pin, Pin.OUT)
while True:
led.value(1 - led.value())
await asyncio.sleep_ms(ms)
async def main():
await asyncio.gather(blink(board.LED, 500),
blink(board.LED_EXT, 130))
asyncio.run(main())async_first.py نجعل فاصل A يساوي 0.5 s. رتّبوا أسطر المُخرَج قبل التشغيل.0.0 B 0
0.5 A 1
0.6 B 1
1.0 A 2
1.2 B 2
1.8 done
async_system.py نحذف await asyncio.sleep_ms(20) من المهمة button. ماذا يحدث؟import asyncio
from machine import Pin
import board
led = Pin(board.LED, Pin.OUT)
btn = Pin(board.BTN, Pin.IN, Pin.PULL_UP)
period = 500
async def blink():
while True:
led.value(1 - led.value())
await asyncio.sleep_ms(period)
async def button():
global period
last = 1
while True:
now = btn.value()
if now == 0 and last == 1:
period = 100 if period == 500 else 500
last = now
await asyncio.sleep_ms(20)
async def main():
await asyncio.gather(blink(), button())
asyncio.run(main())await، فلا تُكتب القيمة وتُقرأ في اللحظة نفسها.requests حاجبة. نقبل بذلك ونطيل زمن الاستطلاع، أو ننقل الوميض إلى مؤقّت ونفحص النتيجة على اللوحة، لأن زمن الطلب الحقيقي غير معروف من المحاكاة.