2. The Language LOFT

2. The Language LOFT

Browsing

Home:
Concurrent Programming with Fair Threads
The LOFT Language

Previous chapter: Introduction
Next chapter: Examples - 1

Sections

2.1 Overview of the Language
    Modules and Threads
    Instructions
    Schedulers
    Memory Mapping
    Communication
    Events
    Implementation
2.2 Modules and Threads
  2.2.1 Local variables
  2.2.2 Threads
  2.2.3 Finalizer
  2.2.4 Main Module
2.3 Atomic Instructions
  2.3.1 Schedulers
  2.3.2 Events
  2.3.3 Control over Threads
  2.3.4 C Statements
2.4 Non-atomic Instructions
  2.4.1 Cooperate
  2.4.2 Await
  2.4.3 Join
  2.4.4 Loops
  2.4.5 If
  2.4.6 Return
  2.4.7 Halt
  2.4.8 Generated Values
  2.4.9 Run
  2.4.10 Link
2.5 Native Modules

Chapters

1. Introduction
2. The Language LOFT
3. Examples - 1
4. Programming Style
5. Semantics
6. FairThreads in C
7. Implementations
8. Examples - 2
9. Related Work
10. Conclusion
11. Annex A: API of FairThreads
12. Annex B: LOFT Reference Manual
13. Annex C: the LOFT Compiler
  Glossary
  Index

One first gives an overview of the language. Then, the basic notions of modules, threads, schedulers, events, and atomic and non-atomic instructions are presented. Finally, native modules are considered.
2.1 Overview of the Language

Modules and Threads

Modules are the basic units of the language. The syntax is based on C and modules are defined in files that can also contain C code. Threads are created from modules. A thread created from a module is called an instance of it. A module can have parameters which define corresponding parameters of its instances. Arguments provided when an instance is created are associated to these parameters. A module can also have local variables, new fresh instances of which are automatically created for each thread created from it.

Instructions

The body of a module is basically a sequence of instructions executed by its instances. There are two types of instructions: atomic instructions and non-atomic ones. Atomic instructions are run in one single step by the same processor. Usual terminating C code belongs to this kind of instructions. Another example of atomic instruction is the generation of an event, described later, which is broadcast to all the threads. Execution of non-atomic instructions may need several steps up to completion. This is typically the case of the instruction which waits for an event to be generated. Execution steps of non-atomic instructions are interleaved, and can thus interfere during execution. Note that all the execution steps of a non-atomic instruction may not be necessarily executed by the same processor.

Schedulers

The basic task of a scheduler is to control execution of the threads that are linked to it. The scheduling is basically cooperative: linked threads have to return the control to the scheduler to which they are linked, in order to let other threads execute. Leaving the control can be either explicit, with the instruction cooperate, or implicit, by waiting for an event which is not present. All linked threads are cyclically considered in turn by the scheduler until all of them have reached a cooperation point (cooperate, or waiting instructions). Then, and only then, a new cycle can start. Cycles are called instants. Note that the same thread can receive the control from the scheduler several times during the same instant; this is for example the case when the thread waits for a first event which is generated by another thread, later in the same instant. In this case, the thread receives the control a first time and then blocks, waiting for the event. The control goes to the others threads, and returns back to the first thread after the generation of the event. A schedulers define some kind of automatic synchronization which forces the threads linked to it to run at the same pace: all the threads must have finished their execution for the current instant, before the next instant can start. The order in which threads are cyclically considered is always the same: it is the order in which they have been linked to the scheduler. This leads to deterministic executions, which are reproducible. Determinism is of course an important point for debugging. At creation, each thread is linked to one scheduler. There exists an implicit scheduler to which threads are linked by default when no specific scheduler is specified. Several schedulers can be defined and actually running in the same program. Schedulers thus define synchronized areas in which threads execute in cooperation. Schedulers can run autonomously, in a preemptive way under the supervision of the OS. During their execution, threads created from special modules, called native modules, can unlink from the scheduler to which they are currently linked, and become free from any scheduler synchronization. Such free threads are run by kernel threads of the system. Of course, native modules and native threads have a meaning only in the context of a preemptive OS.

