实验目标:【在用户级线程包中实现线程之间的切换、使用多个线程来加速程序、实现屏障】
需要注意的代码文件:
spinlock定义了一个互斥锁
user/uthread.c 和 user/uthread_switch.S Makefile 中有一条规则用于编译 uthread 程序 uthread.c 包含用户级线程包的大部分内容,以及三个简单测试线程的代码
Uthread: switching bwtween threads(moderate)
- 给自定义的线程添加上下文信息用于保存状态
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;
};
struct thread {
char stack[STACK_SIZE]; /* the thread's stack */
int state; /* FREE, RUNNING, RUNNABLE */
struct context contx; /* 线程上下文 */
};
- 在
thread_schedule()里添加交换上下文代码(参照内核里的schedule())
/* YOUR CODE HERE
* Invoke thread_switch to switch from t to next_thread:
* thread_switch(??, ??);--定义在开头
*/
thread_switch((uint64)&t->contx, (uint64)¤t_thread->contx);
- 在
user/uthread_switch.S中的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 */
- 补齐
thread_create()
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->contx.ra = (uint64)func; // 设置返回地址为线程函数的地址,这样第一次调度到该线程,执行到线程切换的ret后就可以跳转到线程函数开始执行
// 指向栈的最高地址(栈是从高地址往低地址增长的)
// 设置sp使得线程拥有自己独有的栈(独立执行流)
t->contx.sp = (uint64)&t->stack + (STACK_SIZE - 1);
}
结果:
Using threads(moderate)
竞争导致有两个线程的时候put会失败
- 补充关于unix下的锁:
// public
pthread_mutex_t lock;
// 在main里面调用初始化
pthread_mutex_init(&lock, NULL);
所以在put()和get()里面加锁
static void put(int key, int value)
{
int i = key % NBUCKET;
pthread_mutex_lock(&lock);
...
pthread_mutex_unlock(&lock);
}
static struct entry* get(int key)
{
int i = key % NBUCKET;
pthread_mutex_lock(&lock);
...
pthread_mutex_unlock(&lock);
return e;
}
现在是只要保证safe就行,所以是对整个代码进行加锁(粒度较大,涉及的代码行多)
测试:
原来竞争是因为有两个线程同时修改一个bucket导致覆盖,但是在哈希表中,不同的bucket是互不影响的,所以实际上确保两个线程不会同时操作同一个bucket即可。 先前的加锁是对整个哈希表加锁,现在是对一个bucket一个锁,粒度降低,运行时间会变快。
// public
pthread_mutex_t lock[NBUCKET];
// main()
for(int i = 0; i < NBUCKET; i++){
pthread_mutex_init(&lock[i], NULL);
}
// put
static void put(int key, int value)
{
int i = key % NBUCKET;
pthread_mutex_lock(&lock[i]);
...
pthread_mutex_unlock(&lock[i]);
}
// get
static struct entry* get(int key)
{
int i = key % NBUCKET;
pthread_mutex_lock(&lock[i]);
...
pthread_mutex_unlock(&lock[i]);
return e;
}
测试:
比之前来说以及有提升了性能,每秒put和get的都要多,且没有missing。 这个示例虽然比较简单,但是很好地领悟到了锁的粒度所带来的性能差别
结果:
barrier(moderate)
每个线程阻塞在barrier()中,直到它们的 所有n个线程都调用了barrier()。
/*barrier()*/
// 只有当最后一个线程到达时,线程才会将state设为“通过”
// 屏障线程数量+1、判断是否已达到总线程数、进入睡眠这三步必须原子,防止lost wake-up
pthread_mutex_lock(&bstate.barrier_mutex);
if(++bstate.nthread < nthread){
pthread_cond_wait(&bstate.barrier_cond, &bstate.barrier_mutex);
/*pthread_cond_wait 会在进入睡眠的时候原子性的释放 barrier_mutex,从而允许后续线程进入 barrier,防止死锁。*/
}
else{
bstate.nthread = 0;
bstate.round++;
pthread_cond_broadcast(&bstate.barrier_cond);
}
pthread_mutex_unlock(&bstate.barrier_mutex);
这里是看了别人的过程之后,发现有一个注意的点就是原子操作
结果: