C++標準庫讀書筆記: Concurrency

由於多核的出現,使用多線程能夠顯著提高性能。
C++11之前,C++並沒有對併發提供語言層面的支持,C++標準庫也沒有。C++11之後:

  • 語言層面,定義一個內存模型,保證在兩個不同線程中對兩個對象的操作相互獨立,增加了 thread_local 關鍵字。
  • 標準庫提供了對開啓多線程、同步多線程的支持。

The High-Level Interface: async() and Futures


  • async()
    提供了接口讓一個可調用的對象(如某個函數)在獨立的線程中運行。
  • future<> 類
    允許等待某個線程完成,並訪問其結果。

一個使用 async() 以及 Future 的例子

計算 func1() + func2()
如果是單線程,只能依次運行,並把結果相加。總時間是兩者時間之和。
多核多線程情況下,如果兩者獨立,可以分別運行再相加。總時間是兩者時間的最大值。

#include <future>
#include <iostream>
#include <random> // for default_random_engine, uniform_int_distribution

using namespace std;

int doSomething (char c) {
    // random-number generator(use c as seed to get different sequences)
    std::default_random_engine dre(c);
    
    std::uniform_int_distribution<int> id(10,1000);


    for (int i=0; i<10; ++i) {
        this_thread::sleep_for(chrono::milliseconds(id(dre)));
        cout.put(c).flush();    // output immediately
    }
    return c;
}

int func1() {
    return doSomething('.');
}

int func2() {
    return doSomething('+');
}

int main() {
    cout << "start func1() in background, and func2() in foreground: " << endl;
    future<int> result1(std::async(func1));
    int result2 = func2();

    int result = result1.get() + result2;
    
    cout << "\nresult of func1()+func2(): " << result << endl;
}

注意到在主函數中,我們使用瞭如下步驟:

    // instead of:
    // int result = func1() + func2();

    future<int> result1(std::async(func1));
    int result2 = func2();
    int result = result1.get() + result2;

我們使用 async() 使 func1() 在別的線程運行,並將結果賦值給 future。func2() 繼續在主線程運行,最後綜合結果。
future 對象的作用體現在:

  1. 允許訪問 func1 產生的結果,可能是一個返回值也可能是一個異常。注意 future 是一個模板類,我們指定爲 func1 的返回值類型 int。對於無返回值的類型我們可以聲明一個 std::future<void>
  2. 保證 func1 的執行。async() 僅僅是嘗試開始運行傳入的 functionality,如果並沒有運行,那在需要結果的時候,future 對象強制開始運行。

最後,我們需要使用到異步執行的函數的結果的時候,會使用 get()。如:

    int result = result1.get() + result2;

當我們使用 get() 的時候,可能發生以下三種情況:

  1. 如果 func1() 是使用 async() 在別的線程啓動的,而且已經運行結束,可以立即得到結果。
  2. 如果 func1() 啓動了但是還未結束,則 get() 會阻塞直到拿到結果。
  3. 如果 func1() 還未啓動,則會強制啓動,這時候就表現得像一個同步執行的程序。

可以看出,綜合使用

   std::future<int> result1(std::async(func1));
   result1.get()

使得無論是否允許多線程,程序都能順利完成。
爲達到最佳使用效果,我們需要記住一條準則“call early and return late”,給予異步線程足夠的執行時間。
async() 的參數可以是任何可調用對象:函數,成員函數,函數對象,或者 lambda。注意 lambda 後面不要習慣性的加上小括號。

std::async([]{ ... }) // try to perform ... asynchronously

Using Launch Policies

有時候我們希望子線程立刻開始,而不要等待調度。那麼我們就需要使用 lauch policy 來顯式指定,如果開始失敗,會拋出系統錯誤異常。

    // force func1() to start asynchronously now or throw std::system_error 
    std::future<long> result1 = std::async(std::launch::async, func1);

使用 std::launch::async 我們就不必再使用 get() 了,因爲如果 result1 的生命週期結束了,程序會等待 func1 完成。因此,如果我沒有調用 get(),在退出 result1 的作用域時一樣會等待 func1 結束。然而,出於代碼的可讀性,推薦還是加上 get()。

同理,我們也可以指定子線程在 get() 時再運行。

    auto f1 = std::async(std::launch::deferred, func1);

Waiting and Polling

