消息傳遞是安全的併發方法.
多線程讀寫相同數據.共享數據不安全,就是大家(不受控的線程)都要來爭.本章雖然簡單,但實際中經常遇見.儘管用std.concurrency
,對core.thread
也是適用的.
在d中不是自動共享的.
默認爲tls(本地線程)的.儘管所有線程都可訪問模塊
級變量,但每個線程只是得到一個副本.
import std.stdio;
import std.concurrency;
import core.thread;
int variable;
void printInfo(string message) {
writefln("%s: %s (@%s)", message, variable, &variable);
}
void worker() {
variable = 42;
printInfo("結束工作前");
}
void main() {
spawn(&worker);
thread_joinAll();
printInfo("結束工作後");
}
兩個地址和值是不一樣的.兩份.
因而,spawn
不允許傳遞引用給線本
對象.
import std.concurrency;
void worker(bool * isDone) {
while (!(*isDone)) {
// ...
}
}
void main() {
bool isDone = false;
spawn(&worker, &isDone);// 編譯錯誤,引用
// ...
// 希望發出信號表明終止
isDone = true;
// ...
}
線程本地數據,給不了其他線程,是引用的地址.
std.concurrency
的static assert
避免訪問其他線程的可變數據.
但是可以訪問全局共享(整個程序獨一份)的__gshared
.在與c/c++
庫(默認數據共享)交流時需要__gshared
.
線程間可變數據的共享,必須要有shared
關鍵字.
import std.concurrency;
void worker(shared(bool) * isDone) {
while (*isDone) {
// ...
}
}
void main() {
shared(bool) isDone = false;
spawn(&worker, &isDone);
// ...
isDone = true;// 通知可以結束了:
// ...
}
一般先考慮傳遞消息
,再用共享
.
immutable
是可共享的.暗含共享
.
import std.stdio;
import std.concurrency;
import core.thread;
void worker(immutable(int) * data) {
writeln("data: ", *data);
}
void main() {//不變的數據,隨便用
immutable(int) i = 42;
spawn(&worker, &i); //編譯
thread_joinAll();
}
無論在哪的不變
都是可以隨便用的.
core.thread.thread_joinAll
等待所有子線程結束.
競技條件示例
在線程間共享可變數據
時,要小心程序
是否正確
.
考慮多線程共享相同可變變量.
import std.stdio;
import std.concurrency;
import core.thread;
void swapper(shared(int) * first, shared(int) * second) {
foreach (i; 0 .. 10_000) {
int temp = *second;
*second = *first;*first = temp;
}
}
void main() {
shared(int) i = 1;
shared(int) j = 2;
writefln("before: %s and %s", i, j);
foreach (id; 0 .. 10) {
spawn(&swapper, &i, &j);
}
thread_joinAll();//等待所有線程來完成他們任務
writefln("after : %s and %s", i, j);
}
由於競爭
關係,都破壞了,結果是隨機的,10個線程在隨機改.都是瞎搞.上面原因就是多個線程,至少1個都在改.
用synchronized
(同步)來避免都來競爭.必須拿到鎖才能做事情,否則就等吧.
foreach (i; 0 .. 10_000) {
synchronized {
int temp = *b;
*b = *a;
*a = temp;
}
}
改爲,多個線程,要同步.
鎖很貴.在有些情況下,可以用原子
來保證正確,而不用鎖.
要同步多個塊時,對每個塊加同步
.
void incrementer(shared(int) * value) {
foreach (i; 0 .. count) {
*value = *value + 1;
}//如分別對此
}
void decrementer(shared(int) * value) {
foreach (i; 0 .. count) {//及此同步,是不正確的
*value = *value - 1;
}//因爲鎖不一樣
}
要如下才能同步,光一個同步是不行的.
import std.stdio;
import std.concurrency;
import core.thread;
enum count = 1000;
class Lock {
}
void incrementer(shared(int) * value, shared(Lock) lock) {
foreach (i; 0 .. count) {
synchronized (lock) {//同步鎖
*value = *value + 1;
}
}
}
void decrementer(shared(int) * value, shared(Lock) lock) {
foreach (i; 0 .. count) {
synchronized (lock) {//同步鎖
*value = *value - 1;
}
}
}
void main() {
shared(Lock) lock = new shared(Lock)();
//任何類對象都可用來作爲同步鎖
shared(int) number = 0;
foreach (i; 0 .. 100) {
spawn(&incrementer, &number, lock);
//必須用同一把鎖來同步,所以必須共享鎖
spawn(&decrementer, &number, lock);
}
thread_joinAll();
writeln("Final value: ", number);
}
也可定義類類型
同步
synchronized class Class {
void foo() {
// ...
}
void bar() {
// ...
}
}
即在給定類對象上,同步所有(非靜態)成員函數.
等價於如下類:
class Class {
void foo() {
synchronized (this) {
// ...
}
}
void bar() {
synchronized (this) {
// ...
}
}
}
在多個對象上同步時,需要同時指定所有對象.否則你等待我,我等待你,造成死鎖.
例如多線程中轉賬.
void transferMoney(shared BankAccount from,
shared BankAccount to) {
synchronized (from) {
synchronized (to) {//分開了,是錯誤的
// ...
}
}
}
有可能兩個線程轉賬,都在鎖定.A鎖定from(A),B鎖定from(B),結果都鎖定了,都在等待,死鎖了.
void transferMoney(shared BankAccount from,
shared BankAccount to) {
// 注意: dmd 2.074.0不支持
synchronized (from, to) {
// ...
}
}
要這樣,按順序挨個挨個鎖上,
shared static this
用於共享初化,只初化一次,static this
而則每個線程都要執行一次,以便所有線程初化模塊級變量.
shared static ~this()
共享析構.
import std.stdio;
import std.concurrency;
import core.thread;
static this() {//要出錯
writeln("執行static this()");
}
void worker() {
}
void main() {
spawn(&worker);
thread_joinAll();
}
要這樣
int a; // 線本
immutable int b; // 所有線程,不變始終是安全的
//當然,如果多次初始化,那一定要出錯
static this() {
writeln("每線程變量", &a);
a = 42;
}
shared static this() {
writeln("每程序變量", &b);
b = 43;
}
同樣,shared static ~this()
,程序級別的最終處理.
原子操作
.
另一種確保只一個線程改變特定變量的方法是使用原子操作
.
由微控器,編譯器,操作系統
提供.
d的原子操作在core.atomic
.
atomicOp
,應用像"+", "+="
樣的模板參數到其雙函數參數中.
import core.atomic;
// ...
atomicOp!"+="(*value, 1);//原子
等價於非原子行的星value += 1;
只是原子操作保證單線程執行(不受其他線程干擾).
因而,當只需要同步二元操作時,沒必要使用同步
塊(因爲鎖,所以太慢)
import core.atomic;
//...
void incrementer(shared(int) * value) {
foreach (i; 0 .. count) {
atomicOp!"+="(*value, 1);
}
}
void decrementer(shared(int) * value) {
foreach (i; 0 .. count) {
atomicOp!"-="(*value, 1);
}
}
由於是原子操作,所以不用擔心出錯了.沒必要再用鎖類了.
也可與其他二元操作一起使用atomicOp
.
cas
大名鼎鼎的比交
(比較交換).如果仍是當前值,則改變
通過同時指定當前值與期望值來用它.
如bool is_mutated = cas(address_of_variable, currentValue, newValue);
仍等於當前值,表明沒其他線程改變它.則給它賦值爲新值,並返回真
.相反,不同,則不變,返回假
.
void incrementer(shared(int) * value) {
foreach (i; 0 .. count) {
int currentValue;
do {
currentValue = *value;
} while (!cas(value, currentValue, currentValue + 1));
}
}
void decrementer(shared(int) * value) {
foreach (i; 0 .. count) {
int currentValue;
do {
currentValue = *value;
} while (!cas(value, currentValue, currentValue - 1));
}
}
重讀舊值
,直到成功操作,調用cas(…).也叫如值爲舊值,則用新值替換.其實就是個自旋鎖.不斷的循環.
上面的函數塊無需同步
即可正常操作.
多數時候,core.atomic
模塊比用同步塊
快幾倍.因此儘量用原子
.
原子操作允許無鎖編程
數據結構.
你還可以查看core.sync
,其包含經典的併發原語.如:
core.sync.barrier core.sync.condition core.sync.config core.sync.exception core.sync.mutex core.sync.rwmutex core.sync.semaphore
不依賴時,並行(任務).依賴時,併發.
靠傳遞消息更好
只能共享
shared數據.不變
隱含共享
.
_ _gshared
用於同c/c++交流.
給類定義synchronized
,則只有其他線程不在其上面操作時,才能執行成員函數.特定時間只能執行一個成員函數.