Search code examples
concurrencyelixirterminate

Program Seems to Terminate Early


I get the feeling this is one of those really simple problems where there's something I just don't understand about the language. But I'm trying to learn Elixir, and my program isn't running all the way through. I've got a minimal example here.

defmodule Foo do
  def run(0) do
    IO.puts("0")
  end
  def run(n) do
    IO.puts(to_string n)
    run(n - 1)
  end
  def go do
    run(100)
  end
end

# Foo.go
# spawn &Foo.go/0

Now, if I uncomment the Foo.go line at the bottom and run it with elixir minimal.exs, then I get the intended output, which is all of the numbers from 100 down to 0. If I uncomment only the spawn &Foo.go/0 line, I consistently get no output at all.

However, if I uncomment both lines and run the program, I get the numbers from 100 to 0 (from the first line), then the first few numbers (usually about 100 to 96 or so) before the program terminates for some reason. So I really don't know what's causing the process to terminate at a random point.

It's worth pointing out that the reason this confusion arose for me was that I was trying to use mix to compile a larger project, when the program seemed to get started, do a small part of its work, and then terminate because mix apparently stops running after a bit. So I'm not sure what the idiomatic way to run an Elixir program is either, given that mix seems to terminate it after a short while anyway.


Solution

  • spawn/1 will create a new process to run the function. While they are not the same, you can sort of think of an Erlang / Elixir process as a thread in most other languages.

    So, when you start your program, the "main" process gets to doing some work. In your case, it creates a new process (lets call it "Process A") to output the numbers from 100 down to 0. However, the problem is that spawn/1 does not block. Meaning that the "main" process will keep executing and not wait for "Process A" to return.

    So what is happening is that your "main" process is completing execution which ends the entire program. This is normal for every language I have ever used.

    If you wanted to spawn some work in a different process and make sure it finishes execution BEFORE ending your program, you have a couple different options.

    You could use the Task module. Something along the lines of this should work.

    task = Task.async(&Foo.go/0)
    Task.await(task)
    

    You could explicitly send and receive messages

    defmodule Foo do
      def run(0, pid) do
        IO.puts("0")
    
        # This will send the message back to the "main" thread upon completion.
        send pid, {:done, self()}
      end
      def run(n, pid) do
        IO.puts(to_string n)
        run(n - 1, pid)
      end
    
      # We now pass along the pid of the "main" thread into the go function.
      def go(pid) do
        run(100, pid)
      end
    end
    
    # Use spawn/3 instead so we can pass in the "main" process pid.
    pid = spawn(Foo, :go, [self()])
    
    # This will block until it receives a message matching this pattern.
    receive do
      # The ^ is the pin operator. It ensures that we match against the same pid as before.
      {:done, ^pid} -> :done
    end
    

    There are other ways of achieving this. Unfortunately, without knowing more about the problem you are trying to solve, I can only make basic suggestions.

    With all of that said, mix will not arbitrarily stop your running program. For whatever reason, the "main" process must have finished execution. Also mix is a build tool, not really the way you should be running your application (though, you can). Again, without knowing what you are attempting to do or seeing your code, I cannot give you anything more than this.