#elixirlang Task.await와 Task.yield

1 minute read

Boxes

defmodule InfoSys do
  # ...
  def compute(query, opts \\ []) do
    # ...
    backends
    |> Enum.map(&async_query(&1, query, opts))
    |> Task.yield_many(timeout)
    |> Enum.map(fn {task, res} -> res || Task.shutdown(task, :brutal_kill) end)
    # ...
  end

  # ...
end

programming elixir 1.4 책에서 본 소스 코드 일부분을 발췌했다. 질문을 입력하면 backends로 등록한 wolfram alpha, google 등에 질의를 하고 답변을 출력한다. Task.yield/2, Task.yield_many/2 함수를 쓴 좋은 예제라서 가져왔다. 태스크(task)는 함수를 별도의 erlang 프로세스(process)를 통해 실행한다. erlang 프로세스 간 통신이 필요 없고 시간이 걸리는 특정 작업을 할 때, 유용하게 사용할 수 있다.

대략적인 내용은 다음과 같다. async_query 함수에서 Task.async/1 함수를 호출한다. Task.yield_many/2 함수를 통해 태스크가 완료됐는지 timeout 만큼 기다리고 그때까지 끝나지 않은 Task는 Task.shutdown/2 함수로 태스크를 종료한다. 질의에 대한 응답을 리턴하기 때문에 이후에 온 응답은 쓸데가 없기 때문이다.

iex> Task.async(fn -> Process.sleep(5000) end) |> Task.await(1000)
** (exit) exited in: Task.await(%Task{owner: #PID<0.116.0>, pid: #PID<0.119.0>, ref: #Reference<0.3797298917.3740008452.20336>}, 1000)
    ** (EXIT) time out
    (elixir) lib/task.ex:607: Task.await/2
iex> Task.async(fn -> Process.sleep(5000) end) |> Task.yield(1000)
nil

Task.async/1 함수로 만든 태스크의 결과 값을 Task.await/2 함수로 받을 수도 있고 Task.yield/2 함수로 받을 수도 있다. 다른 점이 몇 가지 있다. 그중 하나가 timeout에 대한 동작이다. Task.await/2 함수 호출 후 timeout이 걸리면 에러가 난다. 반면 Task.yield/2 함수는 nil을 리턴한다. 외부 서비스에 질의를 보내고 응답을 추려서 사용자에게 보여주는 코드에선 Task.yield/2 함수를 사용하는 게 더 적절하다. 외부 서비스에 보내는 질의에 timeout 발생하는지 여부가 프로그래머 손을 떠났기 때문이다.

timeout에 다른 동작을 보면 함수에서 의도한 호출 횟수를 알 수 있다. Task.await/2 함수는 한 번만 호출하는 의도로 만들었고 Task.yield/2 함수는 여러 번 호출해도 괜찮게 만들었다.

iex> Process.flag(:trap_exit, true)
false
iex> Task.async(fn -> Process.sleep(1000) end) |> Task.await(100)
** (exit) exited in: Task.await(%Task{owner: #PID<0.104.0>, pid: #PID<0.107.0>, ref: #Reference<0.3587823342.2401501192.59551>}, 100)
    ** (EXIT) time out
    (elixir) lib/task.ex:607: Task.await/2
iex> flush
{#Reference<0.3587823342.2401501192.59551>, :ok}
{:EXIT, #PID<0.107.0>, :normal}
:ok

Task.await/2 함수를 호출한 뒤엔 프로세스가 죽었을 때, 받는 :DOWN 메시지를 못 받는다.

iex> Process.flag(:trap_exit, true)
iex> Task.async(fn -> Process.sleep(1000) end) |> Task.yield(100)
nil
iex> flush
{#Reference<0.3587823342.2401501192.59598>, :ok}
{:EXIT, #PID<0.110.0>, :normal}
{:DOWN, #Reference<0.3587823342.2401501192.59598>, :process, #PID<0.110.0>,
 :normal}
:ok

반면 Task.yield/2 함수를 호출한 뒤에 프로세스가 죽으면 :DOWN 메시지를 받는다.

defmodule Task do
  @spec await(t, timeout) :: term
  def await(%Task{ref: ref, owner: owner} = task, timeout \\ 5000) when is_timeout(timeout) do
    # ...
    receive do
      # ...
    after
      timeout ->
        Process.demonitor(ref, [:flush])
        exit({:timeout, {__MODULE__, :await, [task, timeout]}})
    end
  end
end

Task.await/2 함수에서 timeout이 걸리면 erlang 프로세스 모니터링을 중지하기 때문이다.