1. 程式人生 > 其它 >MIT6.S081 ---- Lab Multithreading

MIT6.S081 ---- Lab Multithreading

Lab Multithreading

Uthread: switching between threads

本題為使用者級執行緒系統設計上下文切換機制,並實現這個機制。uthread.c 含有大多數使用者級執行緒包,以及一些簡單的測試執行緒。需要完善執行緒包中的建立和切換相關程式碼。

提出一個建立執行緒和儲存/恢復暫存器切換執行緒的方案,實現這個方案。
完成後,用 make grade 測試。

需要完善 user/thread.c 中的 thread_create() 和 user/thread_switch.S 中的 thread_schedule()
一個目標是:確保當 thread_schedule()

首次執行一個給定的執行緒,執行緒在自己的棧上執行傳給 thread_create() 的函式。
另一個目標是:確保 thread_switch 儲存被切換的執行緒的暫存器,恢復切換到的執行緒的暫存器,返回後一個執行緒的指令。
必須確定要儲存/恢復哪些暫存器;修改 struct thread 儲存暫存器是一個好的計劃。
需要在 thread_schedule 中新增一個 thread_switch 呼叫;可以向 thread_switch 傳遞任何需要的引數,但目的是從執行緒 t 切換到執行緒 next_thread

執行緒切換關鍵點在棧和暫存器的儲存和恢復。

增加 context 結構體宣告

struct context {
  uint64 ra;
  uint64 sp;

  // callee-saved
  uint64 s0;
  uint64 s1;
  uint64 s2;
  uint64 s3;
  uint64 s4;
  uint64 s5;
  uint64 s6;
  uint64 s7;
  uint64 s8;
  uint64 s9;
  uint64 s10;
  uint64 s11;
};

執行緒切換

thread_switch:
    sd ra, 0(a0)
    sd sp, 8(a0)
    sd s0, 16(a0)
    sd s1, 24(a0)
    sd s2, 32(a0)
    sd s3, 40(a0)
    sd s4, 48(a0)
    sd s5, 56(a0)
    sd s6, 64(a0)
    sd s7, 72(a0)
    sd s8, 80(a0)
    sd s9, 88(a0)
    sd s10, 96(a0)
    sd s11, 104(a0)

    ld ra, 0(a1)
    ld sp, 8(a1)
    ld s0, 16(a1)
    ld s1, 24(a1)
    ld s2, 32(a1)
    ld s3, 40(a1)
    ld s4, 48(a1)
    ld s5, 56(a1)
    ld s6, 64(a1)
    ld s7, 72(a1)
    ld s8, 80(a1)
    ld s9, 88(a1)
    ld s10, 96(a1)
    ld s11, 104(a1)
    ret    /* return to ra */

執行緒建立:設定 $ra 為切換後開始執行的指令地址。$sp 為棧地址。

t->context.ra = (uint64)func;
t->context.sp = (uint64)(t->stack + STACK_SIZE - 1);

這個實現使用者級執行緒無法利用多核處理器,所有執行緒理論上只能在一個核心上交替執行。對應關係是讀個使用者級執行緒對應一個核心級執行緒。

Using threads

本實驗將使用 hash table 探索執行緒和鎖的並行程式設計。需要在真正的 Linux 或 MacOS 多核計算機(不是 xv6,不是 qemu)上完成這個實驗。

本實驗使用 Unix pthread 執行緒庫。可以通過 man pthreads 找到相關手冊。或通過下列網站,hereherehere

檔案 notxv6/ph.c 含有一個 hash table,單執行緒使用正確,但多執行緒使用不正確。在 xv6 主目錄下,鍵入:

$ make ph
$ ./ph 1

ph 程式執行的引數是對 hash table 執行 put 和 get 操作的執行緒的數量。ph 1 執行的結果與下列類似:

100000 puts, 3.991 seconds, 25056 puts/second
0: 0 keys missing
100000 gets, 3.981 seconds, 25118 gets/second

本地執行結果與樣例結果相差兩倍甚至更多,這取決於計算機執行速度,計算機核心數量,其他任務是否繁忙。

ph 執行兩個基準。第一:通過呼叫 put() add 大量的 keys 到 hash table,打印出每秒 put 的次數。第二:通過 get() 從 hash table 中取出 keys,列印因 put() 出現在 hash table 中但丟失的 keys 的數量(本例為 0),打印出每秒能達到的 get() 數量。

用多執行緒操作 hash table 可以嘗試 ph 2

