MIT6.1810 Lab6

Lab: Multithreading

实验链接

实验准备

切换到thread 分支

ruby 复制代码
  $ git fetch
  $ git checkout thread
  $ make clean

Uthread: switching between threads

需求

在本练习中,您将为用户级线程系统 设计上下文切换机制,然后实现它。首先,您的xv6有两个文件user/uthread.c和user/uthread_switch.s。uthread.c包含了大部分用户级线程package,以及三个简单测试线程的代码。线程package缺少一些创建线程和在线程之间切换的代码。

您的工作是提出一个计划来创建线程和保存/恢复寄存器以在线程之间切换,并实现该计划。当你完成后,make grade的结果应该表明你的解决方案通过了uthread测试。

The solution

注:用户及线程系统是在单核上运行

首先在user/uthread.c的thread结构体中添加切换线程时需要保存的寄存器:

c 复制代码
struct thread {
  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;
  
  char       stack[STACK_SIZE]; /* the thread's stack */
  int        state;             /* FREE, RUNNING, RUNNABLE */
};

然后借鉴kernel/swtch.S中在xv6进程调度时用来保存寄存器状态的汇编代码,将其copy到user/uthread_switch.s中:

risc—v 复制代码
	.globl thread_switch
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

然后在user/uthread.c的thread_create函数中添加线程初始化所需的代码: 注:xv6中栈是从高地址向低地址增长,故要将stack的高地址赋给线程的sp寄存器

c 复制代码
void 
thread_create(void (*func)())
{
  ...
  // YOUR CODE HERE
  t->ra = (uint64)func;//令返回地址寄存器保存测试函数入口地址,从thread_switch返回时可切换到对应线程
  t->sp = (uint64) (t->stack + STACK_SIZE);//为线程准备的栈
}

最后在user/uthread.c的thread_schedule函数中添加线程切换调用即可:

c 复制代码
void 
thread_schedule(void)
{
  ...
  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, (uint64)next_thread);
  } else
    next_thread = 0;
}

Using threads

需求

在本作业中,您将探索使用哈希表进行线程和锁并行编程。您应该在具有多个内核的真正的Linux或MacOS计算机(不是xv6,也不是qemu)上执行此任务。大多数最新的笔记本电脑都有多核处理器。

这个任务使用UNIX pthread线程库。您可以通过man pthreads从手册页找到有关它的信息,也可以在web上查找,例如pthread_mutex_t互斥锁

文件notxv6/ph.c包含一个简单的哈希表,如果从单个线程使用它是正确的,但如果从多个线程使用它就不正确了。需要通过加锁来保证其在多线程下的正确性。

The solution

Hint:可以为每个哈希桶加一个锁,减少线程间冲突的情况以提高运行速率。

首先为每个哈希桶配上互斥锁

c 复制代码
struct{
  pthread_mutex_t lock;//互斥锁
  struct entry *tab;
}table[NBUCKET];

然后在main函数中添加初始化互斥锁和释放互斥锁的代码:

c 复制代码
int
main(int argc, char *argv[])
{
  for(int i = 0; i < NBUCKET; i++)//初始化互斥锁
    pthread_mutex_init(&table[i].lock,NULL);
  ...
  for(int i = 0; i < NBUCKET; i++)//销毁互斥锁
    pthread_mutex_destroy(&table[i].lock);
}

最后就是去找需要加锁的地方,发现当多个线程同时执行到insert()函数时会出现丢失更新问题(具体就是在向单链表插入时会多次修改表头指针导致某些key插入失败)。故我们需要在调用insert()函数前上锁。

c 复制代码
static 
void put(int key, int value)
{
  int i = key % NBUCKET;

  // is the key already present?
  struct entry *e = 0;
  for (e = table[i].tab; e != 0; e = e->next) {
    if (e->key == key)
      break;
  }
  if(e){
    // update the existing key.
    e->value = value;
  } else {
    // the new is new.
    pthread_mutex_lock(&table[i].lock);//加🔓
    insert(key, value, &table[i].tab, table[i].tab);
    pthread_mutex_unlock(&table[i].lock);//解🔓
  }
}

