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 找到相關手冊。或通過下列網站,here,here,here
檔案 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_lock
和 pthread_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 原語(here,here)。
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);
}