future 的 get() 方法只能被調用一次,在使用 get() 之後,future 實例就變爲 invalid 的了。而 future 也提供了 wait() 方法,可以被調用多次,並且可以加上時限。其調用形式爲:

std::future<...> f(std::async(func));
f.wait_for(std::chrono::seconds(10));  // wait at most 10 seconds
std::future<...> f(std::async(func));
f.wait_until(std::system_lock::now() + std::chrono::minites(1));  // wait until a specific timepoint has reached

這兩個函數的返回值一樣,有三種:

  • std::future_status::deferred
    func 還沒有開始執行。
  • std::future_status::timeout
    func 開始執行但是還沒完成
  • std::future_status::ready
    func 執行完畢

綜合使用 launch policy 和 wait 方法的例子如下,我們可以讓兩個線程分別進行計算,然後等待一定時間後輸出結果。

#include <cstdio>
#include <future>
#include <iostream>

// defined here, lifetime of accurateComputation() may be longer than
// bestResultInTime()
std::future<double> f_slow;

double accurateComputation() {
  std::cout << "Begin accurate computation..." << std::endl;
  std::this_thread::sleep_for(std::chrono::seconds(5));
  std::cout << "Yield accurate answer..." << std::endl;
  return 3.1415;
}

double quickComputation() {
  std::cout << "Begin quick computation..." << std::endl;
  std::this_thread::sleep_for(std::chrono::seconds(1));
  std::cout << "Yield quick answer..." << std::endl;
  return 3.14;
}

double bestResultInTime(int seconds) {
  auto tp = std::chrono::system_clock::now() + std::chrono::seconds(seconds);
  
  // 立即開始
  f_slow = std::async(std::launch::async, accurateComputation);
  // 這兩句順序不可交換
  double quick_result = quickComputation();
  std::future_status f_status = f_slow.wait_until(tp);

  if (f_status == std::future_status::ready) {
    return f_slow.get();
  } else {
    return quick_result;
  }
}

int main() {
  using namespace std::chrono;

  int timeLimit;
  printf("Input execute time (in seconds):\n");
  std::cin >> timeLimit;
  printf("Execute for %d seconds\n", timeLimit);
  auto start = steady_clock::now();
  std::cout << "Result: " << bestResultInTime(timeLimit) << std::endl;
  std::cout
      << "time elapsed: "
      << duration_cast<duration<double>>(steady_clock::now() - start).count()
      << std::endl;
}

// g++ -o async2_test async2.cpp -std=c++0x -lpthread

在上面程序中尤爲值得注意有兩點。

  1. 我們沒有把 future 放在 bestResultInTime() 中。這是因爲如果 future 是局部變量,退出 bestResultInTime() 時,future 的析構函數會阻塞直到產生結果。

  2. wait 方法會阻塞等待,也就是說如果順序不對,就會變成順序執行,而非並行。

  // 這兩句順序不可交換
  double quick_result = quickComputation();
  std::future_status f_status = f_slow.wait_until(tp);

如果交換,則會先等待直到 timepoint,然後再執行 quickComputation(),變成串行程序。

以上程序輸出結果爲:

Input execute time (in seconds):
3
Execute for 3 seconds
Begin quick computation...
Begin accurate computation...
Yield quick answer...
Result: 3.14
time elapsed: 3.00111
Yield accurate answer...

非常值得注意的是 main 函數結束後並沒有立刻退出,而是等待 func 執行完畢後 future 析構。

給 wait_for() 方法傳入 0,相當於立即獲取 future 的狀態。可以利用這點來得知某個任務 現在 是否開始了,或者是否還在運行。

The Low-Level Interface: Threads and Promises


C++ 標準庫也提供了底層接口來啓動和處理線程,我們可以先定義一個線程對象,以一個可調用對象來初始化,然後等待或者detach。

  void doSomething();
  std::thread t(doSomething);  // start doSomething in the background
  t.join();  // wait for t  to finish (block until doSomething() ends)

如同 async() 一樣,我們可以用任何可調用對象來初始化。但是作爲一個底層的接口,一些在 async() 中的特性是不能使用的。

  • thread 沒有 launch policy。它總是立即開啓新線程運行 func。類似於使用了 std::launch::async。
  • 沒有用於處理結果的接口。我們能獲得的只有 thread ID(利用 get_id() 方法)。
  • 如果出現異常,程序會立即停止。
  • 我們需要聲明我們是需要等待 thread 運行結束(使用join()),或者讓它自己在別的線程運行。(使用detach())
  • 如果 main() 函數結束後,線程還在運行,則所有線程都會強制地結束。(future 會等待結束再析構)