Memory Mapping

Three kinds of variables exist in Loft:
  • Global variables that are C global variables shared by all the threads. These variables are placed in the shared memory in which the program executes.
  • Local variables that are instances of local variables of modules. These variables are local to threads. Their specific characteristic is that their value is preserved across instants: they have thus to be stored in the heap.
  • Automatic variables which are defined in atomic instructions, and which are automatically destroyed when the atomic instruction in which they appear is left. The value of an automatic variable is not preserved from one instant to the next. As in C, automatic variables can be stored in registers or in the stack.
Note that non-native threads need a stack only for execution of their atomic instructions. Indeed, atomic instructions cannot be preempted during execution, by definition. Thus, in this case, the stack is always in the same state at the beginning and at the end of the execution of each atomic instruction. Thus, the content of the stack needs not to be saved when a context switch occurs. The situation is of course different for native threads: because they can be preempted arbitrarily by the OS, they need a specific stack, provided by the native thread in charge of running them.

Communication

The simpler way for threads to communicate is of course to use shared variables. For example, a thread can set a boolean variable to indicate that a condition is set, and others threads can test the variable to know the status of the condition. This basic pattern works if all threads accessing the variable are linked to the same scheduler. Indeed, in this case atomicity of the accesses to the variable is guaranteed by the cooperativeness of the scheduler. A general way to protect a data from concurrent accesses is thus to associate it with one unique scheduler to which threads willing to access the data should first link to. However, this solution does not work if some of the threads are non-native and belong to different schedulers or are unlinked. This is actually the standard situation in concurrent programming, where protection is basically obtained with locks. Standard POSIX mutexes can be used for this purpose. Events, described in the next section, give threads an other different means of communication.

Events

Events are synchronizing data basically used by threads to avoid busy-waiting on conditions. An event is created in a scheduler which is in charge of it during all its lifetime. An event is either present or absent during each instant of the scheduler which manages it. It is present if it is generated by the scheduler at the beginning of the instant, or if it is generated by one of the thread executed by the scheduler during the instant; it is absent otherwise. The presence or the absence of an event can change from an instant to another, but it cannot change during the course of an instant: all the threads always "see" the presence or the absence of an event in the same way, independently on the order in which they are scheduled. This is what is meant by saying that events are broadcast. Values can be associated to event generations; they are collected during each instant and are available only during it.

Implementation

The basic implementation of Loft is build on top of a C library of native threads. Each scheduler and each instance of a native module is implemented as a native thread. Threads which are instances of non-native modules do no need the full power of a native thread to execute. Actually, they don't need any specific stack as they can use the one of the native thread of the scheduler to which they are linked. In this way, the number of native threads can be limited to a minimum. This is important when a large number of concurrent activities is needed, as the number of native threads that can be created in systems is usually rather low. Unlinked threads and schedulers can be run in full parallelism, by distinct processors. Programs can thus take benefit of SMP architectures, and be speed-up by multiprocessors. Note that one gets program structures that conform to the so-called M:N architectures: M linked threads and N schedulers running them. An important point is that these architectures become programmable directly at language level, and not through the use of a library (as for example in the Solaris system of Sun).
2.2 Modules and Threads

Modules are templates from which threads are created. A thread created from a module is called an instance of it. A thread is always created in a scheduler which is initially in charge of executing it. Modules are made from atomic and non-atomic instructions that are defined in the next sections. A module can have parameters which define corresponding parameters of its instances. Arguments provided when an instance is created are associated to these parameters. A module can also have local variables, new fresh instances of which are automatically created for each thread created from it. The syntax of Loft is based on C and modules are defined in files that can also contain C code. The syntax of modules is:


module <kind> <name> ( <params> )
<locals>
<body>
<finalizer>
end module
  • <kind> is the optional native keyword. The case of native modules is considered in section Native Modules.
  • <name> is the module name; it can be any C identifier.
  • <params> is the list of parameters; it is empty if there is no parameter at all.
  • <locals> is the optional list of local variables declarations. This list begins with the keyword local.
  • <body> is a sequence of instructions defining the module body.
  • <finalizer> is an optional atomic instruction executed in case of forced termination (instruction stop). This part begins with the keyword finalize.
