51用d編程並行

std.parallelism程序在多核中並行運算.
僅當並行操作真正獨立時,才使用這些算法.
parallel,並行訪問區間元素
task,並行執行的任務,
asyncBuf,半激進並行迭代輸入區間元素
map,用輸入區間元素半激進並行調用函數
amap,用隨機訪問區間元素全激進並行調用函數
reduce,通過隨機訪問區間元素並行計算

void main(){
    auto students =
        [ Student(1), Student(2), Student(3), Student(4) ];

    foreach (student; students) {
        student.aSlowOperation();
    }//如果每個學生操作相互獨立,則可以並行計算
}

import std.stdio;
import core.thread;

struct Student {
    int number;

    void aSlowOperation() {
        writefln("The work on student %s has begun", number);

        // 等待來模擬長時操作
        Thread.sleep(1.seconds);

        writefln("The work on student %s has ended", number);
    }
}

有多少核:

import std.stdio;
import std.parallelism;

void main() {
    writefln("系統有%s個核.", totalCPUs);
}

taskPool.parallel()
parallel()並行訪問區間的元素.
僅這樣,就可以並行執行了:

import std.parallelism;
// ...
    foreach (student; parallel(students)) {
//多核同時並行運行

parallel返回一個知道如何對每個元素分發閉包執行給多個核.
程序可同時包含多個主動執行線程.操作系統啓動並執行u並調度核心上的每個線程.
如果多個對象是獨立的,則可以並行.即無序執行.
支持依賴其他線程的線程編程模型,叫併發.
= parallel(range, work_unit_size = 100);

    foreach (student; parallel(students, 2)) {
        // ...
    }

有時,循環操作短,這時一個線程執行多個元素會比較快(減少成本),工作單元決定線程每次操作幾個元素.循環內容快內容少,最好就多點.不浪費線程.一般爲100,基本上合適.
非隨機訪問的,會序化.
parallel()工作在asyncBuf() 和 map()上時,忽略工作單元大小數,而是重用結果區間的內部緩衝區.
同程序的其他操作並行執行的叫任務.如std.parallelism.Task.
parallel爲每個工作線程構造一個新任務並自動啓動它,parallel再等待所有線程完成任務後才退出.因爲它自動構造,啓動,等待任務,所以,很有用.
當任務與區間不相關或不能由區間表示時,程序員可顯式處理他們.構造task() , 啓動executeInNewThread() , 執行yieldForce()
執行以下代碼anOperation兩次,打印id的第一個字母來表明哪個任務在幹活.
stdout不直接輸出,而是放入輸出緩衝區中,直到完成一行才輸出,寫不輸出換行符,在到達行尾前調用stdout.flush()來輸出數據.

import std.stdio;
import std.parallelism;
import std.array;
import core.thread;

//每半秒打印首字母,返回1來模擬計算,在主中使用結果.
int anOperation(string id, int duration) {
    writefln("%s 將花%s 秒", id, duration);

    foreach (i; 0 .. (duration * 2)) {
        Thread.sleep(500.msecs);  /* 半秒*/
        write(id.front);stdout.flush();
    }

    return 1;
}

void main() {
    //構造執行操作的任務,把指定函數參數傳遞給任務函數
    auto theTask = task!anOperation("theTask", 5);
    theTask.executeInNewThread();//啓動
    //任務執行時,再直接在主調用`aa..`,
    immutable result = anOperation("main's call", 3);

    //此時完成操作,常規函數操作發起的
    //不確定完成`任務`,等待操作,返回值爲結果

    immutable taskResult = theTask.yieldForce();
    writeln();
    writefln("完成了;結果是%s.",result + taskResult);
}

上面任務函數作爲任務的參數task!anOperation

import std.parallelism;

double foo(int i) {
    return i * 1.5;
}

double bar(int i) {
    return i * 2.5;
}

void main() {
    auto tasks = [ task!foo(1),
                   task!bar(2) ];    // 編譯錯誤
}

類型不一樣,這時應該用類似c++的函數<雙精(整)>.
下面也是一種,task的重載接受函數地址作參數,避免模板實例化

import std.parallelism;

double foo(int i){
     return i * 1.5;
}
double bar(int i){
     return i * 2.5;
}
void main(){
     auto tasks=[任務(&foo,1),任務(&bar,2)];//編譯
}//加個地址

這樣也可以,task接個閉包(λ函數)

    auto theTask = task((int value) {
                            /* ... */
                        }, 42);

由不同線程執行任務時,任務自動抓異常.要重拋的話,調用任務yieldForce函數,使主線程能夠抓異常.

import std.stdio;
import std.parallelism;
import core.thread;

void mayThrow() {
    writeln("啓動mayThrow() ");
    Thread.sleep(1.seconds);
    writeln("mayThrow()拋異常了");
    throw new Exception("錯誤消息");
}

void main() {
    auto theTask = task!mayThrow();
    theTask.executeInNewThread();

    writeln("繼續main");
    Thread.sleep(3.seconds);

    writeln("main在等待任務");
    theTask.yieldForce();
}

任務拋出的未抓異常,只停止任務,而主程序未停止.

    try {
        theTask.yieldForce();

    } catch (Exception exc) {
        writefln("Detected an error in the task: '%s'", exc.msg);
    }

試-抓塊中調用yieldForce,可抓任務拋出的異常.這與單線程不一樣,單線程抓可能拋的代碼的異常.
並行時,它包裝yieldForce.
任務的成員函數:

    if (theTask.done) {//done,任務是否完成
        writeln("是,已完成任務");

    } else {
        writeln("不,還在進行");
    }

done,是否完成,如任務被異常終止時重拋異常.
executeInNewThread,在新線程執行任務,
executeInNewThread(int priority),以特定優先級(操作系統)在新線程執行任務,
有三個函數等待任務完成.
yieldForce,未啓動,則啓動;已完成,返回值;在運行,則等待;拋異常,則重拋;
spinForce,如在運行,則使微控器忙,其餘與上一樣.
workForce,在等待任務完成時,開啓新任務,其餘與第1個一樣
一般,第1個就可以了,如果想盡快完成任務,用第2個,第3個用於啓動其他任務而不是暫停當前任務.
其他看標準庫的Task.
taskPool.asyncBuf()
類似parallel,asyncBuf並行迭代輸入區間,
他將區間產生的元素放在緩衝,並轉給用戶.
其實都是折騰,一開始就不用區間,行不行?
它一波一波的將區間元素轉成緩衝,等到都消費完了,再來一波.說是折騰不爲過

auto elements = taskPool.asyncBuf(range, buffer_size);

參數爲:一個源區間,一個緩衝大小(一個波次大小),

import std.stdio;
import core.thread;

struct Range {
    int limit;
    int i;

    bool empty() const @property {
        return i >= limit;
    }

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

    void popFront() {
        writefln("在%s後產生元素", i);
        Thread.sleep(500.msecs);
        ++i;
    }
}

void main() {
    auto range = Range(10);

    foreach (element; range) {
        writefln("用元素%s", element);
        Thread.sleep(500.msecs);
    }
}

用半秒迭代,半秒處理的區間,只生成指定數的緩衝.
元素是懶生產並使用的.

import std.parallelism;
//...
    foreach(element;taskPool.asyncBuf(range,2)){

並行.也可在外部使用

    auto range = Range(10);
    auto asyncRange = taskPool.asyncBuf(range, 2);
//作爲輸入區間
    writeln(asyncRange.front);

taskPool.map()
如果無論怎樣都要調用且每個操作相互獨立,就比並行慢得很了.
std.parallelism中的taskPool.map()和taskPool.amap()在多核時多種情況下都要快些.

import std.stdio;
import std.algorithm;
import core.thread;

struct Student {
    int number;
    int[] grades;

    double averageGrade() @property {
        writefln("在%s學生上執行",number);
        Thread.sleep(1.seconds);
        const average =grades.sum / grades.length;
        writefln("在%s學生上執行完", number);
        return average;
    }
}

void main() {
    Student[] students;

    foreach (i; 0 .. 10) {/* 每個學生兩個得分*/
        students ~= Student(i, [80 + i, 90 + i]);
    }
    auto results = map!(a => a.averageGrade)(students);
    //函數/λ作模板參數,區間作函數參數
    foreach (result; results) {
        writeln(result);
    }
}

taskPool.map()半激進執行程序,並把結果放在緩衝區供執行,緩衝大小由第2個參數決定.

import std.parallelism;
// ...
double averageGrade(Student student) {
    return student.averageGrade;
}
// ...
    auto results = taskPool.map!averageGrade(students, 3);

同樣,函數爲模板參數,區間爲函數參數.加個緩衝大小.需要自由函數,
auto results =taskPool.map!(a => a.averageGrade)(students, 3);,這樣是編譯不過的.

 ...= taskPool.map!func(range, buffer_size = 100,work_unit_size = size_t.max);

緩衝大小與工作單元大小,注意區別.
taskPool.amap()
與上面的區別是:1,全激進.2,與同隨機訪問區間工作.

  auto results = taskPool.amap!averageGrade(students);

amap()map()快的原因是,前面的用大把的內存來存儲結果,花內存得速度.
auto results = taskPool.amap!averageGrade(students, 2);
第2個參數爲工作單元數.第2個參數放存儲位置(隨機訪問區間).

    double[] results;
    results.length = students.length;
    taskPool.amap!averageGrade(students, 2, results);

taskPool.reduce()類似fold,化簡/摺疊.區別是,他們的函數參數是相反的.因而,對非並行代碼,推薦fold,在區間表達式中可利用統一調用.
reduce是許多函數式語言的常見的高階算法.以1/多個函數爲模板參數,以初始值作爲結果,用結果的當前值和區間中的每個元素來調用函數,當沒有初始值時,用區間的第一個元素.就是個遞歸.

import std.stdio;
import std.algorithm;

void main() {
    writeln(reduce!((a, b) => a + b * b)(0, [5, 10]));
}
r=i;循環(區間元素e)r=f(r,e);中 r;

因而多任務版taskPool.reduce()出現了.可以利用多核.

import std.stdio;
import std.algorithm;
import core.thread;

int aCalculation(int result, int element) {
    writefln("started  - element: %s, result: %s",
             element, result);

    Thread.sleep(1.seconds);
    result += element;

    writefln("finished - element: %s, result: %s",
             element, result);

    return result;
}

void main() {
    writeln("Result: ", reduce!aCalculation(0, [1, 2, 3, 4]));
}

替換:

import std.parallelism;
// ...
    writeln("Result: ", taskPool.reduce!aCalculation(0, [1, 2, 3, 4]));

應用非常受限.參數類型與返回類型要相同,
同時,如果必須按順序計算,則多核並沒有什麼好處.在本程序中並行版本的更慢.
關鍵是把程序設計成獨立的,無依賴的.
還可以帶多個函數作模板參數

import std.stdio;
import std.algorithm;
import std.conv;

double quarterOf(double value) {
    return value / 4;
}

string tenTimes(double value) {
    return to!string(value * 10);
}

void main() {
    auto values = [10, 42, 100];
    auto results = map!(quarterOf, tenTimes)(values);

    writefln(" Quarters  Ten Times");

    foreach (quarterResult, tenTimesResult; results) {
        writefln("%8.2f%8s", quarterResult, tenTimesResult);
    }
}

結果用元組表示.一個結果一個位置.
並行算法容器用的是taskPool.

import std.stdio;
import std.parallelism;

void main() {
    auto workers = new TaskPool(2);

    foreach (i; workers.parallel([1, 2, 3, 4])) {
        writefln("Working on %s", i);
    }//調用`並行`.

    workers.finish();//完成任務時停止.
}

任務池,包含一堆線程,默認比多核數少1.

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