Sync / Singles

View syncsingle.chpl on GitHub

This primer illustrates Chapel’s sync and single variables.

Sync and single are type qualifiers that can be applied to all Chapel primitive types except strings and complex.

Sync and single variables have an associated state that is either full or empty. Single variables are sync variables that can only be written once. If a sync or single variable is declared with an initializing expression, its state is set to full and its value is that of the expression. Without an initializing expression, a sync or single variable’s state is set to empty and its value is the default value for the base type.

config const n = 7;

var sy$: sync int=1;  // state = full, value = 1
var si$: single bool; // state = empty, value = false

As a convention, sync and single variable names sometimes end in a $ to add a visual cue indicating that reads and writes are possibly expensive operations that can potentially block the task.

Because of their full/empty state, reads and writes to sync and single variables must be done with methods to make it clear how the full/empty state is treated in the operation. If a sync or single variable is not in the correct state for a given operation, the current task blocks until it is.

Here are the methods defined on sync / single variables:

The reset() method, defined for sync variables, sets the value of the variable to the default value for the type and the state to empty.

sy$.reset();  // value = 0, state = empty

The isFull method returns true if the sync or single variable is in the full state, false otherwise.

writeln("sy$.isFull ", sy$.isFull);
writeln("si$.isFull ", si$.isFull);

The writeEF() method, defined for sync and single variables, blocks until the state is empty and then assigns the value argument to the variable and then sets the state to full. Assignment of sync and single variables are performed using writeEF().

sy$.writeEF(2*n);
si$.writeEF(true);

The readFE() method, defined for sync variables, blocks until the state is full and then reads the value of the variable, sets the state to empty, and then returns the value. Normal reads of sync variables are performed using readFE().

var sy2 = sy$.readFE();
writeln("sy2 ", sy2);

The readFF() method, defined for sync and single variables, blocks until the state is full and then reads the value of the variable and returns the value. The state remains full. Normal reads of single variables are performed using readFF().

var si2 = si$.readFF();
writeln("si2 ", si2);

The writeXF() method, defined for sync variables, assigns the value argument to the variable and then sets the state to full. This method does not block.

sy$.writeXF(3*n);

The readXX() method, defined for sync and single variables, returns the value of the variable regardless of the state. This method does not block and the state is unchanged.

var sy3 = sy$.readXX();
var si3 = si$.readXX();
writeln("sy3 ", sy3);
writeln("si3 ", si3);

The writeFF() method, defined for sync variables, blocks until the state is full and then and then assigns the value argument to the variable. The state is unchanged.

sy$.writeFF(4*n);

The following creates a new task via a begin statement and declares a variable sy that is initialized using sy$. The initialization statement will block until sy$ is full. The last statement in the begin block sets done$ to full.

var done$: sync bool;
writeln("Launching new task");
begin {
  var sy = sy$.readFE(); // This statement will block until sy$ is full
  writeln("New task unblocked, sy=", sy);
  done$.writeEF(true);
}

Recall that execution proceeds immediately following the begin statement after task creation.

writeln("After launching new task");

When sy$ is written, its state will be set to full and the blocked task above will continue.

sy$.writeEF(n);

This next statement blocks until the last statement in the above begin completes.

done$.readFE();

Example: simple split-phase barrier for tasks

var count$: sync int = n;  // counter which also serves as a lock
var release$: single bool; // barrier release

coforall t in 1..n {
  var myc = count$.readFE(); // read the count, grab the lock (state = empty)
  if myc!=1 {                // still waiting for others
    write(".");
    count$.writeEF(myc-1); // update the count, release the lock (state = full)
                             // we could do some work while waiting
    release$.readFF();       // wait for everyone
  } else {                   // last one here
    release$.writeEF(true);  // release everyone first (state = full)
    writeln("done");
  }
}

sy$.reset();

Sync and single arguments are passed by reference. As a result, the state of the variable does not change.

writeln("sy$.isFull ", sy$.isFull, " - now passing to f_withSyncIntFormal");
f_withSyncIntFormal(sy$);
writeln("si$.isFull ", si$.isFull, " - now passing to f_withSingleBoolFormal");
f_withSingleBoolFormal(si$);

sy$.writeEF(4*n);

When passing a sync or single variable to a generic formal, whether with a ref intent or a default intent, the variable is passed by reference. The state of the variable does not change and sync operations are available.

f_withGenericDefaultIntentFormal(sy$);
f_withGenericDefaultIntentFormal(si$);
f_withGenericRefFormal(sy$);
f_withGenericRefFormal(si$);

sy$.reset();
sy$.writeEF(5*n);

Currently, sync and single variables cannot be written out directly. We need to extract the value, for example using readFE() or readFF().

writeln(sy$.readFE());
writeln(si$.readFF());

Definitions of functions used above

proc f_withSyncIntFormal(x: sync int) {
  writeln("the full bit is: ", x.isFull);
}

proc f_withSingleBoolFormal(x: single bool) {
  writeln("the full bit is: ", x.isFull);
}

proc f_withGenericDefaultIntentFormal(x) {
  writeln("the full bit is: ", x.isFull);
}

proc f_withGenericRefFormal(ref x) {
  writeln("readXX returns: ", x.readXX());
}