Unicon IPC classes

This page documents an IPC facility for Unicon, based on three Unix IPC facilities, namely shared memory, semaphores and message queues. This facility enables communication between different processes, either within the same or a different programs.

The shared library

In order to use these functions, a shared library must be compiled and added to the LD_LIBRARY_PATH, or whatever path is used to load shared libraries for the particular operating system variety.  The library is in the "native" subdirectory of the source code.  The makefile needs the environment variable UNICON_HOME set to the top level of your Unicon installation, as it needs to include some header files from the Unicon source code.  So on my system, I would use the following commands :-

$ export UNICON_HOME=/opt/unicon
$ make
gcc -fPIC -O2 -I/usr/include -I/opt/unicon/src/h -I/opt/unicon/src/gdbm -I/opt/unicon/src/libtp -c ipc.c
gcc -shared -o uniipclib.so -fPIC ipc.o
$ export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:`pwd`

One other thing may need to be changed depending on your operating system.   On Linux I found that the iconx executable needed to be linked with the -E flag, to enable the shared library to access symbols in the runtime executable.  To do this edit the file src/runtime/Makefile so that the target iconx looks something like this :-

iconx: $(OBJS)
$(CC) $(LDFLAGS) -Xlinker -E -o iconx $(OBJS) $(XL) $(RLIBS)
cp iconx ../../bin
strip ../../bin/iconx

It is the "-Xlinker -E" bit that passes the "-E" flag to the linker.

Then just cd to src/runtime, rm iconx and run make.

Creating a sub-process

Creating a sub-process is easy, and can be done with the fork() function, or alternatively using the Process class, which is just a wrapper around fork() and some other common commands.  Here is a simple use of the Process class.

import lang

class Child : Process()
method run()
write("In child")
end
end

procedure main()
local c

write("In parent")

c := Child()
c.start()
c.join()

write("Finished")
end

This program when compiled prints


 
In parent
In child
Finished

The child process is defined by subclassing Process and implementing the run() method.  In the parent process in the main method, invoking the start() method of the child begins the process, whilst the join() method waits for the child to finish.

This is very straightforward, but for things become more complicated when the child and parent process wish to communicate with one another.  Consider the following expanded version of the above program.

import lang

global var

class Child : Process()
method run()
write("In child, setting var")
var := 2
end
end

procedure main()
local c

var := 1

write("In parent var=", var)

c := Child()
c.start()
c.join()

write("Finished, var=", var)
end

The output of this program is :-

In parent var=1
In child, setting var
Finished, var=1

This is not what you would expect at first glance;  the child's attempt to set the variable has had no effect on the parent.  This is because at the point the child process is created, it is given a complete copy of the memory space of the parent process, and so all assignments affect the copy, not the original.

Shared Memory

To solve this problem, shared memory can be used.  Here is the same program, but this time with the variable var replaced by an instance of the Shm shared memory class.

import ipc, lang

global var

class Child : Process()
method run()
write("In child, setting var")
var.set_value(2)
end
end

procedure main()
# Create a Shm instance with initial value 1
var := create_private_shm(1)

write("In parent var=", var.get_value())

c := Child()
c.start()
c.join()

write("Finished, var=", var.get_value())

var.remove()

end

Now the output is :-

In parent var=1
In child, setting var
Finished, var=2

Here, var is an instance of the Shm class, and is created with the "factory" procedure create_private_shm.  The parameter 1 is just the initial value.  Then get_value() and set_value() are used to get/set the value of the object, and finally remove() cleans up the operating system resources used by the instance (if this is not done, it will be done automatically when the program ends, and a warning message output).  These various calls delegate their work to the C functions in the shared library.

More or less anything can be used as the parameter to set_value(), but note that the value is copied into and out of the shared variable, ie reference semantics do  not apply.  So, for example :-

   var := create_private_shm([1,2,3])

put(var.get_value(), 4) # No effect; just adds 4 to the copy returned by get_value
every write(!var.get_value()) # 1,2,3

var.set_value(put(var.get_value(), 4)) # OK, sets the value again with the 4 added
every write(!var.get_value()) # 1,2,3,4

There is one restriction regarding setting class instances as the value of the shared variable, and that is that they must subclass ClassCoding.  This is best achieved by actually subclassing one of ClassCoding subclasses, BasicClassCoding or SelectiveClassCoding.  Please see the documentation of those classes for details, but here is an example.

import ipc, lang

class MyClass : BasicClassCoding(x)
method thing()
write("In thing, x=", x)
end
end

procedure main()
local var, c, d
var := create_private_shm()

c := MyClass(10)
var.set_value(c)

d := var.get_value()
# Note that now d ~=== c
d.thing()

var.remove()
end

Which outputs


In thing, x=10

If MyClass does not extend BasicEncode as shown, then an unfriendly runtime error will result!

Semaphores

The Sem class provides the classic semaphore mechanism, based on the underlying Unix implementation, which can be used for inter-process co-ordination, critical regions, and so on.

Here is an example in which a child process creates some information and puts into a shared memory variable, whilst the parent retrieves that information.  The processes are co-ordinated by two semaphores to make sure they remain in step.

import ipc, lang

global var, sem1, sem2

class Child : Process()
method run()
every i := 1 to 10 do {
# Wait till okay to set value
sem1.wait()
var.set_value(i)
# Signal value set
sem2.signal()

# Sleep for 1/2 second
sleep(500)
}
end
end

procedure main()
var := create_private_shm()

# Create two private semaphores with initial value 0.
sem1 := create_private_sem(0)
sem2 := create_private_sem(0)

c := Child()
c.start()

repeat {
# Signal okay to set value
sem1.signal()
# Wait till value set
sem2.wait()
i := var.get_value()
write(i)
if i = 10 then
break
}
c.join()

write("Finished")

var.remove()
sem1.remove()
sem2.remove()
end

Note that the semaphores are created and destroyed in a similar way to shared memory.

Other than the standard wait and signal methods, the Sem class also has methods to get and set the semaphore value, and to poll it for a value without suspending.

Message queues

The third IPC facility is message queues.  One process adds messages to the queue and another one reads the messages.  A message can be any Icon value, subject to the same rules as for shared memory, described above.  Here is the same example as just shown above, but using a message queue instead.

import ipc, lang

global mq

class Child : Process()
method run()
every i := 1 to 10 do {
# Send the message
mq.send(i)

# Sleep for 1/2 second
sleep(500)
}
end
end

procedure main()
mq := create_private_msg()

c := Child()
c.start()

repeat {
i := mq.receive()
write(i)
if i = 10 then
break
}
c.join()

write("Finished")

mq.remove()
end

As you can see, this particular task is much simpler using message queues.

Public IPC resources

All of the examples above used "private" resources; for example the message queue was created with

 mq := create_private_msg()

But it is possible to provide a pre-arranged key in order to create a "public" resource instead.  For example

 mq := create_public_msg(999) | stop("Couldn't create resource (already exists)")

After this succeeds a message queue will exist with key 999.  If the call fails, then this means a queue with id 999 already exists.

This message queue may then be opened by another process, possibly in another program.

 mq := open_public_msg(999) | stop("Couldn't open resource (doesn't exists)")

Similar factory procedures exist for semaphores and shared memory.

There is one difference between "private" and "public" resources: the "public" variety aren't destroyed automatically at the end of the creating process; they have to be destroyed explicitly either by calling the remove() method, or ultimately using the Unix commands ipcs and ipcrm.