Omp critical

According to Gemini the equivalent of #pragma omp critical is:

var myLock: sync bool; // Lock variable
forall i in 1..10 {
  sync myLock {
    // Critical section: only one task executes this block at a time
    writeln("Task ", i, " is in the critical section.");
  }
}

However, I cannot find a description of such a sync statement in the Chapel documentation. The one I can find it to just ensure all threads are joined. Is Gemini hallucinating? If so, what is a simple way to achieve this in Chapel? My current approach uses sync boolean variables in a more elaborate way which makes me distrust it.

Thanks.

--shiv--

Hi @00shiv

I think you are correct that Gemini is hallucinating. As you say, the Chapel sync statement is a dynamically scoped "join tasks" concept.

The two ways to do this natively in the language would be to use a sync or atomic variable to guard a critical section. E.g., using a sync, I might write:

use Time;

config const nTasks = here.maxTaskPar;
        
var myLock: sync int; 
coforall i in 1..nTasks { 
  myLock.writeEF(i);  // blocks until empty, leaves full

  sleep(1);
  writeln("task ", i, " in critical section");
  sleep(1);

  const completedTask = myLock.readFE();  // readFE leaves empty
  writeln("task ", completedTask, " finished");
}

Here, the use of the int value in the sync int is almost beside the point since we're really only using this sync for its full/empty state and not its value. I'm obviously using it to store a logical task ID, but not for any deep purpose, and I could just as easily write the same integer for each task. We've discussed adding support for a sync void that only had the full empty state for this reason, but it doesn't look like we've ever gotten around to adding that.

Using an atomic, I might write this pattern like:

use Time;

config const nTasks = here.maxTaskPar;
        
var myLock: atomic bool; 
coforall i in 1..nTasks {
  do {
    var expect = false;
  } while !myLock.compareExchange(expect, true);

  sleep(1);
  writeln("task ", i, " in critical section");
  sleep(1);

  myLock.write(false);
  writeln("task ", i, " done");
}

where here, I am using the value of the boolean to indicate whether the lock is held or not.

Generally, when giving guidance between using a sync or an atomic I tend to recommend the sync in pessimistic situations where you imagine that most tasks will be waiting for their turn to get into the critical section, because they have generally been architected to minimize spending resources on blocked tasks; whereas in a situation where you expect most tasks to not be waiting on the critical section, I tend to think of the atomic as being better, in terms of being a lighter-weight mechanism, but one that uses more busy-waiting, at least as I've written it here with the continual spin on the compareExchange() call.

I'll also mention that there's an internal module modules/internal/ChapelLocks.chpl that implements a local spin lock that we use internally in our code a fair amount and that we've discussed making into a standard library or mason package. It isn't intended to be user facing and isn't stable as a result, but could be worth looking at, and of course you're welcome to use it at your own risk.

Hope this is helpful,
-Brad

Hi @bradcray,

Thanks for the quick reply. I am currently using sync variables. But, the whole {read/write}{F/E}{F/E} API is not confidence inspiring --- the compiler cannot help if I get it wrong, and neither does runtime testing guarantee much.

In some cases I use arrays of atomic variables. However I am not sure how scalable this approach is.

It would be nice to have a critical {statements;} construct in Chapel and just mimic whatever OpenMP does.

Thanks.

--shiv--