MIT 6.S081 lab 7:Multithreading

本文档详细介绍了如何实现用户级线程的上下文切换,包括在xv6中创建线程、保存/恢复寄存器以及调度线程。同时,探讨了在多线程环境下哈希表的并发问题及其解决方案,通过添加锁来避免数据丢失。最后,讨论了栅栏同步原语的实现,确保所有线程在继续执行前等待其他线程到达栅栏。
摘要由CSDN通过智能技术生成

1 Uthread: switching between threads

在本次练习中,你将要设计面向用户级线程系统的上下文切换机制,然后实现它。为了帮助你开始实现它,xv6提供了两个文件,user/uthread.c和user/uthread_switch.S,和一个在Makefile文件中提供的build uthread 程序的规则。uthread.c包括绝大多数用户级线程包,和三个简单的线程测试样例。这个线程包中丢失了部分创建线程和转换线程的代码。

你的任务是提出一种创建线程和在线程切换中保存/恢复寄存器的方案,并实现它。当你完成之后,make grade会告诉你你的方案通过了uthread测试。

一旦你完成了,当你在xv6上运行uthread你应当看到以下形式的输出。

$ make qemu
...
$ uthread
thread_a started
thread_b started
thread_c started
thread_c 0
thread_a 0
thread_b 0
thread_c 1
thread_a 1
thread_b 1
...
thread_c 99
thread_a 99
thread_b 99
thread_c: exit after 100
thread_a: exit after 100
thread_b: exit after 100
thread_schedule: no runnable threads
$

这些输出来自于三个测试线程,每个测试线程都会有一个循环来打印行,然后放弃CPU给其他线程。

然而,如果没有上下文切换的代码,什么都不会输出。

你需要给user/uthread.c中的thread_create()thread_schedule()user/uthread_switch.S中的thread_switch部分添加代码。一个目标是确保当thread_schedule()第一次执行某个线程时,这个线程执行被传递到thread_create()的函数,在它自己的栈上面。另外一个目标是确保thread_switch保存被切换出去线程的寄存器,恢复被切换进来的线程寄存器的内容,并且返回后者线程执行的指令的位置。你必须决定哪里去保存/恢复寄存器;修改struct thread来保存寄存器内容是一个好的方案。你需要给在thread_schedule中的thread_switch增加一个调用;你可以传递任何参数给thread_switch,但是目标是将线程从线程t切换到线程next_thread。

一些提示:

  • thread_switch只需要保存或恢复callee-save的寄存器。为什么?

  • 你可以看看user/uthread.asm中的相关汇编代码,这对debug非常有帮助。

  • 为什么测试你的代码,单步测试是非常有帮助的。你可以按以下方式开始:

    (gdb) file user/_uthread
    Reading symbols from user/_uthread...
    (gdb) b uthread.c:60
    

    这在uthread.c的60行设置了断点。在你运行uthread之前,这个断点也可能会被触发。为什么?

    一旦你的xv6 shell运行,输入“uthread”,gdb将会在60行break。现在你可以输入以下命令来查看uthread的状态:

    (gdb) p/x *next_thread
    

    用“x",你可以查看某个地址的内容:

    (gdb) x/x next_thread->stack
    

    你可以跳过thread_switch的开头:

    (gdb) b thread_switch
    (gdb) c
    

    你可以单步调试汇编指令:

    (gdb) si
    

新建一个thread的成员来存放寄存器内容

struct thread {
  char       stack[STACK_SIZE]; /* the thread's stack */
  int        state;             /* FREE, RUNNING, RUNNABLE */
  uint64     regs[REG_SIZE/8];    /* the saved registers */
};

在thread_create中添加代码

注意寄存器ra中存放的就是返回执行指令的地址。

就修改ra和sp的指向即可。

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
  // Clean the regs.
  memset((char*)t->regs,0,REG_SIZE);

  // Modify the sp and pc.
  *(t->regs + 1) = (uint64)(t->stack + STACK_SIZE);
  *(t->regs) = (uint64)func;
  return;
}

在thread_scheduler中添加代码

其实只需要添加上下文切换的函数即可,因为这边状态的修改已经完成。

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

  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->regs,(uint64)current_thread->regs);
  } else
    next_thread = 0;
}

上下文切换

跟swtch完全一样。

	.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 */

这部分都比较简单,可以说是内核切换的简化版,毕竟也不用考虑并发的问题。

2 Using threads

在这部分,你将会探索并行编程,涉及到线程和使用哈希表的锁。你应当在一个真实的多核Linux或者MacOS机上面完成这个部分。大多数笔记本都是多核处理器。

这个任务会用到UNIX的pthread线程库。你可以在参考页找到线管信息,你也可以在网上查看相关信息。

