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.