المهام عمليات غرضها تنفيذ عملية واحدة محددة. وهي عادةً لا تتواصل مع العمليات الأخرى، لكنها يمكن أن تُرجع نتيجة إلى العملية التي بدأت المهمة.
وتُستخدم المهام كثيرًا لموازاة العمل.
async/await
لبدء مهمة، استخدم Task.async/1. وهي تأخذ دالة مجهولة كوسيط وتنفّذها في عملية جديدة مرتبطة بعملية المستدعي. وتُرجع بنية %Task{}.
للحصول على نتيجة التنفيذ، مرّر بنية %Task{} إلى Task.await/2. ستنتظر حتى تنتهي المهمة وتُرجع نتيجتها. والوسيط الثاني هو مهلة زمنية بالمللي ثانية، وقيمتها الافتراضية 5000. ويمكن تمرير القيمة :infinity كمهلة زمنية للسماح للمهمة بالتنفيذ دون مهلة.
task = Task.async(fn -> Enum.sum(0..999_999) end)
# => %Task{
# owner: #PID<0.110.0>,
# pid: #PID<0.119.0>,
# ref: #Reference<0.3715954046.815529991.83334>
# }
Task.await(task)
# => 499999500000
لاحظ أنّ العملية التي بدأت المهمة ليست محجوبة في الفترة بين بدء المهمة وانتظارها، ويمكنها تنفيذ عمليات أخرى.
ينبغي انتظار أي مهمة تبدأ بـ Task.async/1 لأنها سترسل رسالة إلى عملية المستدعي. ولا يمكن استدعاء Task.await/2 لأي مهمة إلا مرة واحدة.
start/start_link
إذا أردت بدء مهمة من أجل الآثار الجانبية فقط، فاستخدم Task.start/1 أو Task.start_link/1. ستبدأ Task.start/1 مهمة غير مرتبطة بعملية المستدعي، وستبدأ Task.start_link/1 مهمة مرتبطة بعملية المستدعي. وتُرجعان كلتا الدالتين زوجًا {:ok, pid}.
من الأنماط الشائعة أن تبدأ مهام غير متزامنة ثم تنتظرها باستخدام Enum.map/2 على النحو التالي:
inputs
|> Enum.map(&Task.async(fn -> function.(&1) end))
|> Enum.map(&Task.await(&1, 1_000))
من المهم أن تدرك أنّ Task.await متزامنة، وأنها في كل مرة تُستدعى تبدأ عدّاد المهلة الزمنية من الصفر. وهذا يعني أنّ المهل الزمنية تتراكم عند استخدامها بهذه الطريقة المتتابعة.
لن تعمل المهمة الأولى التي يُنتظر انتهاؤها مدة تتجاوز المهلة الزمنية، أما المهمة الثانية التي يُنتظر انتهاؤها فستعمل بمقدار المدة التي عملتها المهمة الأولى زائد المهلة الزمنية.
تأمّل حالة تستغرق فيها كل مهمة وقتًا أطول قليلًا من المهمة السابقة، لكن الفرق في مدة تشغيل المهام أصغر من مهلة الانتظار.
inputs = 1..10
function = fn n -> :timer.sleep(n * 900) && n end
يمكننا استخدام :timer.tc/1، وهي دالة من Erlang، لقياس المدة التي ستعملها تلك المهام، بالميكروثانية.
:timer.tc(fn ->
inputs
|> Enum.map(&Task.async(fn -> function.(&1) end))
|> Enum.map(&Task.await(&1, 1_000))
end)
# => {9004517, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]}
9004517 ميكروثانية تعادل 9 ثوانٍ، وهو أكثر بكثير من المهلة الزمنية المعطاة والبالغة ثانية واحدة (1000 مللي ثانية).
إذا كان هذا هو السلوك الذي تريده، فهذا رائع، فامضِ قدمًا! وإن لم يكن، فالبديل هو استخدام Task.async_stream/3 أو Task.await_many/2.
Task.async_stream/3يتيح لك Task.async_stream/3 تشغيل الدالة نفسها على مدخلات مختلفة كثيرة، كل واحدة في مهمة خاصة بها. والمهام مرتبطة بالعملية الحالية، تمامًا كما هو الحال عند التشغيل بـ Task.async/1. وتُرجع تدفقًا.
وهو يقبل مهلة زمنية تُطبَّق على كل مهمة على حدة. تأمّل المدخلات نفسها والدالة نفسها عند استخدامها مع Task.async_stream/3:
inputs
|> Task.async_stream(function, timeout: 1_000)
|> Enum.to_list()
# => ** (exit) exited in: Task.Supervised.stream(1000)
# ** (EXIT) time out
# (elixir 1.11.3) lib/task/supervised.ex:304: Task.Supervised.stream_reduce/7
# (elixir 1.11.3) lib/enum.ex:3473: Enum.reverse/1
# (elixir 1.11.3) lib/enum.ex:3066: Enum.to_list/1
يُعدّ Task.async_stream/3 خيارًا ممتازًا إذا كانت لديك دالة واحدة تريد تشغيلها على مدخلات مختلفة. وهو يمنحك قدرًا كبيرًا من التحكم لأنه يقبل خيارات مفيدة أخرى، مثل عدد المهام التي تعمل في الوقت نفسه، وما يجب فعله عند انتهاء مهلة مهمة.
وإذا احتجت إلى تشغيل مهام تنفّذ عمليات مختلفة، فقد تحتاج إلى Task.await_many/2 بدلًا من ذلك.
Task.await_many/2طُرحت Task.await_many/2 في Elixir 1.11. وهي تتيح لك انتظار مهام كثيرة بمهلة زمنية مشتركة. تأمّل المدخلات نفسها والدالة نفسها عند استخدامها مع Task.await_many/2:
inputs
|> Enum.map(&Task.async(fn -> function.(&1) end))
|> Task.await_many(1_000)
# => ** (exit) exited in: Task.await_many([...], 1000)
# ** (EXIT) time out
# (elixir 1.11.3) lib/task.ex:725: Task.await_many/5
# (elixir 1.11.3) lib/task.ex:709: Task.await_many/2