notxv6/ph.c文件包含了简单的哈希表,如果是单线程,这个哈希表就是正确的,但如果是多线程就不是了。在你的xv6目录中键入:

$ make ph
$ ./ph 1

注意,为了build ph,Makefile用了自己OS的gcc,而不是6.S081的工具。ph的参数表示了在哈希表上执行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(给哈希表增加键值,并且打印出每秒的执行速率。get()从哈希表上面取得值。它会打印哈希表上本身应该具有多少值,来作为丢失数量的结果,并且打印每秒的get()速率。

你可以告诉通过给一个大于1的参数,使得ph会进行多进行执行。尝试 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

第一行表示有两个线程并发地增添entry,他们每秒总共实现了53044次put。这大概是ph 1的两倍。

然而下面两行表明15479的键值丢失了,这说明很多本该在哈希表上的键值丢失了。put本应该把这些值加入到哈希表上面,但是出了一些问题。看一下notxv6/ph.c,尤其是put()和insert()

为什么单线程不会丢失,而双线程就丢失了这么多?阐述清除双线程导致这种丢失的事件先后顺序。提交这个顺序到 answers-thread.txt
为什么避免这种事件顺序,需要在put()和get()中添加锁,来实现丢失数为0.相关的pthread调用是:
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显示你哦通过了 ph_safe测试,0 missing。ph_fast测试失败是正常的。

不要忘记调用pthread_mutex_init()。先用单线程测试你的代码,然后用双线程。它对吗?双线程版本的速率是两倍吗?

并发的put()并不会覆盖整个内存,因此不需要保护全部的bucket。你可以改变ph.c来得到两倍的速度吗?提示:一个锁怎么给每个bucket加锁?

修改你的代码,使得可以在某些put操作中实现并行并保证正确。当你make grade,并显示通过ph_safe和ph_fast就表示完成了。ph_fast要求双线程的速度必须是单线程素的1.25倍多。

实现

这个task非常简单~~

其中实现的连接法的哈希表,所以每次对bucket上锁即可。

// 添加锁
pthread_mutex_t lock[NBUCKET];

// 修改put
static 
void put(int key, int value)
{
  int i = key % NBUCKET;

  pthread_mutex_lock(lock + 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(lock + i);
}

// 在main中初始化锁

  // Init the locks.
  for(int i = 0; i < NBUCKET;i++)
      pthread_mutex_init(lock+i,NULL);


3 Barrier

在这个ass中,你将会实现一个栅栏:线程必须等待所有其他线程抵达才能继续。你可以使用pthread的条件变量,这个技术和xv6的sleepwakeup类似。

你应当在一个真实计算机上完成这个ass。

文件 notxv6/barrier.c 中包含了未完成的栅栏。

$ make barrier
$ ./barrier 2
barrier: notxv6/barrier.c:42: thread: Assertion `i == t' failed.

2表示在栅栏中同步的线程数量。每个线程在循环中执行。在每个循环迭代中线程都调用*barrier()*然后sleep若干微秒。警告会在其他线程并未全部抵达栅栏,而某个线程离开栅栏的情况下引发。理想的情况是每个线程都阻塞至其他线程都调用了barrier()。

你的目标是实现一个理想的栅栏行为。除了你之前见过的lock原语,你还需要学习以下线程原语:
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

请确保你的答案通过了make grade中的barrier测试。

pthread_cond_wait释放mutex,并且在返回之前重新申请mutex。

我们已经给你了 barrier_init()。你的工作就去实现 barrier(),使得panic不出现。我们已经定义了 struct barrier ,你可以使用它的域。

这里有两个问题会增加你工作的难度:

  • 你必须处理一次成功的栅栏调用,我们把次数叫做轮数。bstate.round记录了当前的轮数。你应该在所有线程都调用barrier()之后增加bstate.round。
  • 你必须处理这样一种情况:在其他进程退出这个栅栏之前,某个进程在循环中竞争。特别的,你会在不同的轮数中重复使用bstate.nthread变量。确保当先前的一轮还未结束时,一个离开栅栏并且竞争的线程不会增加bstate.nthread。

修改barrier()

这个也非常简单~~

最后一个抵达栅栏的线程修改状态,并唤醒沉睡的线程即可。

static void 
barrier()
{
  // YOUR CODE HERE
  //
  // Block until all threads have called barrier() and
  // then increment bstate.round.
  //
    pthread_mutex_lock(&bstate.barrier_mutex);

    if(++bstate.nthread == nthread){
        bstate.nthread = 0;
        bstate.round++;
        pthread_cond_broadcast(&bstate.barrier_cond);
    }
    else{
        pthread_cond_wait(&bstate.barrier_cond,&bstate.barrier_mutex);
    }
    pthread_mutex_unlock(&bstate.barrier_mutex);
  
}

总体来说这个lab难度比较低。
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值