Barrier

需求

在这个任务中,你将实现一个屏障:在应用程序中的一个点,所有参与的线程必须等待,直到所有其他参与的线程也到达该点。你将使用pthread条件变量,它们是一种类似于xv6的sleep和wakeup的序列协调技术。您应该在一台真正的计算机(不是xv6,也不是qemu)上完成这个任务。

文件notxv6/barrier.c包含一个损坏的barrier。您需要借助pthread库函数来修复它以保证其通过barrier测试。

The solution

pthread_cond_wait(cond, mutex)的功能有3个:

  • 调用者线程首先释放mutex
  • 然后阻塞,等待被别的线程唤醒
  • 当调用者线程被唤醒后,调用者线程会再次获取mutex

通常的应用场景下,当前线程执行pthread_cond_wait时,处于临界区访问共享资源,存在一个mutex与该临界区相关联

  • 当前线程执行pthread_cond_wait前,已经获得了和临界区相关联的mutex;执行pthread_cond_wait会阻塞,但是在进入阻塞状态前,必须释放已经获得的mutex,让其它线程能够进入临界区
  • 当前线程执行pthread_cond_wait后,阻塞等待的条件满足,条件满足时会被唤醒;被唤醒后,仍然处于临界区,因此被唤醒后必须再次获得和临界区相关联的mutex

直接上代码,思路在注释中:

c 复制代码
static void 
barrier()
{
  pthread_mutex_lock(&bstate.barrier_mutex);//加🔓
  //-------------------临界区
  if(++bstate.nthread == nthread){//判断被阻塞线程数是否等于总线程数
    bstate.nthread = 0;//重置计数
    bstate.round++;//记录轮数,由于bstate.round被共享故只能在最后一个未被阻塞的线程中更新
    pthread_cond_broadcast(&bstate.barrier_cond);//唤醒所有在cond上休眠的线程
  }
  else pthread_cond_wait(&bstate.barrier_cond, &bstate.barrier_mutex);
  //-------------------临界区
  pthread_mutex_unlock(&bstate.barrier_mutex);//解🔓
}
相关推荐
WZF-Sang1 天前
Linux—进程学习-01
linux·服务器·数据库·学习·操作系统·vim·进程
Goboy2 天前
0帧起步:3分钟打造个人博客,让技术成长与职业发展齐头并进
程序员·开源·操作系统
结衣结衣.2 天前
【Linux】Linux管道揭秘:匿名管道如何连接进程世界
linux·运维·c语言·数据库·操作系统
OpenAnolis小助手2 天前
龙蜥副理事长张东:加速推进 AI+OS 深度融合,打造最 AI 的服务器操作系统
ai·开源·操作系统·龙蜥社区·服务器操作系统·anolis os
小蜗的房子3 天前
SQL Server 2022安装要求(硬件、软件、操作系统等)
运维·windows·sql·学习·microsoft·sqlserver·操作系统
邂逅岁月5 天前
【多线程奇妙屋】 Java 的 Thread类必会小技巧,教你如何用多种方式快速创建线程,学并发编程必备(实践篇)
java·开发语言·操作系统·线程·进程·并发编程·javaee
CXDNW6 天前
【系统面试篇】进程和线程类(1)(笔记)——区别、通讯方式、同步、互斥、死锁
笔记·操作系统·线程·进程·互斥·死锁
Anemone_6 天前
MIT 6.S081 Lab3
操作系统
掘了7 天前
持久化内存 | Persistent Memory
c++·架构·操作系统
结衣结衣.8 天前
【Linux】掌握库的艺术:我的动静态库封装之旅
linux·运维·服务器·c语言·操作系统·