54用d編程纖程

纖程允許執行一個線程完成多個任務.與線程相比,切換更有效,類似協程(更小)與綠色線程.
纖程允許每個線程有多個調用棧.要掌握纖程,必須瞭解線程的調用棧.
參數,局部變量,返回值,函數的臨時表達式,及其他執行時的額外信息組成了函數的本地狀態
運行時調用函數時自動分配和初化函數的本地狀態.
爲函數調用分配的局部存儲空間叫棧楨(楨),隨着函數調用其他函數,一幀一幀的,當前活動的函數調用是線程的調用棧.

void main() {
    int a;int b;
    int c=foo(a, b);
}
int foo(int x, int y) {
    bar(x + y);return 42;
}

void bar(int param) {
    string[] arr;
    // ...
}

會有三級棧幀.
遞歸函數來說,棧幀的優勢更明顯.
遞歸極大的簡化了分而治之的函數.

import std.array;

int sum(int[] arr, int currentSum = 0) {
    if (arr.empty) {
        //不添加元素,已計算的結果
        return currentSum;
    }
    //用剩餘元素加當前元素
    return sum(arr[1..$], currentSum + arr.front);
}

void main() {
    assert(sum([1, 2, 3]) == 6);
}

std.algorithm.sum用特殊算法爲浮點計算更精確的計算.
當遞歸函數返回自己時,編譯器用尾調用優化,爲每個遞歸調用消除調用棧.
多線程時,每個線程擁有自己的線程棧來維護自己的執行狀態
纖程強大之處在於,雖然不是線程,但有自己的調用棧,允許一個線程中有多個調用棧.一個調用棧保持一個任務的執行狀態,從而允許一個線程執行多個任務.

void fiberFunction() {
    // ...
}

纖程從可調用實體開始(函數指針,λ函數)不帶參數也不返回.

import core.thread;
// ...
auto fiber=new Fiber(&fiberFunction);

core.thread.Fiber+可調用實體,可創建纖程.

class MyFiber : Fiber {//繼承
    this() {
        super(&run);//傳遞纖程函數
    }
    void run() {
        // ...
    }
}
//...
auto fiber = new MyFiber();

fiber.call();啓動和恢復纖程.
不像線程,當纖程執行時,調用者就停止執行了.兩個都是同一線程,所以必然的.

void fiberFunction() {
    // ...
        Fiber.yield();
    // ...
}

Fiber.yield(),將執行權交給調用者.
纖程產生後,調用者就恢復了.也有可能由纖程轉到另一個纖程,畢竟多個調用棧嘛,想往哪個棧轉就往哪個棧轉.

    if (fiber.state == Fiber.State.TERM) {
        // ...
    }

狀態由纖程的.state屬性決定.
Fiber.State有以下值:
HOLD,暫停,可啓動/恢復,
EXEC,執行,正在執行.
TERM,終止,再次使用前必須調用reset(),
區間實現的纖程,要記住狀態,

struct FibonacciSeries {
    int current = 0;int next = 1;//兩個狀態

    enum empty = false;

    @property int front() const {
        return current;
    }

    void popFront() {
        const nextNext = current + next;
        current = next;
        next = nextNext;
    }
}

但有些區間不易保存狀態,但用遞歸卻很容易保存狀態.
如以下插入與打印的遞歸實現不定義變量,與樹中包含的元素獨立,插入通過insertOrSet間接遞歸.

import std.stdio;
import std.string;
import std.conv;
import std.random;
import std.range;
import std.algorithm;

struct Node {
//表示二叉樹的節點,在樹實現中使用,不應直接用
    int element;
    Node * left;     // 左子樹
    Node * right;    // 右子樹

    void insert(int element) {
        if (element < this.element) {//小往左
            insertOrSet(left, element);
        }else if(element>this.element){//大往右
            insertOrSet(right, element);
        } else {
            throw new Exception(format("已存在%s", element));
        }
    }

    void print() const {//先打印左子樹元素
        if (left) {
            left.print();write(' ');
        }

        write(element);//打印當前

        if (right) {//打印右子樹
            write(' ');right.print();
        }
    }
}

//插入指定右子樹,可能的話初始化其節點
void insertOrSet(ref Node * node, int element) {
    if (!node) {
        node=new Node(element);//子樹的第一個節點
    } else {
        node.insert(element);
    }
}

struct Tree {
//實際樹表示,允許樹根`無效`的空樹
    Node * root;

    void insert(int element) {
        insertOrSet(root, element);
    }//插元素到樹

    void print() const {
        if (root) {
            root.print();
        }
    }//有序打印
}