As example, consider the module trace defined by:

module trace (char *s)
while (1) do
   printf ("%s\n",local(s));
   cooperate;
end
end module
This module is a non-native one and it has a parameter which is a character string. The body is reduced to a single while loop (actually, an infinite one) considered in section Loops. At each instant, the loop prints the message in argument and cooperates. Note the presence of the do and end keywords around the loop body; indeed, as in Loft "{" and "}" are used to delimit atomic instructions (see C Statements), one needs a different notation for loop bodies. The parameter s is actually a local variable which is passed at creation to the instances of trace. In Loft, all accesses to local variables, and thus also to parameters, must be of the form local(...) (local is actually a macro).

2.2.1 Local variables

Threads can have local variables which keep their values across instants. These are to be distinguished from static variable which are global to all the threads, and from automatic variables which loose their values across instants. Let us return to the previous module trace and suppose that one wants to also print the instant number. A first idea would be to define a variable inside the atomic instruction:

module trace (char *s)
while (1) do
   {
      int i = 0;
      printf ("%s (%d)\n",local(s),i++);
   }
   cooperate;
end
end module
This is not correct because the variable i is allocated in the stack at the beginning of the atomic instruction, and vanishes at the end of it. Thus, a new instance of the variable is used at each instant. A solution would be to declare a global C variable to store the instant number:

int i = 0;

module trace (char *s)
while (1) do
   printf ("%s (%d)\n",local(s),i++);
   cooperate;
end
end module
This solution produces a correct output. However, i is a global variable shared by all instances of trace. A local variable should be used if one wishes that each instance owns a distinct variable:

module trace (char *s)
local int i;
{local(i) = 0;}
while (1) do
   printf ("%s (%d)\n",local(s),local(i)++);
   cooperate;
end
end module
Note that local variables must always be accessed using the form local(...). This allows one to access in the same context a local variable and a global or automatic one having the same name.

2.2.2 Threads

Threads are instances of modules. Threads which are instances of a module m are created using the function m_create which returns a new thread. Threads are of the pointer type thread_t. Note that, as in many C APIs, all types defined in Loft have their name terminated by "_t". Arguments given to m_create are passed to the created instance (as in C, arguments are passed by value). For example, the following atomic instruction creates two instances of the previous module trace:

{
   trace_create ("first thread");
   trace_create ("second thread");
}
The output is:

first thread (0)
second thread (0)
first thread (1)
second thread (1)
first thread (2)
second thread (2)
...
Several points are important:
  • Threads are created in the implicit scheduler which is automatically created and started in all program (see Schedulers).
  • Threads are automatically started. This is a difference with Java in which threads have to be explicitly started.
  • Threads are always executed in the same order, which is the order of their creation. Thus, the previous output is the only one possible.
It is possible to create a thread in a specific scheduler using a creation function of the form create_in. For example, an instance of trace is created in the scheduler sched by:

trace_create_in (sched,"a thread");
Threads are always incorporated in the scheduler at the beginning of an instant, in order to avoid interferences with already running threads. Moreover, if the current scheduler and the one in which the thread is created are the same, then this instant is the next one. The running thread is returned by the function self().

2.2.3 Finalizer

The finalizer of a module is an atom executed when an instance of the module is forced to terminate by a stop instruction (defined in Control over Threads). The finalizer is only executed if the stopped thread is not already terminated. A typical use of finalizer is for deallocation purposes, as in:

module m (...)
local resource_type resource;

{local(resource) = allocate ();}
...
{deallocate (local(resource));}

finalization
{deallocate (local(resource));}
end module
The allocated resource is deallocated in case of normal termination, at the end of the module body. It is also deallocated if termination is forced, as in this case the finalizer is executed. An important point to note is that the thread executing a finalizer is unspecified, as the instance considered is stopped. This means that self cannot be safely used in finalizers.

2.2.4 Main Module

The entry point of a program is a module with name main. An instance of it is automatically created in the implicit scheduler. For example, the previous output could be produced by the execution of a program with the following module main:

module main ()
trace_create ("first thread");
trace_create ("second thread");
end module
As in C, arguments can be given to the module main, following the standard argc/argv convention. The global program does not terminate when the main thread terminates: it is the programmer's responsibility to end the program (typically, by calling the exit function of C).
2.3 Atomic Instructions

In this section, one considers the atomic instructions which terminate at the same instant they start. We have already seen two examples in previous sections: call to the C function printf, and creation of instances of a module m with the m_create and m_create_in functions.

2.3.1 Schedulers

Creation

Schedulers can be created arbitrarily with the scheduler_create atomic instruction. They are of the type scheduler_t.

scheduler_t sched = scheduler_create ();
One instant of a scheduler is got by executing the acheduler_react atomic instruction.

scheduler_react (sched);
The scheduler_react function allows user to build hierarchies of schedulers and to make them react following arbitrary strategies.

Implicit and Current Schedulers

A scheduler, called the implicit scheduler, always exists in every program. Two atomic instructions are available to get the implicit scheduler and also the current scheduler, which is the one to which the executing thread is linked (the NULL value is returned if the thread is not linked to any scheduler).

scheduler_t implicit = implicit_scheduler ();
scheduler_t current  = current_scheduler ();

2.3.2 Events

Events are basically variables with 3 possible values: present, absent, or unknown. An event is created in a scheduler and managed by it during all its lifetime. The value of an event is automatically set to unknown at the beginning of each instant. There is a way for the programmer to set an event to present (namely, to generate it), but there is no way to set it to absent: only the system is able to decide that an event is absent. Actually, the system automatically decides that unknown events are absent when all threads have reached a cooperation point and when there is no thread waiting for an event which has been previously generated. The language definition assures that, once unknown events are declared absent, all is finished for the current instant (in particular, no event can be generated anymore during the same instant). As consequence, one can consider that:
  • Events are non-persistent data which lose their presence status at the beginning of each instant.
  • Events are broadcast: all threads always "see" their presence/absence status in the same way, independently on the order in which they are scheduled.
  • Reaction to the absence of an event is always postponed to the next instant.
The last point is a major difference with the synchronous approach, in particular with the Esterel [9] language (see Related Work for details).

Creation

A event is created in a scheduler whose instants determine its presence/absence status. event_create creates an event in the implicit scheduler. event_create_in creates an event in the scheduler given as argument. Events are of the type event_t.

event_t evt1 = event_create ();
event_t evt2 = event_create_in (sched);
The first event is created in the implicit scheduler while the second one is created in the scheduler sched.

Generation

Generation of an event makes it present in the scheduler in charge of it (the one in which the event has been created). If the thread running the generation is linked to the scheduler of the event, then the generation becomes immediately effective. In this case, all the threads waiting for the event will be re-scheduled during the same instant, and thus will see the event as present. If the scheduler of the event and the one of the thread are different, then the order to generate the event is sent to the scheduler of the event. In this case, the event will be generated later, at the beginning of the next instant of the target scheduler. Note that this can occur at an aritrary moment if the target scheduler is run asynchronously by a dedicated native thread. A value can be associated to an event generation; in this case one speaks on a valued generation of the event; in the other case, the generation is said to be pure. The value must be of a pointer type, or of a type that can be cast to such a type. All the values generated for a same event are collected during the instant and are available during this instant using the get_value non-atomic instruction (see section Generated Values). A pure generation of a present (that is, already generated) event has no effect. This is of course different for a valued generation: then, the value is appended to the list of values of the event. Note that, as for the presence/absence status, the list of values of an event is automatically reset to the empty list at the beginning of each instant.

generate (evt);
generate_value (evt,val);
The first call is the pure generation of the event evt. The second call generates the same event, but with the value val. Note that a valued generation with a null value is not equivalent to a generation without any value.

2.3.3 Control over Threads

Three means for controlling threads execution are provided: the way to stop the execution of a thread which is thus forced to terminate definitively; the way to suspend and resume the execution of a thread. With these means, the user can design its own scheduling strategies for controlling threads.

Stop