以下例子顯示了 join 和 detach 的區別。
我們新建了一個線程輸出 +,另外5個線程 detach 輸出字母。按任意鍵將輸出 + 的線程 join。程序在等待 + 打印結束後,就會停止,不管 detach 線程是否完成了任務。

#include <exception>
#include <iostream>
#include <random>
#include <thread>

void doSomething(int num, char c) {
  try {
    std::default_random_engine dre(c);
    std::uniform_int_distribution<int> distribution(10, 1000);
    for (int i = 0; i < num; i++) {
      std::this_thread::sleep_for(std::chrono::milliseconds(distribution(dre)));
      std::cout.put(c).flush();
    }
  } catch (const std::exception &e) {
    std::cerr << "THREAD-EXCEPTION (thread " << std::this_thread::get_id()
              << e.what() << std::endl;
  } catch (...) {
    std::cerr << "THREAD-EXCEPTION (thread " << std::this_thread::get_id()
              << ")" << std::endl;
  }
}

int main() {
  try {
    std::thread t1(doSomething, 5, '+');
    std::cout << "- started fg thread " << t1.get_id() << std::endl;

    for (int i = 0; i < 5; i++) {
      std::thread t(doSomething, 10, 'a' + i);
      std::cout << "- detach started bg thread " << t.get_id() << std::endl;
      t.detach();
    }

    std::cin.get();

    std::cout << "- join fg thread " << t1.get_id() << std::endl;
    t1.join();
  } catch (const std::exception &e) {
    std::cerr << "EXCEPTION: " << e.what() << std::endl;
  }
}

使用 detached 線程的時候,一定要注意儘量避免使用非局部變量。也就是說,它所使用的變量都要和它的生命週期相同。因爲我們 detach 之後就丟失了對它的控制權,不能保證它會對其他線程中的數據做什麼更改。儘量使用傳值,而不要傳引用
對於 static 和 global 變量,我們無法阻止 detached 線程使用他們。如果我們已經銷燬了某個 static 變量和 global 變量,detached 線程仍然在訪問,那就會出現 undefined behavior。
在我們上面的程序中,detached 線程訪問了 std::cin, std::cout, std::cerr 這些全局的流,但是這些訪問時安全的,因爲這些流會持續直到程序結束。然而,其他的全局變量不一定能保證。

Promises

現在我們需要考慮一個問題:如何才能在線程之間傳遞參數,以及處理異常。這也是上層的接口,例如 async() 的實現需要考慮的。當然,我們可以簡單地進行處理,需要參數則傳入參數,需要返回值則傳入一個引用。
但是,如果我們需要獲取函數的返回值或者異常,那我們就需要用到 std::promise 了。它就是對應於 future 的底層實現。我們可以利用 set_value() 以及set_exception() 方法來設置 promise 的值。

#include <future>
#include <iostream>
#include <thread>

void doSomething(std::promise<std::string> &p) {
  try {
    std::cout << "read char ('x' for exception): ";
    char c = std::cin.get();
    if (c == 'x') {
      throw std::runtime_error(std::string("char ") + c + " read");
    }
    std::string s = std::string("char ") + c + " processed";
    p.set_value(std::move(s)); // use move to avoid copying
  } catch (...) {
    p.set_exception(std::current_exception());
  }
}

int main() {
  try {
    std::promise<std::string> p;
    std::thread t(doSomething, std::ref(p));
    t.detach();

    std::future<std::string> f(p.get_future());
    std::cout << "result: " << f.get() << std::endl;
  } catch (const std::exception &e) {
    std::cerr << "EXCEPTION: " << e.what() << std::endl;
  } catch (...) {
    std::cerr << "EXCEPTION " << std::endl;
  }
}

以上程序定義了一個 promise,用這個 promise 初始化了一個 future,並在一個 detached 的線程之中爲其賦值(可能返回 string 也可能是個 exception)。賦值過後,future 的狀態會變成 ready。然後調用 get() 獲取。

Synchronizing Threads


使用多線程的時候,往往都伴隨着併發的數據訪問,很少有線程相互獨立的情況。

