Lab5:Thread
【PS:thread_switch 只需要保存被调用者保存寄存器(callee-saved registers),为什么?
caller-saved寄存器由调用的C代码保存在堆栈上(如果需要) 。
Swtch知道struct context中每个寄存器字段的偏移量。它不保存pc。相反,swtch保存了ra寄存器[1],它保存了swtch应该返回的地址。现在,swtch从新的上下文中恢复寄存器,新的上下文中保存着前一次swtch所保存的寄存器值。当swtch返回时,它返回到被恢复的ra寄存器所指向的指令,也就是新线程之前调用swtch**的指令。此外,它还会返回新线程的堆栈。
这个是因为协程切换的过程本质是一个函数调用,因此 caller-save registers 是被调用者(如 thread_a() )保存好的。】
本 lab 的任务是理解并实现多线程。
线程是运行在进程上下文中的逻辑流,每个线程都有自己的上下文:唯一的整数线程ID、栈、栈指针、程序计数器、通用目的寄存器和条件码。所有运行在一个进程里的线程共享该线程的整个虚拟地址空间。
阅读指路:
user/uthread.c:包含大部分用户级线程包,以及三个简单测试线程的函数。
user/uthread_switch.S:线程切换函数的汇编代码。
Uthread: switching between threads
- 目标:为用户系统设计并实现一个方案,创建线程,在线程之间进行切换时保存/恢复寄存器。
- 方法:修改 user/uthread.c 中的 thread_create() 和
thread_schedule(),以及 user/uthread_switch.S 中的 thread_switch
修改 struct thread 以保存寄存器;
在 thread_schedule() 中添加对 thread_switch 的调用,该函数在 uthread_switch.S 中实现;
thread_switch() 保存和还原唤醒线程和被唤醒线程的上下文
主要工作:
首先,在 user/uthread.c 定义线程上下文的数据结构,并在 thread 结构体中加入上下文信息
(此处 struct thread_context 的定义与 kernel/proc.h 里面的进程上下文定义 struct context 是一样的,完全复制)
// [user/uthread.c]
struct thread_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;
};
struct thread {
char stack[STACK_SIZE]; /* the thread's stack */
int state; /* FREE, RUNNING, RUNNABLE */
struct thread_context context;
};
thread_create():
新建线程要设置相关的上下文context
要求thread_schedule()在运行刚才加入的切换代码后,自动开始运行对应函数。
这就需要在thread_create()初始化线程的时候,把它保存的ra寄存器的值赋成对应函数的入口地址,这样在切换结束后运行汇编代码中的ret就自动跳转到ra指向的位置了;另一个需要初始化的是sp寄存器,因为每个线程都各自有一个栈,所以要让自己的sp寄存器指向自己的栈底,由于栈地址从高向低增长,所以sp寄存器赋为栈数组的最高地址。
【ra 寄存器指向线程要运行的函数,switch 结束后,调度到CPU的线程从 ra 处开始运行】
【sp 指向线程自己的栈(要注意:压栈时栈指针减小STACK_SIZE,所以一开始在最高处)】
void
thread_create(void (*func)())
{
struct thread *t;
for (t = all_thread; t < all_thread + MAX_THREAD; t++) {
if (t->state == FREE) break;
}
t->state = RUNNABLE;
// YOUR CODE HERE
t->context.ra = (uint64)func;// 执行完thread_switch()后会返回到这里
t->context.sp = (uint64)(t->stack + STACK_SIZE);// 栈从高地址往低地址增长
}
thread_schedule():
添加对 thread_switch 的调用,用于切换线程上下文
// [user/uthread.c]
void
thread_schedule(void)
{
struct thread *t, *next_thread;
/* Find another runnable thread. */
// 一直循环遍历,直到找到下一个可运行进程
next_thread = 0;
t = current_thread + 1;
for(int i = 0; i < MAX_THREAD; i++){
if(t >= all_thread + MAX_THREAD)
t = all_thread;
if(t->state == RUNNABLE) {
next_thread = t;
break;
}
t = t + 1;
}
if (next_thread == 0) {
printf("thread_schedule: no runnable threads\n");
exit(-1);
}
// 【current_thread为公共变量,是指向当前线程的指针】
if (current_thread != next_thread) { /* switch threads? */
next_thread->state = RUNNING;
t = current_thread;
current_thread = next_thread;
/* YOUR CODE HERE
* Invoke thread_switch to switch from t to next_thread:
* thread_switch(??, ??);
*/
thread_switch((uint64)&t->context, (uint64)¤t_thread->context);
} else
next_thread = 0;
}
user/uthread_switch.S :
【kernel/swtch.S 是进程上下文切换的功能,可以完全复制】
/* uthread_switch.S */
.text
/*
* save the old thread's registers,
* restore the new thread's registers.
*/
.globl thread_switch
thread_switch:
/* YOUR CODE HERE */
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 */
Using threads
目标:利用多线程和锁在哈希表上实现并行编程。
notxv6/ph.c
介绍:
用 UNIX 的 pthread 线程库;
notxv6/ph.c 是对哈希表的操作,单线程时正确,多线程有待完成;
可执行文件./ph后面的参数表示线程数(./ph k 即为k个线程);
ph 运行两个benchmark:首先,调用 put() 将大量 key-value 添加到哈希表中,并打印每秒 put 的次数;然后,调用 get() 从哈希表中获取 key 对应的 value,打印出由于 put 并发而丢失的数字,并打印每秒 get 的次数。
方法:a lock per hash bucket
首先熟悉一下ph.c中定义的数据结构(哈希表)
#define NBUCKET 5
#define NKEYS 100000
struct entry { // element
int key;
int value;
struct entry *next;
};
struct entry *table[NBUCKET]; // 5(NBUCKET) queues of entries
int keys[NKEYS];
int nthread = 1; // 线程数(由命令行参数传入)
为每一个 bucket 定义一个锁:
pthread_mutex_t lock[NBUCKET]; // 【one lock for each bucket.(according to Hints)】
在main()函数中创建线程前给每一个 bucket 的锁初始化 :
for (int i = 0; i < NBUCKET; i++){ //【initialize locks】
pthread_mutex_init(&lock[i], NULL);
}
创建线程先后执行 put_thrread() 和 get_thread(),分别调用了 put() 和 get():
【 put() 函数在 insert 操作前后必须保证互斥性,加锁】
【 get() 函数本身不写入,每个线程各自遍历bucket中的元素,不需要互斥性】
static
void put(int key, int value)
{
int i = key % NBUCKET; // key除以5的余数i决定了插入到table[i]中(每个table中的entry的key对5同余)
// is the key already present?
struct entry *e = 0;
for (e = table[i]; e != 0; e = e->next) { // 遍历应该插入的table[i]
if (e->key == key) // 找到相同的key已经存在, break
break;
}
if(e){ // 存在相同的key(e != 0)
// update the existing key.
e->value = value;
} else { // 不存在相同的key(e==0), 插入新的entry包含传入的参数(key, value)
// the new is new.
pthread_mutex_lock(&lock[i]); //【对第i个bucket的操作上锁】
insert(key, value, &table[i], table[i]);
pthread_mutex_unlock(&lock[i]); //【对第i个bucket的操作解锁】
}
}
Barrier
- 目标:实现多线程的同步(barrier),即在应用程序的一个点,所有线程都必须等待,直到所有其他线程到达此处
notxv6/barrier.c
- 介绍:
每个线程执行一个循环。在每次循环迭代中,每个线程调用 barrier(),然后休眠随机一段时间。目标是每个线程都阻塞在 barrier(),直到所有线程都调用了 barrier()。 - 方法:
用到以下两个pthread条件变量的函数:
pthread_cond_wait(&cond,&mutex):
在cond上睡眠,释放锁mutex,醒来时重新占有锁mutex
pthread_cond_broadcast(&cond):
唤醒所有睡眠在cond上的线程
Hints:
- 处理一系列 barrier() 调用,每次调用称为一轮,bstate.round 记录当前调用的轮数;当所有线程都到达barrier() 时,增加它们的调用轮数,保持一致。
分析:
main函数的主要部分,创建多线程并调用 thread() 函数:
for(i = 0; i < nthread; i++) {
assert(pthread_create(&tha[i], NULL, thread, (void *) i) == 0);
}
for(i = 0; i < nthread; i++) {
assert(pthread_join(tha[i], &value) == 0);
}
thread() 循环调用barrier():
static void *
thread(void *xa)
{
long n = (long) xa; // thread number
long delay;
int i;
for (i = 0; i < 20000; i++) {
int t = bstate.round;
assert (i == t); // 保证当前轮数正确
barrier(); // 调用barrier()
usleep(random() % 100); // 随机休眠一段时间
}
return 0;
}
定义的共享变量:
static int nthread = 1;
static int round = 0;
struct barrier {
pthread_mutex_t barrier_mutex;
pthread_cond_t barrier_cond;
int nthread; // 【Number of threads that have reached this round of the barrier】
int round; // 【Barrier round】
} bstate;
主要工作:
static void
barrier()
{
// YOUR CODE HERE
//
// Block until all threads have called barrier() and
// then increment bstate.round.
//
pthread_mutex_lock(&bstate.barrier_mutex); // 上锁
bstate.nthread++;
if(bstate.nthread == nthread) {
// 所有线程到达这一步 【最后一个到达的线程进入, 负责唤醒所有前面的线程】
bstate.round++;
bstate.nthread = 0;
pthread_cond_broadcast(&bstate.barrier_cond); // 唤醒所有线程
} else { // 需要等待 【非最后到达的线程进入】
// 【必须在上锁状态下调用】: 释放锁, 阻塞当前线程, 等待被唤醒, 返回时重新上锁
pthread_cond_wait(&bstate.barrier_cond, &bstate.barrier_mutex);
// (不过可以看到接下来就会解锁, 所有线程会一个一个退出)
}
pthread_mutex_unlock(&bstate.barrier_mutex); // 解锁
}