比特幣原始碼分析:多執行緒檢查指令碼

姜家志發表於2018-03-20

多執行緒指令碼檢查啟動

多執行緒指令碼檢查啟動程式碼:

bool AppInitMain(Config &config, boost::thread_group &threadGroup, CScheduler &scheduler) {
    ...
    if (nScriptCheckThreads) {
        for (int i = 0; i < nScriptCheckThreads - 1; i++) {
            threadGroup.create_thread(&ThreadScriptCheck);
        }
    }
    ...
}
static CCheckQueue<CScriptCheck> scriptcheckqueue(128);     

void ThreadScriptCheck() {
    RenameThread("bitcoin-scriptch");
    scriptcheckqueue.Thread();  
}
複製程式碼

AppInitMain 中根據選項,建立多個執行緒。 此處使用了boost的執行緒庫,在繫結的執行緒函式ThreadScriptCheck中,呼叫一個全域性狀態的任務佇列scriptcheckqueue。每個執行緒都去該佇列中去任務,當佇列中無任務可執行時,執行緒被條件變數阻塞。

任務佇列

任務佇列程式碼:

template <typename T> class CCheckQueue {
private:
    boost::mutex mutex;
    boost::condition_variable condWorker;
    boost::condition_variable condMaster;
    std::vector<T> queue;
    int nIdle;
    int nTotal;
    bool fAllOk;
    unsigned int nTodo;
    bool fQuit;
    unsigned int nBatchSize;
    bool Loop(bool fMaster = false);
public:
    //! Create a new check queue
    CCheckQueue(unsigned int nBatchSizeIn)
            : nIdle(0), nTotal(0), fAllOk(true), nTodo(0), fQuit(false),
              nBatchSize(nBatchSizeIn) {}

    void Thread() { Loop(); }
    
    bool Wait() { return Loop(true); }
    
    void Add(std::vector<T> &vChecks) {
        boost::unique_lock<boost::mutex> lock(mutex);
    
        for (T &check : vChecks) {
            queue.push_back(std::move(check));
        }
    
        nTodo += vChecks.size();
        if (vChecks.size() == 1) {
            condWorker.notify_one();
        } else if (vChecks.size() > 1) {
            condWorker.notify_all();
        }
    }
    bool IsIdle() {
        boost::unique_lock<boost::mutex> lock(mutex);
        return (nTotal == nIdle && nTodo == 0 && fAllOk == true);
    }
    ~CCheckQueue() {}
}

bool CCheckQueue::Loop(bool fMaster = false){
    boost::condition_variable &cond = fMaster ? condMaster : condWorker;

    std::vector<T> vChecks; 
    vChecks.reserve(nBatchSize);
    unsigned int nNow = 0;      
    bool fOk = true;
    do {
        {
            boost::unique_lock<boost::mutex> lock(mutex);       
            // first do the clean-up of the previous loop run (allowing us
            // to do it in the same critsect)  
            if (nNow) {
                fAllOk &= fOk;
                nTodo -= nNow;
                if (nTodo == 0 && !fMaster)
                    // We processed the last element; inform the master it
                    // can exit and return the result  
                    condMaster.notify_one();
            } else {
                nTotal++;
            }
           
            while (queue.empty()) {
                if ((fMaster || fQuit) && nTodo == 0) {
                    nTotal--;
                    bool fRet = fAllOk;     
                    // reset the status for new work later
                    if (fMaster) fAllOk = true;
                    return fRet;
                }
                nIdle++;
                cond.wait(lock); 
                nIdle--;
            }
            nNow = std::max(
                1U, std::min(nBatchSize, (unsigned int)queue.size() /
                                             (nTotal + nIdle + 1)));
            vChecks.resize(nNow);
            for (unsigned int i = 0; i < nNow; i++) {
                vChecks[i].swap(queue.back());
                queue.pop_back();       //將放到區域性佇列中的任務清除
            }
            fOk = fAllOk;
        }
        // execute work; 執行本執行緒剛分到的工作。
        for (T &check : vChecks) {
            if (fOk) fOk = check();
        }
        vChecks.clear();
    } while (true);
    
}
複製程式碼