The only safe way to concurrently access the same data by multiple threads without synchronization is when ALL threads only READ the data.

然而,當多個線程訪問同一個變量,並且至少一個線程會對它作出更改時,就必須同步了。這就叫做 data race。定義爲“不同線程中的兩個衝突的動作,其中至少一個動作是非原子的,兩個動作同時發生”。
編程語言,例如C++,抽象化了不同的硬件和平臺。因此,會有一個標準來指定語句和操作的作用,而不是每個語句具體生成什麼彙編指令。也就是說,這些標準指定了 what,而不是 how。
例如,函數參數的 evaluation 順序就是未指明的。編譯器可以按照任何順序對操作數求值,也可以在多次求值同一個表達式時選擇不同的順序。
因此,編譯器幾乎是一個黑箱,我們得到的只是外表看起來一致的程序。編譯器可能會展開循環,整理表達式,去掉 dead code 等它認爲的“優化”。
C++ 爲了給編譯器和硬件預留了足夠的優化空間,並沒有給出一些你期望的保證。我們可能會遇到如下的問題:

  • Unsynchronized data access
    如果兩個線程並行讀寫同樣的數據,並不保證哪條語句先執行。
    例如,下面的程序在單線程中確保了使用 val 的絕對值:
  if (val >= 0) {
    f(val);
  } else {
    f(-val);
  }

但是在多線程中,就不一定能正常工作,val 的值可能在判斷後改變。

  • Half-written data
    如果一個線程讀取數據,另一個線程修改,讀取的線程可能會在寫入的同時讀取,讀到的既不是新數據,也不是老數據,而是不完整的修改中的數據。
    例如,我們定義如下變量:
long long x = 0;

新開一個線程 t1 更改變量的值:

x = -1;

在其他線程 t2 讀取:

std::cout << x;

那麼我們可能得到:

  • 0(舊值),如果 t1 還沒有賦值
  • -1(新值),如果 t1 完成了賦值
  • 其他值,如果 t2 在 t1 賦值的時候讀取

這裏解釋一下第三類情況,假設已在一個 32 位機器上,存儲需要 2 個單位,假設第一個單位已經被更改,第二個單位還沒有更改,然後 t2 開始讀取,就會出現其他值。
這類情況不止發生在 long long 類型上,即使是基礎類型,C++ 標準也沒有保證讀寫是原子操作。

  • Reordered statements
    表達式和操作有可能會被改變順序,因此單線程運行可能沒問題,但是多線程運行,就會出錯。
    假設我們需要在兩個線程之中共享一個 long,使用一個 bool 標誌數據是否準備好。
// 定義
long data;
bool readyFlag = false;
// 線程 A
data = 42;  // 1
readyFlag = true;  // 2
// 線程 B
while (!readyFlag) {
  ;
}
foo(data);

粗一看似乎沒有什麼問題,整個程序只有在線程 A 給 data 賦值後,readyFlag 纔會變 true。
以上代碼的問題在於,如果編譯器改變了 1,2 的語句順序(這是允許的,因爲編譯器只保證在 一個 線程中的執行是符合預期的),那麼就會出現錯誤。

The Features to Solve the Problems


爲了解決上述問題,我們需要下面的幾個概念。

  • **Atomicity: ** 不被中斷地、獨佔地讀寫變量。其他進程無法讀取到中間態。
  • **Order: ** 確保某些語句的順序不被改變。

C++ 標準庫提供了不同的方案來處理。

  • 可以使用 future 以及 promise 來同時保證 atomicity 以及 order,它保證了先設置 outcome,再處理 outcome,表明了讀寫肯定是不同步的。

  • 使用 mutexlock 來處理臨界區(critical section)。只有得到鎖的線程才能執行代碼。

  • 使用條件變量,使進程等待其他進程控制的斷言變爲 true。

Mutexes and Locks


互斥量是通過提供獨佔的訪問來控制資源的併發訪問的對象。爲了實現獨佔訪問,對應的進程 “鎖住” 互斥量,阻止別的進程訪問直到 “解鎖”。
一般使用 mutex 的時候,可能會這麼使用:

int val
std::mutex valMutex;
// Thread A
valMutex.lock();
if (val >= 0) {
  f(val);
} else {
  f(-val);
}
valMutex.unlock();
// Thread B
valMutex.lock();
++val;
valMutex.unlock();