Execution of a thread is stopped with a call to the stop instruction. To stop a thread means that its execution is definitively abandoned. The effect of the stop instruction is to send to the scheduler of the thread in argument the order to stop the thread. The order will always be processed at the beginning of a new instant, in order to avoid interferences with the other threads. If the executing thread and the stopped threads are both linked to the same scheduler, then the thread will be stopped at the beginning of the next instant. As example, a thread can stop itself by:

stop (self ());
Note that the thread terminates its execution for the current instant and is only effectively stopped at the next instant; the preemption is actually a "weak" one.

Suspend and Resume

Threads can be suspended and resumed using the two atomic instructions suspend and resume. Suspension of a thread means that its execution is suspended until it is resumed. Suspension and resuming are orders given to the scheduler running the considered thread, and are always processed at the beginning of new instants. Like for stop, if the executing thread and the concerned threads are both linked to the same scheduler, then the thread execution status will be changed at the beginning of the next instant.

suspend (main_thread);
resume (other_thread);
Orders are processed in turn. For example, the following sequence of instruction has no effect as thread is immediately resumed after being suspended:

suspend (thread);
resume (thread);

2.3.4 C Statements

Basic atomic instructions are standard C blocks, made of C statements enclosed by "{" and "}". Of course, the C statement must terminate and one must keep in mind that it should not take too much time to execute it. Indeed, the others threads belonging to the same scheduler cannot execute while the current thread has not finish to execute it (see the discussion on this point in Necessity of Cooperation). Almost any C code can syntactically appear in atomic instructions, between "{" and "}". It is of the programmer's responsibility to avoid the use of any statement that would prevent the complete execution of an atomic instruction. For example, setjump/longjump should absolutely be avoided. However, some forbidden statements are detected, and lead to program rejection. This is the case of the C return statement. break statements not enclosed in a C bloc are also forbidden, as well as goto statements. Procedure calls are considered as atomic instructions and can thus be used directly without being put inside a C block. This is however just a syntactic facility and the semantics of an atomic action made of a C procedure call is exactly the one of the block containing it. For example, the module main of Main Module could be equivalently written:

module main ()
{
   trace_create ("first thread");
   trace_create ("second thread");
}
end module
2.4 Non-atomic Instructions

In this section, one considers the non-atomic instructions. As opposite to atomic instructions, execution of non-atomic instructions can last several instants.

2.4.1 Cooperate

The basic non-atomic instruction is the cooperate instruction which returns the control back to the scheduler. When receiving the control after a cooperate instruction, the scheduler knows that the executing thread has finished its execution for the current instant, and thus that is is not necessary to give it back the control another time during the instant. When the thread will receive the control in a future instant, the cooperate instruction terminates and passes the control in sequence. Execution of a cooperate instruction thus needs two instants to complete.

2.4.2 Await

Execution of an await instruction suspends the executing thread until the event is generated. There is of course no waiting at all if the event is already present. Otherwise, the waiting can just take a portion of the current instant, if the awaited event is generated later in the same instant, by a thread scheduled later; the waiting can also last several instants, or can even be infinite, if the awaited event is never generated.

await (event);
There is a way to limit the time during which the thread is suspended waiting for an event. The limitation is expressed as a number of instants. The thread is resumed when the limit is reached. Of course, the waiting ends, as previously, as soon as the event is generated before reaching the limit. For example, the following instruction suspends the executing thread at most 10 instant, waiting for the event e.

await (e,10);
After resuming, one can know if the limit was exceeded or if the event has been generated before the limit was reached, by examining the value returned by the predefined function return_code ():
  • ETIMEOUT means that the limit has been reached.
  • OK means normal termination because the awaited event is generated.
For example, the following code tests the presence of an event during one instant:

await (e,1);
{
   if (return_code () == ETIMEOUT) 
      printf ("was absent");
   else 
      printf ("is present"); 
}
Note that the message "was absent" is printed at the instant that follows the starting of the waiting, while "is present" is printed at the same instant. Reaction to the absence of an event (here, the printing action) cannot be immediate, but is always postponed to the next instant. The value returned by return_code () concerns the last non-atomic instruction executed. For example, in the following code, the printed messages concern f, not e:

await (e,1);
await (f,1);
{
   if (return_code () == ETIMEOUT) 
      printf ("f was absent");
   else 
      printf ("f is present");
}