使用解讀:

  • boost::mutex mutex;: 互斥鎖保護內部的狀態
  • boost::condition_variable condWorker;: 在沒有工作時,工作執行緒阻塞條件變數
  • boost::condition_variable condMaster;: 在沒有工作時,master執行緒阻塞條件變數
  • std::vector<T> queue;: 要處理元素的佇列
  • int nIdle;: 空閒的工作執行緒數量(包含主執行緒)
  • int nTotal;: 總的工作執行緒的數量,包含主執行緒
  • bool fAllOk;: 臨時評估結果
  • unsigned int nTodo;: 還有多少驗證任務沒有完成,包括不在排隊的,但仍在工作執行緒自己的批次中的任務數量
  • bool fQuit;: 是否需要退出
  • unsigned int nBatchSize;: 每個批次最大的元素處理數量

佇列中使用了模板類,執行的驗證任務由T標識,T都必須提供一個過載的operator()方法,並且反回一個bool。 預設為主執行緒push 批量任務到佇列中,其他的工作執行緒去處理這些任務,當主執行緒push完任務後,也去處理這些任務,直到任務佇列全部處理完畢。 上述是佇列的實現:主要的任務處理是在Loop()函式中; 該佇列會進行兩種呼叫,來處理佇列中的任務:

  1. 新增任務後:自動喚醒阻塞的工作執行緒去處理新增的任務;細節請看:void Add(std::vector<T> &vChecks)
  2. 主執行緒新增完任務後,呼叫bool Wait(),也去處理佇列中的任務,佇列中的全部任務處理完後,主執行緒退出。 void Add():給類的內部佇列批量新增任務,本次操作受鎖保護,並更新所有的狀態。

如果剛新增的任務數量為1,只喚醒一個工作執行緒去處理;否則,喚醒全部工作執行緒。

採用RAII機制去操作任務佇列

RAII機制(Resource Acquisition Is Initialization)是Bjarne Stroustrup首先提出的。要解決的是這樣一個問題:

在C++中,如果在這個程式段結束時需要完成一些資源釋放工作,那麼正常情況下自然是沒有什麼問題,但是當一個異常丟擲時,釋放資源的語句就不會被執行。 於是 [Bjarne Stroustrup] 就想到確保能執行資源釋放程式碼的地方就是在這個程式段(棧幀)中放置的物件的解構函式了,因為 stack winding 會保證它們的解構函式都會被執行。

將初始化和資源釋放都移動到一個包裝類中的好處:

  • 保證了資源的正常釋放
  • 省去了在異常處理中冗長而重複甚至有些還不一定執行到的清理邏輯,進而確保了程式碼的異常安全。
  • 簡化程式碼體積。
template <typename T> class CCheckQueueControl {
private:
    CCheckQueue<T> *pqueue;
    bool fDone;

public:
    CCheckQueueControl(CCheckQueue<T> *pqueueIn)
        : pqueue(pqueueIn), fDone(false) {
        if (pqueue != nullptr) {
            bool isIdle = pqueue->IsIdle();    
            assert(isIdle);
        }
    }
    
    bool Wait() {
        if (pqueue == nullptr) return true;
        bool fRet = pqueue->Wait();    
        fDone = true;
        return fRet;
    }

    void Add(std::vector<T> &vChecks) {
        if (pqueue != nullptr) pqueue->Add(vChecks);
    }
    
    ~CCheckQueueControl() {
        if (!fDone) Wait();
    }
};
複製程式碼

該類主要是用來管理 CCheckQueue物件;採用RAII機制,保證每次析構該類的物件時,CCheckQueue中的任務佇列被全部處理。 用來構建該物件的任務佇列只能是nil, 或者佇列中無任務。 因為建立的該物件在析構時會呼叫任務佇列的wait()方法去處理完佇列中所有的任務,然後退出。 方法解釋:

  • bool Wait()處理完佇列中的所有任務後,該方法退出,並返回這些任務的處理結果
  • void Add()向 CCheckQueue 中新增任務,喚醒子執行緒去處理
  • ~CCheckQueueControl()物件析構時,呼叫wait()方法保證了該佇列中的所有任務都被處理