$ ./ph 2
100000 puts, 1.885 seconds, 53044 puts/second
1: 16579 keys missing
0: 16579 keys missing
200000 gets, 4.322 seconds, 46274 gets/second

ph 2 表示兩個執行緒併發向 hash table 中新增表項,理論上速率可以達到 ph 1 的兩倍,獲得良好的並行加速(parallel speedup)。

但是,兩行 16579 keys missing 表明很多 keys 在 hash table 中不存在,put() 應該將這些 keys 加入了 hash table,但是有些地方出錯了。需要關注 notxv6/ph.c 的 put()insert()

為什麼兩個執行緒會丟失 keys,但是一個執行緒不會?確定一種兩個執行緒的執行序列,可以使得 key 丟失。提交在 answers-thread.txt 中。
(Answer)[https://gitee.com/seaupnice/xv6-labs-2021/blob/c308aad14ef9d272fce6f4c7f2016afdb425615d/answers-thread.txt]

為了避免這種情況,需要在 notxv6/ph.c 中的 put()get() 中新增 lock 和 unlock 語句,使得兩個執行緒丟失的 keys 數量為 \(0\)
相關的執行緒函式如下:

pthread_mutex_t lock;            // declare a lock
pthread_mutex_init(&lock, NULL); // initialize the lock
pthread_mutex_lock(&lock);       // acquire lock
pthread_mutex_unlock(&lock);     // release lock

完成後用 make grade 測試。

記憶體中沒有交集的併發讀寫操作不需要鎖相互制約,利用這個特性提高併發加速。
提示:每個 hash bucket 一個鎖。

put()get() 中的不變數被破壞的地方增加 pthread_mutex_lockpthread_mutex_unlock 以便保護不變數。

Barrier

本實驗,實現一個 barrier:當一個執行緒到這個點後,必須等待其餘所有執行緒都到達這點。使用 pthread 條件變數,類似於 xv6 的 sleep/wakeup 的序列協調技術。

本實驗應在真正的計算機上完成(不是 xv6,不是 qemu)

檔案 notxv6/barrier.c 含有一個不完整的 barrier。

$ make barrier
$ ./barrier 2
barrier: notxv6/barrier.c:42: thread: Assertion `i == t' failed.

\(2\) 表示在 barrier 上同步執行緒的數量(是 barrier.c 中的 nthread)。每個執行緒執行一個迴圈。迴圈的每次迭代呼叫 barrier(),然後睡眠一段隨即時間。當一個執行緒在另一個執行緒到達 barrier 之前就越過了 barrier,則 assert 觸發。理想的情況是每個執行緒都阻塞在 barrier(),直到 nthreads 個執行緒都呼叫 barrier()

本實驗應完成理想的 barrier 行為。除了上個實驗的鎖原語,還需要新的 pthread 原語(herehere)。
pthread_cond_wait(&cond, &mutex); // go to sleep on cond, releasing lock mutex, acquiring upon wake up
pthread_cond_broadcast(&cond); // wake up every thread sleeping on cond

呼叫 pthread_cond_wait 時釋放 mutex,返回之前重新獲得 mutex。

已經給出了 barrier_init(),需要實現 barrier(),使不發生 panic,struct barrier 已定義方便使用。

有兩個問題使得實驗複雜化:

  • 必須處理一連串的 barrier 呼叫,稱每次呼叫為一次 round。bstate.round 記錄當前的 round。每次當所有的執行緒到達 barrier 之後,將 bstate.round 加一。
  • 必須處理一種情況:在其他執行緒退出屏障之前,一個執行緒進入迴圈。特別是,從一個 round 到另一個 round,正在重新使用 bstate.nthread。確保之前的 round 正在使用時,一個執行緒離開 barrier,再進入迴圈不會增加 bstate.nthread。(這個問題的目的和 xv6 的 proc->lock 有異曲同工之妙)
static void
barrier()
{
  pthread_mutex_lock(&bstate.barrier_mutex);
  bstate.nthread++;

  if (bstate.nthread < nthread) {
    pthread_cond_wait(&bstate.barrier_cond, &bstate.barrier_mutex); // sleep on bstate.barrier_cond,release bstate.barrier_mutex.
  } else {
    bstate.nthread = 0;
    bstate.round++;
    pthread_cond_broadcast(&bstate.barrier_cond); // wake up other threads
  }

  pthread_mutex_unlock(&bstate.barrier_mutex);
}

Code

Code: Lab thread