chapel

What is the task number specification on "begin" statements? (compared with cobegin, forall, and coforall)


Reading the documentation on Task Parallelism and Begin, I wondered what the specification for the number of tasks a "begin" statement or a "begin" block will create. "Cobegin", as described, creates a task for each statement.

I wondered if there was a ~~parallel~~ between cobegin and begin as exists between forall and coforall (number of threads set in environment variable or number of statements or iterators, respectively). Additionally, I was curious if there exist similar restrictions on what can be done when forking on a begin versus a cobegin like the forall paradigms.

For example, a forall loop must be able to be executed sequentially and each thread cannot access and modify another. How many tasks will a begin statement create and does it hold the same requirements as forall?

My simple test shown below. That second test (with the cobegin) did not function correctly when it was just a begin. The documentation states that the behavior of begin is that the main thread continues after that block immediately, however this says to me there may be similar restrictions or expectations for begin statements as for forall loops.

// https://chapel-lang.org/docs/primers/taskParallel.html
use Time;

config const dur : real(64) = 3.0;
config const run : bool = false;

proc sayHello() {
    writeln("Hello there!");
}

proc sayGoodbye() {
    writeln("Goodbye!");
}

proc main()
{
    // A blocking task parallel segment
    // Expect no specific order of processing
    // except where logical within a function or block {...}
    //
    // Outputs in this case can still be interleaved, as shown with C
    cobegin { 
        // A
        sayHello();

        // B
        sayGoodbye();

        // C
        {
            writeln("I'm a block");
            writeln("^ and this message will come below");
        }
    }

    writeln("\nI wait until everything in the above block is done.");

    if !run then exit(0);

    var a : atomic uint(64) = 0;
    var b : atomic bool = false;
    begin cobegin {
        // Creates a stopwatch and reads the value of atomic int a
        {
            var it : uint(64) = 0;
            var t : stopwatch;
            t.start();
            while (t.elapsed() < dur) {
                if (it % 1000000 == 0) {
                    writeln("The value I read is ", a.read());
                }
                it += 1;
            }
            t.stop();
            b.write(true);
            writeln("\nThe amount of time passed: ", t.elapsed());
            writeln("And the final value I read is ", a.read());
        }

        // Task to constantly increment an atomic int
        {
            while(!b.read()) {
                a.write(a.read()+1);
            }
        }
    }

    writeln("Out here!");

    var t2 : stopwatch;
    t2.start();
    // Creates a task for EACH index
    // 100,000 tasks is slow
    coforall i in 1..#100000 {
        if (i % 5000 == 0) {
            writeln(i);
        }
    }
    writeln("Time elapsed outside: ", t2.elapsed());
    t2.stop();
}

Solution

  • thanks for the question.

    To answer your most specific question, a begin statement will always create a single new task to run the statement that it prefixes. Thus,

    begin foo();
    bar();
    

    will create a single task to run foo() while the original task goes on to execute bar() (and whatever may follow).

    Note that this is true whether the statement that follows is a singleton statement (like foo(); above) or a compound statement as in this example:

    begin { foo(); boo(); goo(); }
    bar();
    

    Specifically, this program will create a single task that will run all of the code in the block, as in any normal compound statement—that is, it will run foo() then boo() then goo() sequentially before terminating. Meanwhile the original task goes on to execute bar() while the new one is doing this.

    This is in contrast with the cobegin statement which can only be used to prefix a compound statement, creating a distinct task for each child statement within it. Thus:

    cobegin { foo(); boo(); goo(); }
    bar();
    

    creates three tasks, one for each of foo(), boo(), and goo(), then waits for them all to complete before going on to execute bar().

    Mixing the two as you did:

    begin cobegin { foo(); boo(); goo(); }
    bar();
    

    creates one task to execute the cobegin while the second task goes on to run bar(). Meanwhile that first task hitting the cobegin creates three more tasks to execute each of foo(), bar(), and goo().

    Your question points out an unintended syntactic symmetry between begin::cobegin and forall::coforall that I somehow never seem to have noticed until you asked today. That is, where the second two are implicit vs. explicit parallel loop forms, the first two might be considered implicit vs. explicit ways of executing a compound statement.

    However, that is not the intention, and the reality is that begin always creates a single task and cobegin always decorates a compound statement and creates a task per child statement. I usually think of begin, cobegin, and coforall as being ways to create 1, several, or arbitrarily many explicit tasks; and for forall to be in its own category, invoking a parallel iterator that creates as many tasks as it wishes.