//從10乘n個數中取隨機數來填充樹
Tree makeRandomTree(size_t n) {
    auto numbers = iota((n * 10).to!int).randomSample(n, Random(unpredictableSeed)).array;
//取數

    randomShuffle(numbers);//洗牌
    auto tree = Tree();
    numbers.each!(e => tree.insert(e));
    //用這些數來填充樹
    return tree;
}

void main() {
    auto tree = makeRandomTree(10);
    tree.print();
}

randomSample,從區間中不改變順序隨機抽取元素,
eachmap類似.但map生成新元素,each有副作用(可能是覆蓋).map是懶的,each是激進的.
std.range.iota,懶生成元素們(比如區間).
randomShuffle,洗牌,隨機移動.
提供區間接口.以便使用現有算法.

struct InOrderRange {
        //???
    }

    InOrderRange opSlice() const {
        return InOrderRange(root);
    }

雖然打印基本實現了,按序訪問元素.
但不容易爲樹實現輸入區間,這裏不實現了,但你可以研究下樹的迭代器(一些實現要求額外的節點 *指向每個節點的父)
遞歸算法的print很普通的原因是自動管理調用棧,調用棧隱式包含當前元素信息,還包含此時到達的執行程序(如該左/右節點),

  void print() const {
        if (left) {
            left.print();
            write(' ');   //調用棧包含該誰了
        }

        // ...
    }

纖程在類似使用調用棧比顯式維護狀態更容易的時候有用.
爲了簡單,我們包含常見纖程操作,實現樹區間.

import core.thread;

//生成元素的纖程函數,設置`ref`參數
void fibonacciSeries(ref int current) {
    //用參數使當前元素同纖程通信.也可爲`out`
    current = 0;//注意是參數
    int next = 1;

    while (true) {
        Fiber.yield();//下個調用從此開始/恢復
        //當前元素可用時,停止
        const nextNext = current + next;
        current = next;
        next = nextNext;
    }
}

void main() {
    int current;
    Fiber fiber = new Fiber(() => fibonacciSeries(current));//纖程函數不帶參,不能直接用
    //用無參閉包(適配器)傳給纖程函數,

    foreach (_; 0 .. 10) {
        fiber.call();//啓動和恢復
        import std.stdio;
        writef("%s ", current);
    }//纖程就是這樣的(一個線程被分成多個纖程).
}

std.concurrency.Generator,把纖程當區間展示.
未提供區間接口,與現有算法不兼容.
修改引用參數展示元素與複製元素到調用者上下文比不理想
通過成員函數顯式構造使用纖程,暴露低級實現細節
std.concurrency.Generator解決以上問題

import std.stdio;
import std.range;
import std.concurrency;

alias FiberRange = std.concurrency.Generator;//避免衝突名字

void fibonacciSeries() {
    int current = 0;int next = 1;

    while (true) {
        yield(current);
        const nextNext = current + next;
        current = next;
        next = nextNext;
    }
}

void main() {
    auto series = new FiberRange!int(&fibonacciSeries);
//生成的是區間
    writefln("%(%s %)", series.take(10));
}

不是用return返回單元素,而是用yield返回多元素,
用的是std.concurrency.yield,而不是Fiber.yield

import std.concurrency;

alias FiberRange = std.concurrency.Generator;

struct Node {
// ...
    auto opSlice() const {
        return byNode(&this);
    }//已刪打印函數
}

//按序產生下個樹節點的纖程函數
void nextNode(const(Node) * node) {
    if (!node) return;//無節點

    nextNode(node.left);    // 下個左節點
    yield(node);            // 下箇中節點
    nextNode(node.right);   // 下個右節點
}

auto byNode(const(Node) * node) {
//返回到樹的輸入區間
    return new FiberRange!(const(Node)*)(() => nextNode(node));
}

// ...

struct Tree {
// ...

    auto opSlice() const {//已刪打印
        return byNode(this).map!(n => n.element);
    }//節點=>元素的轉換
}

//返回節點樹的輸入區間,如根爲空,則爲空區間
auto byNode(const(Tree) tree) {
    if (tree.root) {
        return byNode(tree.root);

    } else {
        alias RangeType = typeof(return);
        return new RangeType(() {});//空區間
    }
}

用生成器,可容易的按輸入區間展示樹的元素.
特別注意,如何通過遞歸結點nextNode按適配器實現byNode節點.
現在可對樹切片了.

   writefln("%(%s %)", tree[]);

異步輸入出中的纖程
纖程的調用棧可簡化異步輸入出任務.

import std.stdio;
import std.string;
import std.format;
import std.exception;
import std.conv;
import std.array;
import core.thread;

struct User {
    string name;
    string email;
    uint age;
}

class SignOnFlow : Fiber {
//用戶登錄流
    string inputData_;//本流最近數據

    string name;
    string email;
    uint age;//信息

    this() {
        super(&run);//函數啓動點
    }

