• MIT6.S081 Lab Multithreading


    Lab Multithreading

    Uthread: switching between threads

    本题为用户级线程系统设计上下文切换机制,并实现这个机制。uthread.c 含有大多数用户级线程包,以及一些简单的测试线程。需要完善线程包中的创建和切换相关代码。

    提出一个创建线程和保存/恢复寄存器切换线程的方案,实现这个方案。
    完成后,用 make grade 测试。

    需要完善 user/thread.c 中的 thread_create() 和 user/thread_switch.S 中的 thread_schedule()
    一个目标是:确保当 thread_schedule() 首次运行一个给定的线程,线程在自己的栈上执行传给 thread_create() 的函数。
    另一个目标是:确保 thread_switch 保存被切换的线程的寄存器,恢复切换到的线程的寄存器,返回后一个线程的指令。
    必须确定要保存/恢复哪些寄存器;修改 struct thread 保存寄存器是一个好的计划。
    需要在 thread_schedule 中添加一个 thread_switch 调用;可以向 thread_switch 传递任何需要的参数,但目的是从线程 t 切换到线程 next_thread

    线程切换关键点在栈和寄存器的保存和恢复。

    增加 context 结构体声明

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

    线程切换

    thread_switch:
        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 */
    

    线程创建:设置 $ra 为切换后开始运行的指令地址。$sp 为栈地址。

    t->context.ra = (uint64)func;
    t->context.sp = (uint64)(t->stack + STACK_SIZE - 1);
    

    这个实现用户级线程无法利用多核处理器,所有线程理论上只能在一个核心上交替运行。对应关系是多个用户级线程对应一个内核级线程。

    Using threads

    本实验将使用 hash table 探索线程和锁的并行编程。需要在真正的 Linux 或 MacOS 多核计算机(不是 xv6,不是 qemu)上完成这个实验。

    本实验使用 Unix pthread 线程库。可以通过 man pthreads 找到相关手册。或通过下列网站,hereherehere

    文件 notxv6/ph.c 含有一个 hash table,单线程使用正确,但多线程使用不正确。在 xv6 主目录下,键入:

    $ make ph
    $ ./ph 1
    

    ph 程序运行的参数是对 hash table 执行 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() add 大量的 keys 到 hash table,打印出每秒 put 的次数。第二:通过 get() 从 hash table 中取出 keys,打印因 put() 出现在 hash table 中但丢失的 keys 的数量(本例为 0),打印出每秒能达到的 get() 数量。

    用多线程操作 hash table 可以尝试 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
    

    ph 2 表示两个线程并发向 hash table 中添加表项,理论上速率可以达到 ph 1 的两倍,获得良好的并行加速(parallel speedup)。

    但是,两行 16579 keys missing 表明很多 keys 在 hash table 中不存在,put() 应该将这些 keys 加入了 hash table,但是有些地方出错了。需要关注 notxv6/ph.c 的 put()insert()

    为什么两个线程会丢失 keys,但是一个线程不会?确定一种两个线程的执行序列,可以使得 key 丢失。提交在 answers-thread.txt 中。
    answers-thread.txt

    为了避免这种情况,需要在 notxv6/ph.c 中的 put()get() 中添加 lock 和 unlock 语句,使得两个线程丢失的 keys 数量为 \(0\)
    相关的线程函数如下:

    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 测试。

    内存中没有交集的并发读写操作不需要锁相互制约,利用这个特性提高并发加速。
    提示:每个 hash bucket 一个锁。

    put()get() 中的不变量被破坏的地方增加 pthread_mutex_lockpthread_mutex_unlock 以便保护不变量。

    Barrier

    本实验,实现一个 barrier:当一个线程到这个点后,必须等待其余所有线程都到达这点。使用 pthread 条件变量,类似于 xv6 的 sleep/wakeup 的序列协调技术。

    本实验应在真正的计算机上完成(不是 xv6,不是 qemu)

    文件 notxv6/barrier.c 含有一个不完整的 barrier。

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

    \(2\) 表示在 barrier 上同步线程的数量(是 barrier.c 中的 nthread)。每个线程运行一个循环。循环的每次迭代调用 barrier(),然后睡眠一段随即时间。当一个线程在另一个线程到达 barrier 之前就越过了 barrier,则 assert 触发。理想的情况是每个线程都阻塞在 barrier(),直到 nthreads 个线程都调用 barrier()

    本实验应完成理想的 barrier 行为。除了上个实验的锁原语,还需要新的 pthread 原语(herehere)。
    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

    调用 pthread_cond_wait 时释放 mutex,返回之前重新获得 mutex。

    已经给出了 barrier_init(),需要实现 barrier(),使不发生 panic,struct barrier 已定义方便使用。

    有两个问题使得实验复杂化:

    • 必须处理一连串的 barrier 调用,称每次调用为一次 round。bstate.round 记录当前的 round。每次当所有的线程到达 barrier 之后,将 bstate.round 加一。
    • 必须处理一种情况:在其他线程退出屏障之前,一个线程进入循环。特别是,从一个 round 到另一个 round,正在重新使用 bstate.nthread。确保之前的 round 正在使用时,一个线程离开 barrier,再进入循环不会增加 bstate.nthread。(这个问题的目的和 xv6 的 proc->lock 有异曲同工之妙)
    static void
    barrier()
    {
      pthread_mutex_lock(&bstate.barrier_mutex);
      bstate.nthread++;
    
      if (bstate.nthread < nthread) {
        pthread_cond_wait(&bstate.barrier_cond, &bstate.barrier_mutex); // sleep on bstate.barrier_cond,release bstate.barrier_mutex.
      } else {
        bstate.nthread = 0;
        bstate.round++;
        pthread_cond_broadcast(&bstate.barrier_cond); // wake up other threads
      }
    
      pthread_mutex_unlock(&bstate.barrier_mutex);
    }
    

    Code

    Code: Lab thread

  • 相关阅读:
    S5PV210开发板刷机(SD卡uboot、串口+USB-OTG刷机方法)
    S5PV210启动过程分析
    总结:ARM逻辑和高级C(朱老师物联网学习)
    C语言笔记(数组地址一些细节)
    shell脚本和常用命令
    ansible
    firewalld
    LAMP架构上线动态网站WordPress
    LNMP架构上线动态网站
    Tomcat集群 Nginx负载均衡 shell脚本实时监控Nginx
  • 原文地址:https://www.cnblogs.com/seaupnice/p/15913729.html
Copyright © 2020-2023  润新知