Ta

Tasks در Elixir

2 تمرین

درباره‌ی Tasks

وظیفه‌ها فرایندهایی هستند که برای اجرای یک عملیات مشخص ساخته شده‌اند. آن‌ها معمولاً با سایر فرایندها ارتباط برقرار نمی‌کنند، اما می‌توانند نتیجه‌ای را به فرایندی که وظیفه را شروع کرده برگردانند.

از وظیفه‌ها معمولاً برای موازی‌سازی کار استفاده می‌شود.

async/await

برای شروع یک وظیفه، از Task.async/1 استفاده کنید. این تابع یک تابع بی‌نام را به عنوان آرگومان می‌گیرد و آن را در یک فرایند جدید اجرا می‌کند که به فرایند فراخوان متصل است. این تابع یک ساختار %Task{} برمی‌گرداند.

برای گرفتن نتیجه‌ی اجرا، ساختار %Task{} را به Task.await/2 بدهید. این تابع منتظر می‌ماند تا وظیفه تمام شود و نتیجه‌اش را برمی‌گرداند. آرگومان دوم یک مهلت بر حسب میلی‌ثانیه است که به‌طور پیش‌فرض ۵۰۰۰ است. مقدار :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 را برای هر وظیفه فقط یک بار می‌توان فراخوانی کرد.

اگر می‌خواهید وظیفه‌ای را فقط برای اثرهای جانبی شروع کنید، از 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]}

۹۰۰۴۵۱۷ میکروثانیه برابر ۹ ثانیه است، بسیار بیشتر از مهلت داده‌شده‌ی ۱ ثانیه (۱۰۰۰ میلی‌ثانیه).

اگر این همان رفتاری است که می‌خواهید، بسیار خوب، پیش بروید! اگر نه، جایگزین این است که از 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
ویرایش از طریق GitHub این پیوند در پنجره یا زبانه‌ی جدیدی باز می‌شود

Tasks را یاد بگیرید

تمرین کردن قفل شده است

برای تمرین Tasks قفل 1 تمرین دیگر را باز کنید