53用d編程共享數據

消息傳遞是安全的併發方法.
多線程讀寫相同數據.共享數據不安全,就是大家(不受控的線程)都要來爭.本章雖然簡單,但實際中經常遇見.儘管用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.concurrencystatic 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,則只有其他線程不在其上面操作時,才能執行成員函數.特定時間只能執行一個成員函數.

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