Mit6.s081-2020-lab7-Multithreading

111 阅读4分钟

实验目标:【在用户级线程包中实现线程之间的切换、使用多个线程来加速程序、实现屏障】

需要注意的代码文件:

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)&current_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);
}

结果:

Pasted image 20230719160347.png

Using threads(moderate)

竞争导致有两个线程的时候put会失败

  • 补充关于unix下的锁:

Pasted image 20230719164309.png

// 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就行,所以是对整个代码进行加锁(粒度较大,涉及的代码行多)
测试:

Pasted image 20230719164607.png

原来竞争是因为有两个线程同时修改一个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;
}

测试:

Pasted image 20230719165732.png

比之前来说以及有提升了性能,每秒put和get的都要多,且没有missing。 这个示例虽然比较简单,但是很好地领悟到了锁的粒度所带来的性能差别

结果:

Pasted image 20230719172707.png

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);

这里是看了别人的过程之后,发现有一个注意的点就是原子操作

结果:

Pasted image 20230719172934.png

Pasted image 20230719172955.png