    void run() {
        name = inputData_;
        Fiber.yield();

        email = inputData_;
        Fiber.yield();

        age = inputData_.to!uint;
        //有信息,可以返回了,而不再產生了
        //纖程狀態變爲`Fiber.State.TERM`
    }

    @property void inputData(string data) {
        inputData_ = data;
    }//從調用者接收數據

    @property User user() const {
        return User(name, email, age);
    }//構造用戶並返回
}

//爲特定流展示從輸入讀取的數據
struct FlowData {
    size_t id;
    string data;
}

//解析相關流數據
FlowData parseFlowData(string line) {
    size_t id;
    string data;

    const items = line.formattedRead!" %s %s"(id, data);
    enforce(items == 2, format("Bad input '%s'.", line));

    return FlowData(id, data);
}

void main() {
    User[] users;
    SignOnFlow[] flows;

    bool done = false;

    while (!done) {
        write("> ");
        string line = readln.strip;

        switch (line) {
        case "hi":
            //從新連接開始流
            flows ~= new SignOnFlow();

            writefln("Flow %s started.", flows.length - 1);
            break;

        case "bye"://退出程序
            done = true;
            break;

        default://按流數據用輸入
            try {
                auto user = handleFlowData(line, flows);

                if (!user.name.empty) {
                    users ~= user;
                    writefln("Added user '%s'.", user.name);
                }

            } catch (Exception exc) {
                writefln("Error: %s", exc.msg);
            }
            break;
        }
    }

    writeln("Goodbye.");
    writefln("Users:\n%(  %s\n%)", users);
}

User handleFlowData(string line, SignOnFlow[] flows) {
//標識輸入的所有者纖程,設置其輸入數據並重用纖程
//如完成流,返回帶有效字段用戶
    const input = parseFlowData(line);
    const id = input.id;

    enforce(id < flows.length, format("Invalid id: %s.", id));

    auto flow = flows[id];

    enforce(flow.state == Fiber.State.HOLD,
            format("Flow %s is not runnable.", id));

    flow.inputData = input.data;//置流數據

    flow.call();//恢復流

    User user;

    if (flow.state == Fiber.State.TERM) {
        writefln("Flow %s has completed.", id);
        user = flow.user;//置返回數據爲新創建用戶
        //待辦:未來可爲新流重用流數組中的本纖程項
        //但首先必須用`flow.reset()`重置.
    }

    return user;
}

從輸入讀流,解析,分發流數據至適當流來處理它,流的調用棧自動保存流狀態.當用戶信息完整時,加新用戶.
運行是普通函數,當其他流登錄時,沒有阻塞.
vibe.d使用類似設計.
異常和纖程.
可利用調用棧實現異常機制.
由於拋出異常,而離開函數清理棧局部變量的行爲叫棧展開.由於異常,先後一個個的清理幀.
纖程有自己的棧,執行纖程時拋出的異常展開纖程而不是調用者的棧.如未抓異常(未處理),則終止纖程,纖程的狀態變爲Fiber.State.TERM.
但有時需要將錯誤條件轉給調用者,而不丟失自身狀態.Fiber.yieldAndThrow允許纖程產生並在調用者自身上下文中拋異常.
age = inputData.to!uint ;
類似這樣

while(true){
    try {
        age = inputData_.to!uint;
        break;  // 成功轉換
    } catch (ConvException exc) {
        Fiber.yieldAndThrow(exc);//相當於重試
    }//拋異常
}

操作系統的線程是不可預測的,搶佔式.而纖程則是協作式.可顯式暫停,調用者也可顯式恢復.從一個線程到另一個線程的切換是很慢的.
而一個線程多個纖程,沒有上下文切換,多個調用棧,多次調用與常規函數調用成本一樣.(不能同時執行調用者和纖程)
協作式多任務處理,調用者和纖程數據都可能位於cpu緩衝中,比從內存讀取快上100倍,進一步提高纖程速度.
調用者與纖程從不同時執行,不存在競爭條件,無需同步數據,但程序員要確保在預期時間產生(yield).如準備好數據時.

void fiberFunction() {
    // ...
        func();           //工作未完成,不能產生
        sharedData *= 2;
        Fiber.yield();    //該產生了.
    // ...
}

一個明顯缺點就是不能利用多核.可以使用M:N(雜模式)來利用多核,都可以去研究研究(不同線程模型).
調用棧可有效簡化遞歸算法.
纖程爲每個線程啓用多個調用棧,模型就是一個調用棧多個纖程,但不同時執行.
纖程自身暫停產生->調用者,調用者->調用,運行纖程.
Generator把纖程當作輸入區間.
纖程簡化了嚴重依賴調用棧的程序及異步輸入出操作,
纖程,協作式多任務處理.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章