Process J - A Concurrent Language

Graceful Termination


← Previous (Building Processes as a Network of Processes) Graceful Termination Next (???) →

The full integrator with producer and consumer that we saw in the previous section clearly runs forever. How do we gracefully terminate the entire network? We know how to select input using an alt statement, so we could introduce a "kill" channel, on which we can send a single boolean value (e.g., true) when we want the process to terminate. We could start by rewriting the integrate procedure to include such a channel:

Integrate with kill channel.
public proc void integrate(chan<int>.read in,
                           chan<int>.write out,
                           chan<boolean>.read kill) {
  int total = 0;
  boolean ok = true;
  while (ok) {
    int x;
    boolean b;
    pri alt {
      x = in.read() : {
        total = total + x;
        out.write(total);
      }
      b = kill.read() : {
        ok = false;
      }
    } // end pri alt
  } // end while
}

It is worth noting that we have changed the while (true) to depend on a boolean variable ok, and the alt is a pri alt. Naturally, we have done this to ensure that input is handled and passed on before the process terminates.

To illustrate the use of the kill channel, let us write a small process that generates the kill signal:

Killer process.
public proc void killer(chan<boolean>.write kill) {
  // wait some time
  kill.write(true);
}

Naturally, // wait some time needs to be sorted out. Here we can use a timer. ProcessJ has a built-in datatype called timer. A timer is a clock that can be read using a read() call (like a channel), and it is always ready to be read and it will return a positive long value. A timer can also be used to perform timeouts. This is simply done by calling timeout() on it; for example t.timeout(1000) will timeout in 1,000 milliseconds, wchich is 1 second. Therefore, we can replace // wait some time with timer t; t.timeout(1000); to delay sending the kill signal for a second. If we then run the killer process concurrently with the other processes we get:

???.
public proc void main(string args[]) {
  chan<int> in, out;
  chan<boolean> kill;
  par {
    produce(in.write);
    integrate(in.read, out.write, kill.read);
    killer(kill.write);
    consumer(out.read);
  }
}

If we run this program, what happens? The runtime system gives us an error. It reports: No processes ready to run. System is deadlocked. Why did that happen? If we look carefully at the code we wrote, we notice that the produce and the consumer processes never terminates.

So, the produce is stuck in its write call and the consumer is stuck in its read call, but since the process that held the other ends of the respective channels is not longer running, these two processes will never terminate, and therefore there are no processes to run in the system. The main process cannot progress either as it is waiting for the par block to finish, which it never will as two of its component processes are forever blocked in read and write calls.

Naturally, we need to kill the produce and the consume processes off as well. However, we have to be a little careful here. An obvious first attempt may be to add simply add kill channels and pri alts to these two processes in the following way:

Incorrect kill implementation.
public proc void consume(chan<int>.read in, chan<boolean>.read kill) {
  boolean ok = true;
  while (ok) {
    int x;
    boolean b;
    pri alt {
      x = in.read() : {
        println(x);
      }
      b = kill.read() : {
        ok = false;
      }
    }
  }
}

public proc void produce(chan<int>.write out, chan<boolean>.read kill) {
  int x = 0;
  boolean ok = true;
  while (ok) {
    boolean b;
    pri alt {
      b = kill.read() : {
        ok = false;
      }
      skip : {
        out.write(x);
        x++;
      }
  }
}

public proc void killer(chan<boolean>.write killProduce,
                        chan<boolean>.write killIntegrate,
                        chan<boolean>.write killConsume) {
  timer t;
  t.timeout(1000);
  par {
    killProduce.write(true);
    killIntegrate.write(true);
    killConsume.write(true);
  }
}

public proc void main(string args[]) {
  chan<int> in, out;
  chan<boolean> killProduce, killIntegrate, killConsume;
  par {
    produce(in.write, killProduce.read);
    integrate(in.read, out.write, killIntegrate.read);
    killer(killProduce.write, killIntegrate.write, killConsume.write);
    consume(out.read, killConsume.read);
  }
}

Chances are that if you run this it runs just fine. numbers will be coming out and after a second of runtime (perhaps a little more because the output is lagging a little) everything will terminate just fine. However, everything is not well! Consider the following scenario (which describes a particular schduling of the processes):

  • The timeout in killer has happened and kill signals are sent on all three kill channels in parallel.
  • The consume process recieves the kill message (no other message is available) and terminates.
  • integrate now commits to writing a value on its out channel, we assume that it read a value from the produce process, but the consume process has terminated and the integrate process is stuck.

The above scenario shows that our first implementation may possibly deadlock. Graceful termination is not a simple task. However, there is a different way: let the killer process send a kill signal to integrate, and let integrate send the kill signal to consume in the following manner:

Correct kill implementation.
import std.io;

public proc void integrate(chan<int>.read in,
                           chan<int>.write out,
                           chan<boolean>.read killMe,
                           chan<boolean>.write killConsumer) {
  int total = 0;
  boolean ok = true;
  while (ok) {
    boolean y;
    int x;
    pri alt {
      y = killMe.read() :
        {
          killConsumer.write(true);
          ok = false;
        }
      x = in.read() :
        {
          total = total + x;
          out.write(total);
        }
     }
  }
}

public proc void produce(chan<int>.write out,
                         chan<boolean>.read killMe,
                         chan<boolean>.write killIntegrate) {
  int i = 0;
  boolean ok = true;
  while (ok) {
  boolean b;
    alt {
      b = killMe.read(): {
        ok = false;
        killIntegrate.write(true);
      }
      skip: {
        out.write(i);
        i=i+1;
      }
    }
  }
}

public proc void consume(chan<int>.read in,
                         chan<boolean>.read killMe) {
  boolean ok = true;
  while (ok) {
    int x;
    boolean b;
    pri alt {
      x = in.read() : {
        println(x);
      }
      b = killMe.read(): {
        ok = false;
      }
    }
  }
}

public proc void killer(chan<boolean>.write killProduce) {
  timer t;
  t.timeout(3);
  killProduce.write(true);
}

public proc void main(string args[]) {
  chan<int> in, out;
  chan<boolean> killProduce, killIntegrate, killConsume;
  par {
    produce(in.write, killProduce.read, killIntegrate.write);
    integrate(in.read, out.write, killIntegrate.read, killConsume.write);
    killer(killProduce.write);
    consume(out.read, killConsume.read);
  }
}

The above code will not deadlock and it will terminate gracefully.


← Previous (Building Processes as a Network of Processes) Graceful Termination Next (???) →