A task is like a coroutine but it creates its own thread.
And only one thread is active in the Task object.
Case Analysis of Objects:
A task may want to schedule access to itself by other tasks - not necessarly in the order that they arrive.
_Task BoundedBuffer {
int front = 0, back = 0, count = 0;
int Elements[20];
public:
_Nomutex int query() const { return count; }
void insert( int elem ) {
Elements[back] = elem;
back = ( back + 1 ) % 20;
count += 1;
}
int remove() {
int elem = Elements[front];
front = ( front + 1 ) % 20;
count -= 1;
return elem;
}
private:
void main() {
for ( ;; ) { // INFINITE LOOP!!!
// _Accept( insert || remove );
_When ( count != 20 ) _Accept( insert ) {
// after call
} or _When ( count != 0 ) _Accept( remove ) {
// after call
}
}
}
};
or
operand is functioning as follows
_Accept(insert, remove) === _Accept(insert){} or _Accept(remove){}
Unlike Monitor where it starts with no thread in it, the task version starts with a thread init, so it can activly pull threads in to the mutex zone and go to acceptor stack. when the job is over, it comes back and repeats the loop.
advancement: take advantage of the task thread
void insert( int elem ) {
Elements[back] = elem;
}
int remove() {
return Elements[front];
}
private:
void main() {
for ( ;; ) { // INFINITE LOOP!!!
// _Accept( insert || remove );
_When ( count != 20 ) _Accept( insert ) {
back = ( back + 1 ) % 20;
count += 1;
} or _When ( count != 0 ) _Accept( remove ) {
front = ( front + 1 ) % 20;
count -= 1;
}
}
}
The essence of task is to leverage more work to the Task rather than letting the caller do all the works.
_Task BoundedBuffer {
uCondition full, empty;
int front = 0, back = 0, count = 0;
int Elements[20];
public:
_Nomutex int query() const { return count; }
void insert( int elem ) {
if ( count == 20 ) empty.wait();
Elements[back] = elem;
back = ( back + 1 ) % 20;
count += 1;
full.signal();
}
int remove() {
if ( count == 0 ) full.wait();
int elem = Elements[front];
front = ( front + 1 ) % 20;
count -= 1;
empty.signal();
return elem;
}
private:
void main() {
for ( ;; ) {
_Accept( insert | | remove );
// do other work
}
}
};
This is not good implementation because all the administration works are done by caller and the Task is only waiting for calls over and over.
This is just like Monitor but just used Task.
pattern:
void insert( int elem ) {
if ( count == 20 ) empty.wait(); // only wait if necessary
Elements[back] = elem;
}
int remove() {
if ( count == 0 ) full.wait(); // only wait if necessary
return Elements[front];
}
private:
void postInsert() { // helper members
back = ( back + 1 ) % size;
count += 1;
}
void postRemove() {
front = ( front + 1 ) % size;
count -= 1;
}
void main() {
for ( ;; ) {
_Accept( insert ) {
if ( count != 20 ) { // producer did not wait ?
postInsert();
if ( ! full.empty() ) { // waiting consumers ?
full.signalBlock(); // wake and adjust
postRemove();
}
}
} or _Accept( remove ) {
if ( count != 0 ) { // consumer did not wait ?
postRemove();
if ( ! empty.empty() ) { // waiting producers ?
empty.signalBlock(); // wake and adjust
postInsert();
}
}
} // _Accept
} // for
}
Task only destructs when the main of the task is terminated.
To do this, you can do something like this:
public:
void stop(){}
. . .
private:
void main(){
for(;;){
_Accept(stop){
break;
} or _When ( count != 20 ) _Accept( insert ) {
. . .
} or _When ( count != 0 ) _Accept( remove ) {
. . .
}
}
}
Since usually what people do after calling stop
routine is to destruct the object, they wanted to make accept destuctor:
void main(){
_Accept(~BoundedBuffer){
break;
} or ...
}
This SHOULD NOT WORK
because call to a destructor wipes out stack so returning to main after destructing object results in segfault.
BUT because makers of uC++ wanted to do this so bad, so they cheated and made it so that when destructor is in _Accept(~destructor)
, instead of calling signalBlock()
and puting current-running-thread in the queue, it called signal()
to the destructor-calling-thread so that it breaks out of main as normal and then the destructor is called.