2.4.3 Join

The join instruction suspends the executing thread until another one, given as argument, has terminated; this is called joining the thread. Note that there are actually two ways for a thread to terminate: either because execution has returned, or because the thread has been stopped. As for await, there is a possibility to limit the time during which the thread is suspended, and the return_code function is used in the same way to detect if the limit was reached.

join (th1);
join (th2,10);
The first instruction waits unconditionally for the termination of th1, while the second waits at most 10 instants to join th2.

2.4.4 Loops

There are two kinds of loops: while loops, that cycle while a boolean condition is true, and repeat loops that cycle a fixed number of times. There is no equivalent of a for loop, as such loops would need in most cases an associated local variable; for loops are thus, when needed, to be explicitly coded.

While

While loops are executing their bodies while a boolean condition is true.

while (1) do
   printf ("loop!");
   cooperate;
end
The value of the condition is only considered at the first instant and when the body terminates. For example, the following loop never terminates, despite the fact that the condition is false at some instants:

{local(i) = 1;}
while (local(i)) do
   {local(i) = 0;}
   cooperate;      
   printf ("loop!");
   cooperate;
   {local(i) = 1;}   
end
Instantaneous loops are loops with a non-cooperating body. This is for example the case of:

while (1) do
   printf ("loop!");
end
Instantaneous loops should generally be avoided as they forbid execution of the others threads (see Necessity of Cooperation). Nevertheless, they can be useful in unlinked threads, or when the executing thread is the only one run by the scheduler (which is a rather special situation, indeed).

Repeat

A repeat loop executes its body a fixed number of times. It could be coded with a while loop and a counter implemented as a local variable; the repeat instruction avoids the use of such a local variable.

repeat (10) do
   printf ("loop!");
   cooperate
end
The expression defining the number of cycles is evaluated when the control reaches the loop for the first time.

2.4.5 If

The if statement can be used in atomic instructions as other standard C statements. However, when it is used in this context, both then and else branches must be atomic instructions. A non-atomic version of the if is available. The syntax is:

if (<expression>) then <instruction> else <instruction> end
For example, the instruction of Await, which tests for the presence of e during the current instant, can be equivalently written as:

await (e,1);
if (return_code () == ETIMEOUT) then 
   printf ("was absent");
else 
   printf ("is present"); 
end
Both then and else branches are optional. The boolean expression exp is evaluated once, when the control reaches the if for the first time. Its value determines which branch is chosen for execution. The chosen branch is then executed up to completion, which can takes several instants, as it can be non-atomic. For example, the following instructions prints at the next instant the value exp has at the current instant:

if (exp) then
   cooperate;
   printf ("true!");
else
   cooperate;   
   printf ("false!");
end

2.4.6 Return

The return statement of C cannot be used in atomic instruction, as it would prevent the instruction to terminate. However, it can be used as a non-atomic instruction to force the termination of the executing thread. For example, the following module terminates when the boolean expression exp becomes true:

module m ()
while (1) do
   if (exp) then return; end
   cooperate;
end
end module
Note that, as the return statement is forbidden in atomic instructions, the following module is incorrect:

module bug ()
while (1) do
   {if (exp) return;} 
   cooperate;
end
end module

2.4.7 Halt

The halt instruction never terminates. It is equivalent to:
while (1) do cooperate; end
Its basic use is to block the execution flow, as in:

if (!exp) then
   generate (error);
   halt;
end
...
In this code, the if instruction tests exp and forbids the control to pass in sequence if it is false. In this case, an event is generated which should be used by an other thread to recover from this situation.

2.4.8 Generated Values

The get_value instruction is the means to get the values associated to an event by the valued generations of it. The values are indexed and, when available, returned in a variable of type void**. The instruction get_value (e,n,r) is an attempt to get the value of index n, generated for the event e during the current instant. If available, the value is assigned to r during the current instant; otherwise, r will be set to NULL at the next instant, and the function return_code () will return the value ENEXT. For example, the following instruction is an attempt to get the first value of evt (for simplicity, values are supposed to be of type int):

