纖程允許執行一個線程完成多個任務.與線程相比,切換更有效,類似協程
(更小)與綠色線程.
纖程允許每個線程有多個調用棧.要掌握纖程,必須瞭解線程的調用棧.
參數,局部變量,返回值,函數的臨時表達式,及其他執行時的額外信息
組成了函數的本地狀態
運行時調用函數時自動分配和初化函數的本地狀態.
爲函數調用分配的局部存儲空間叫棧楨(楨),隨着函數調用其他函數,一幀一幀的,當前活動的函數調用是線程的調用棧.
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
,從區間中不改變順序隨機抽取元素,
each
與map
類似.但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
把纖程當作輸入區間
.
纖程簡化了嚴重依賴調用棧的程序及異步輸入出操作,
纖程,協作式多任務處理.