CCheckQueue的使用

在塊來的時候啟用主鏈使用使用了檢查佇列:

static bool ConnectBlock(const Config &config, const CBlock &block, CValidationState &state, CBlockIndex *pindex,
    CCoinsViewCache &view, const CChainParams &chainparams, bool fJustCheck = false) {
    ...
    
    CCheckQueueControl<CScriptCheck> control(fScriptChecks ? &scriptcheckqueue : nullptr);
        ...
    for (size_t i = 0; i < block.vtx.size(); i++) {
        ...
        if (!tx.IsCoinBase()) {
            Amount fee = view.GetValueIn(tx) - tx.GetValueOut();
            nFees += fee.GetSatoshis();

            // Don't cache results if we're actually connecting blocks (still
            // consult the cache, though).
            bool fCacheResults = fJustCheck;

            std::vector<CScriptCheck> vChecks;
          
            if (!CheckInputs(tx, state, view, fScriptChecks, flags,
                             fCacheResults, fCacheResults,
                             PrecomputedTransactionData(tx), &vChecks)) {
                return error("ConnectBlock(): CheckInputs on %s failed with %s",
                             tx.GetId().ToString(), FormatStateMessage(state));
            }

            control.Add(vChecks);   
        }
        ...
    }
    
    ...
}
複製程式碼

ConnectBlock將該區塊連結到當前啟用鏈上,並更新UTXO集合。 在該方法中:使用了全域性物件scriptcheckqueue去構造了一個臨時的管理物件,並通過該管理物件來操作全域性任務佇列,用來新增任務,以及執行任務。當該臨時的管理物件析構時,會呼叫wait()方法,加入任務處理,處理完所有任務後,該物件析構完成。

CScriptCheck(根據每個交易輸入構造的檢查任務)

CScriptCheck原始碼:

class CScriptCheck {
private:
    CScript scriptPubKey;   
    Amount amount;      
    const CTransaction *ptxTo;
    unsigned int nIn;         
    uint32_t nFlags;          
    bool cacheStore;
    ScriptError error;       
    PrecomputedTransactionData txdata;  
public:
    CScriptCheck()
        : amount(0), ptxTo(0), nIn(0), nFlags(0), cacheStore(false),
          error(SCRIPT_ERR_UNKNOWN_ERROR), txdata() {}

    CScriptCheck(const CScript &scriptPubKeyIn, const Amount amountIn,
                 const CTransaction &txToIn, unsigned int nInIn,
                 uint32_t nFlagsIn, bool cacheIn,
                 const PrecomputedTransactionData &txdataIn)
        : scriptPubKey(scriptPubKeyIn), amount(amountIn), ptxTo(&txToIn),
          nIn(nInIn), nFlags(nFlagsIn), cacheStore(cacheIn),
          error(SCRIPT_ERR_UNKNOWN_ERROR), txdata(txdataIn) {}

    bool operator()();   
    
    void swap(CScriptCheck &check) {
        scriptPubKey.swap(check.scriptPubKey);
        std::swap(ptxTo, check.ptxTo);
        std::swap(amount, check.amount);
        std::swap(nIn, check.nIn);
        std::swap(nFlags, check.nFlags);
        std::swap(cacheStore, check.cacheStore);
        std::swap(error, check.error);
        std::swap(txdata, check.txdata);
    }

    ScriptError GetScriptError() const { return error; }
};
複製程式碼

程式碼解釋:

  • CScript scriptPubKey; 鎖定指令碼(即該驗證交易的某個引用輸出對應的鎖定指令碼)
  • Amount amount; 上述鎖定指令碼對應的金額(即花費的UTXO的金額)
  • const CTransaction *ptxTo; 正在花費的交易,即要檢查的交易
  • unsigned int nIn; 要檢查該交易的第幾個輸入;
  • uint32_t nFlags; 檢查標識
  • ScriptError error; 驗證出錯的原因
  • bool operator()(); 此處過載了()運算子,執行指令碼檢查操作;

詳情見下篇文章:《指令碼驗證》


本文由 Copernicus團隊 姚永芯寫作,轉載無需授權。

相關文章