get_value (evt,0,res);
{
   if (return_code () == ENEXT) printf ("there was no value! ");
   else printf ("first value is %d ", (int)(*res));
}
The following module awaits an event and prints all the values generated for it:

module print_all_values (event_t evt)
local int i,int res;
await (local(evt));
{local(i) = 0;}
while (1) do
   get_value (local(evt), local(i), (void**)&local(res));
   if (return_code () == ENEXT) then return;
   else
      {
         printf ("value #%d: %d\n", local(i), local(res));
         local(i)++;
      }
   end
end
end module
Note that the module always terminates at the next instant. This should not be surprising: one must wait for the end of the current instant to be sure that all the values have been effectively got.

2.4.9 Run

A module can run another one, using the run non-atomic instruction. The calling thread suspends execution when encountering a run instruction. Then, an instance of the called module is created with the arguments provided. Finally, the calling thread is resumed when the instance of the called module terminates. Moreover, a thread which is stopped retransmits the stop order to the thread it is running, if there is one. Thus, the instance of a called module is automatically stopped when the calling thread is.

run mod1 ();
run mod2 ("mod2 called");
In this sequence, an instance of mod1 is first executed. When it terminates (if it does), then an instance of mod2 is executed with a string argument. Finally, the sequence terminates when the last instance does.

2.4.10 Link

When created, a thread is always linked to a scheduler. During execution, the thread can link to others schedulers, using the link instruction. The effect of execution the instruction link (sched) is to extract the executing thread from the current scheduler and to add it to sched, in the state in which it has left the current scheduler. Thus, after re-linking, the thread will resume execution in the new scheduler. This can be seen as a restricted migration of threads. For example, the following module cycles between two schedulers. On each scheduler, it awaits an event and then prints a message. Then, if the two events are generated at each instant, the program prints "Ping Pong" forever:

module play ()
while (1) do
   link (sched1);
   await (evt1);
   printf ("Ping ");
   link (sched2);
   await (evt2);
   printf ("Pong\n");
end
end module
2.5 Native Modules

Native modules only have meaning in the context of a preemptive OS as instances of native modules should be run by native threads belonging to the kernel. The presence of the native keyword just after the keyword module defines the module as native. The unlink non-atomic instruction unlinks the instance of a native module from the scheduler to which it was previously linked. After the unlink instruction, the thread is free from any synchronization. However, an unlinked thread can always re-link to a scheduler using the link non-atomic instruction (see Link). The unlink instruction is specific to native modules: programs where unlink instructions appears in non-native modules are rejected at compile time. When unlinked, instances of native modules are autonomous, which means that they execute independantly of any scheduler, at their own pace. For example, consider the following program which creates two native threads instances of the same native module pr:


module native pr (char *s)
unlink;
while (1) do
   printf (local(s));
end
end module

module main ()
pr_create ("hello ");
pr_create ("world!\n");
end module
The two lists of messages produced are merged in an unpredictable way in the output: nondeterminism is introduced by unlinked instances of native modules. Note that the loop in pr is instantaneous; this is not a problem as the thread is unlinked. The granularity of each thread is, however, under the dependance of the OS, which can be unacceptable in some situations (see Granularity of Instants). Native module are important for using standard blocking I/Os in a cooperative context. For example, the following module uses the getchar function which blocks execution of the calling thread until a character is read on the standard input:

module native AnalyseInput ()
unlink;
...
while (1) do
   {
      switch (getchar ()){
         ...
      }
   }
end
end module
The first instruction unlinks the thread from the current scheduler. Then, the getchar function can be safely called without any risk to block other threads. The following module read_module implements a cooperative read I/O, using the standard blocking read function. The thread first unlinks from the scheduler, then performs the read, and finally re-links to the scheduler:

module native read_module (int fd,void *buf,size_t count,ssize_t *res)
local scheduler_t sched;
{local(sched) = current_scheduler ();}
unlink;
{(*local(res)) = read (local(fd),local(buf),local(count));}
link (local(sched));
end module
Note that the pattern suggested by this example is a very general one, useful to reuse code which was not designed to be run in a cooperative context.

This page has been generated by Scribe.
Last update Wed Oct 22 18:41:04 2003