链接如下所示:
1.Uthread: switching between threads
这个task是实现一个简单的线程机制,首先,在汇编里边加上这个内核swtch一样的东西:
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)
这里为啥要保存ra呢,因为这里的ra是要跳转到另外的地址去执行的,保存sp是为了在保存前和之后的sp是相等的。其余寄存器是被调用者保存寄存器,防止在被调函数中要使用。
然后是对数据结构的声明,使得用户级的数据结构保存寄存器信息:
// Saved registers for kernel context switches.
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 context; /*线程上下文*/
};
在创建线程的时候设置接下来要跑的地址ra,然后把栈设置成栈顶位置,这里一定一定注意栈是高地址向低地址增加,所以一开始把栈顶设置成stack[STACK_SIZE-1]的地址。如果设置错误,可能在测试的时候会跑出神奇的结果(可能没跑完或者直接error)。
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;
// 找到空闲位置 设置接下来要跑的线程栈以及运行起始位置 然后切换线程
// t->context.sp=(uint64)t->stack+STACK_SIZE-1;
t->context.sp=(uint64)(&t->stack[STACK_SIZE-1]);
t->context.ra=(uint64)func;
}
在schedule里面切换即可:
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)&next_thread->context);
} else
next_thread = 0;
2.Using threads
第二个task相对简单,就是简单地运用锁。
具体来说,我们给每个哈希桶的链表设置一个锁,每次在put的时候先获取锁,修改或者插入完毕之后再释放锁,当然要记得在main里边初始化锁,具体代码如下所示:
pthread_mutex_t lk[NBUCKET];
static
void put(int key, int value)
{
int i = key % NBUCKET;
pthread_mutex_lock(&lk[i]);
// is the key already present?
struct entry *e = 0;
for (e = table[i]; e != 0; e = e->next) {
if (e->key == key)
break;
}
if(e){
// update the existing key.
e->value = value;
} else {
// the new is new.
insert(key, value, &table[i], table[i]);
}
pthread_mutex_unlock(&lk[i]);
}
main里边初始化锁:
for (int i = 0; i < NKEYS; i++) {
keys[i] = random();
}
for (int i = 0; i < NBUCKET; i++)
{
pthread_mutex_init(&lk[i],NULL);
}
3.Barrier
这里先要获取锁,然后再用条件变量的wait,这里设置的条件当然就是到达当前位置的线程的数量,如果数量到达了总的线程数,则唤醒所有线程,同时将轮数round+1,清空等待线程数为0。
先给个错误的代码:
static void
barrier()
{
// YOUR CODE HERE
//
// Block until all threads have called barrier() and
// then increment bstate.round.
//
// printf("1\n");
pthread_mutex_lock(&bstate.barrier_mutex);
bstate.nthread++;
printf("2\n");
printf("nthread: %d\n",bstate.nthread);
while (bstate.nthread!=nthread)
{
pthread_cond_wait(&bstate.barrier_cond, &bstate.barrier_mutex);
printf("loop\n");
}
if(bstate.nthread==nthread)
{
bstate.round++;
pthread_cond_broadcast(&bstate.barrier_cond);
}
bstate.nthread--;
printf("decrease nthread: %d\n",bstate.nthread);
// bstate.nthread--;
// printf("3\n");
pthread_mutex_unlock(&bstate.barrier_mutex);
// printf("4\n");
}
这里错的很有个性啊,举个2个线程的例子,第一个到了之后,就直接条件变量等待了,此时i和t是0,第二个线程到了之后,还是0,但是这个时候已经到达了两个线程,上面就会唤醒所有的线程,假设一个线程获取了锁跑完了后边所有的代码直至释放锁,然后现在的bstate.nthread就是1,那么另外一个线程就会等待下一个线程来唤醒,这样一直到最后这个线程由于每次测试的时候在循环里边都满足继续循环的条件,所以他永远跑不完的,一直卡在循环这里。
那么修改的方法如下,上面错在我们用nthread-1个线程都不停地试探条件是否正确,我们应当做的是让第nthread个线程只负责更新round,bstate.nthread,以及唤醒其他的线程,而之前的线程在判断了条件不符合之后就直接等待,而不是循环等待。代码如下所示:
static void
barrier()
{
// YOUR CODE HERE
//
// Block until all threads have called barrier() and
// then increment bstate.round.
//
// printf("1\n");
pthread_mutex_lock(&bstate.barrier_mutex);
// printf("2\n");
bstate.nthread++;
if (bstate.nthread!=nthread)
{
pthread_cond_wait(&bstate.barrier_cond, &bstate.barrier_mutex);
}
else
{
bstate.round++;
bstate.nthread=0;
pthread_cond_broadcast(&bstate.barrier_cond);
}
// bstate.nthread--;
// printf("3\n");
pthread_mutex_unlock(&bstate.barrier_mutex);
// printf("4\n");
}