看起來似乎能夠正常運行,但是如果 f(val) 中出現 exception,那麼 unlock() 就不會執行,資源會被一直鎖住。

lock_guard

爲解決這個問題,C++ 標準庫提供了一個在析構時能夠釋放鎖的類型:std::lock_guard。實現了類似於 Golang 中的 defer mu.unlock() 的功能。上面的例子可以改進爲:

// Thread A
...
{  //新的scope
  std::lock_guard<std::mutex> lg(valMutex);
  if (val >= 0) {
    f(val);
  } else {
    f(-val);
  }
}
// Thread B
{
  std::lock_guard<std::mutex> lg(valMutex);
  ++val;
}

需要注意新開了一個作用域。確保 lg 能在合適的地方析構。
再看一個完整的例子。

#include <future>
#include <mutex>
#include <iostream>
#include <string>

std::mutex printMutex;

void print(const std::string& str) {
  std::lock_guard<std::mutex> lg(printMutex);  // lg 初始化時自動鎖定
  for (char c : str) {
    std::cout.put(c);
  }
  std::cout << std::endl;
}  // lg析構時自動解鎖

int main() {
  auto f1 = std::async(std::launch::async, print, "Hello from first thread");
  auto f2 = std::async(std::launch::async, print, "Hello from second thread");
  print("Hello from main thread");
}

如果不加鎖會亂序打印。

unique_lock

有時候,我們並不希望在鎖初始化的同時就上鎖。C++ 還提供了 unique_lock<> 類,它與 lock_guard<> 接口一致,但是它允許程序顯式地決定什麼時候、怎樣上鎖和解鎖。它還提供了 owns_lock() 方法來查詢是否上鎖。使用更佳靈活,最常用的場景就是配合條件變量使用。具體例子在後面介紹 condition_variable 的部分。

處理多個鎖

這個的處理多個鎖並不是說挨個上鎖,而是假設一個線程執行同時需要用到多個資源,應該要麼一起鎖上,要麼全都不鎖。否則很容易出現死鎖。
例如線程 A 和 B 都需要鎖 m1 和 m2,而線程 A 獲得了 m1,在請求 m2,而線程 B 獲得了 m2,在請求 m1,這時候就會相互等待,發生死鎖。
C++ 標準庫提供了 lock() 函數以解決上述問題。它的功能簡單來講就是要麼都鎖,要麼都不鎖。
以下實現了一個簡單的銀行轉帳的例程。

#include <mutex>
#include <thread>
#include <iostream>

struct Bank_account {
    explicit Bank_account(int Balance) : balance(Balance) {}
    int balance;
    std::mutex mtx;
};

void transfer(Bank_account& from, Bank_account& to, int amount) {
/*
    // std::adopt_lock 假設已經上過鎖,在初始化時不會再加鎖,但是保留了析構釋放鎖的功能
    std::lock(from.mtx, to.mtx);
    std::lock_guard<std::mutex> lg1(from.mtx, std::adopt_lock);
    std::lock_guard<std::mutex> lg2(to.mtx, std::adopt_lock);
*/

// equivalent approach:
    std::unique_lock<std::mutex> ulock1(from.mtx, std::defer_lock);
    std::unique_lock<std::mutex> ulock2(to.mtx, std::defer_lock);
    std::lock(ulock1, ulock2);  // 這裏鎖的是封裝過後的 mutex

    from.balance -= amount;
    to.balance += amount;
}

int main() {
    Bank_account a(100);
    Bank_account b(30);

    // 注意使用 std::ref
    std::thread t1(transfer, std::ref(a), std::ref(b), 20);
    std::thread t2(transfer, std::ref(b), std::ref(a), 10);


    t1.join();
    t2.join();
    std::cout << " a now has " << a.balance << ", b now has " << b.balance << std::endl;
}
    

以上例程有三個重點。已經在程序中中文標註。
重點講一下 std::ref,這裏如果不加 std::ref 會報錯。說一下個人理解。
雖然這裏已經在函數中說明了是引用:

void transfer(Bank_account& from, Bank_account& to, int amount);

但是我們實際是傳參數給了 std::thread 類的構造函數。它是一個模板類,默認肯定是進行值傳遞的。因此我們有必要在這裏聲明是引用傳遞,這樣可以將模板改爲引用。例如

template <typename T>
void foo (T val);

如果我們使用

int x;
foo (std::ref(x));

則模板類自動以 int& 爲參數類型。
具體可以參見 std::reference_wrapper<> 類。

Condition Variables


有時,不同線程運行的任務可能需要互相等待。因此,除了訪問同一個數據,我們還有其他使用同步的場景,即邏輯上的依賴關係。
你可能會認爲我們已經介紹過了這種機制:Futures 允許我們阻塞直到另一個線程執行結束。但是,Future 實際上是設計來處理返回值的,在這個場景下使用並不方便。
這裏我們將介紹條件變量,它可以用於同步線程間的邏輯依賴。

在引入條件變量之前,爲了實現這個功能,只能採用輪詢的方法,設置一個時間間隔,不斷去檢查。例如:

bool readyFlag;
std::mutex readyFlagMutex; // wait until readyFlag is true:
{
  std::unique_lock<std::mutex> ul(readyFlagMutex);
  while (!readyFlag) {
    ul.unlock();
    std::this_thread::yield(); // hint to reschedule to the next thread
    std::this_thread::sleep_for(std::chrono::milliseconds(100));
    ul.lock();
  }
} // release lock

這顯然不是一個好的方案,因爲時間間隔設置太短和太長都不行。但是它表現了條件變量的基本思想。就是在未滿足條件時,放棄對鎖和 cpu 資源的佔有,讓別的線程運行,等待條件滿足跳出 while。但是條件變量沒有采取輪詢而是採用一個信號通知。
一個典型應用是消費者-生產者模型。

#include <condition_variable>
#include <mutex>
#include <queue>
#include <thread>
#include <iostream>
#include <future> // for async

std::queue<int> q;
std::mutex mu;
std::condition_variable condVar;

void provider(int val) {
    for (int i=0; i<6; i++) {
        {
            std::lock_guard<std::mutex> lg(mu);
            q.push(val + i);
        }
        condVar.notify_one();
        std::this_thread::sleep_for(std::chrono::milliseconds(val));
    }
}

void consumer(int id) {
    while (true) {
        int val;
        {
            std::unique_lock<std::mutex> ul(mu);
            condVar.wait(ul, [](){return !q.empty();});
            val = q.front();
            q.pop();
        }
        std::cout << "consumer " << id << ": " << val << std::endl;
    }
}

int main() {
    // 3 個生產者
    auto p1 = std::async(std::launch::async, provider, 100);
    auto p2 = std::async(std::launch::async, provider, 300);
    auto p3 = std::async(std::launch::async, provider, 500);
    // 2 個消費者
    auto c1 = std::async(std::launch::async, consumer, 1);
    auto c2 = std::async(std::launch::async, consumer, 2);
}

Atomic

這是實現 lock-free 的重要類型。非常值得深入瞭解。
atomic 的效率比鎖要快很多,在 linux 下大概快 6 倍。
具體應用可以看我的 github 項目:使用mmap實現文件極速無鎖並行寫入
書中有一處錯誤:

經過實驗,實際上返回的並不是 new value 而是 previous value。具體可以參考cppreference

而且還能用於實現 spinlock

附錄


補充:C++11 中的隨機數生成方法

關於爲什麼要引入新的隨機數生成方法,參考這裏
標準流程如下所示。

std::random_device rd;  // 隨機數種子 generator
std::default_random_engine e(rd());  // 原始隨機數 generator
std::uniform_int_distribution<> u(5,20);  // 在 [5,20] 上的均勻分佈

for ( size_t i = 0 ; i < 10 ; i ++ ) {
     cout << u ( e ) << endl ;  // 迫使原始隨機數服從規定的分佈
}

一個簡單的例子,觀察其轉化關係。

#include <iostream>
#include <random> // for default_random_engine, uniform_int_distribution

using namespace std;

void randomPrint () {
    // random-number generator(use c as seed to get different sequences)
    std::random_device rd;
    std::default_random_engine dre(rd());
    
    std::uniform_int_distribution<int> id(10,100);
    for (int i=0; i<10; i++) {
        // random test
        cout << dre() << " => " <<id(dre) << endl;
    }
}

int main() {
    randomPrint();
}

輸出爲:

1337774351 => 49
354763686 => 93
1223972804 => 56
827960471 => 33
54361696 => 94
540202105 => 51
90156615 => 84
1553865488 => 64
780656749 => 21
134206787 => 74

更加詳細的可參考這裏

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