Lecture 23

SFR1811·2022년 3월 15일
0

CS343-Parallel Programming

목록 보기
20/24
post-thumbnail

9 Direct Communication

9.1 Task

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:


9.2 Scheduling

External Scheduling

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.


Internal Scheduling

_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
  }

9.2.3 Accepting the destructor

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.

profile
3B CS

0개의 댓글