MIT6.S081 Lab7:Multithreading

实验涉及用户态线程库的线程切换,通过保存和恢复callee-saved寄存器实现。还讨论了多线程中数据丢失问题,解释了全局互斥锁如何避免数据竞争,但降低了并发性能。最后介绍了使用pthread的条件变量实现barrier同步机制,确保所有线程达到特定点后一起继续执行。
摘要由CSDN通过智能技术生成

This lab will familiarize you with multithreading. You will implement switching between threads in a user-level threads package, use multiple threads to speed up a program, and implement a barrier.
实现在用户态线程库切换线程;用多线程来为程序提速;实现一个同步屏障

Uthread: switching between threads [moderate]

需要做的是创建线程并且在线程切换时恢复/保存相应寄存器
这个实验其实相当于在用户态重新实现一遍 xv6 kernel 中的 scheduler() 和 swtch() 的功能,所以大多数代码都是可以借鉴的
提示

  • thread_switch needs to save/restore only the callee-save registers. Why?
  • You can see the assembly code for uthread in user/uthread.asm, which may be handy for debugging.
    实验过程
    1.uthread_switch.S 中需要实现上下文切换的代码,这里借鉴 swtch.S:
    a0,a1寄存器用来保存函数参数,分别保存着下面的context *old 和context *new
	.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 */

注意,在调用本函数uthread_switch()的过程中,caller-saved registers 已经被调用者保存到栈帧中了,所以这里无需保存这一部分寄存器

引申:内核调度器无论是通过时钟中断进入(usertrap),还是线程自己主动放弃 CPU(sleep、exit),最终都会调用到 yield 进一步调用 swtch。
由于上下文切换永远都发生在函数调用的边界(swtch 调用的边界),恢复执行相当于是 swtch 的返回过程,会从堆栈中恢复 caller-saved 的寄存器,
所以用于保存上下文的 context 结构体只需保存 callee-saved 寄存器,以及 返回地址 ra、栈指针 sp 即可。恢复后执行到哪里是通过 ra 寄存器来决定的(swtch 末尾的 ret 转跳到 ra)
而 trapframe 则不同,一个中断可能在任何地方发生,不仅仅是函数调用边界,也有可能在函数执行中途,所以恢复的时候需要靠 pc 寄存器来定位。
并且由于切换位置不一定是函数调用边界,所以几乎所有的寄存器都要保存(无论 caller-saved 还是 callee-saved),才能保证正确的恢复执行。
这也是内核代码中 struct trapframe 中保存的寄存器比 struct context 多得多的原因。
另外一个,无论是程序主动 sleep,还是时钟中断,都是通过 trampoline 跳转到内核态 usertrap(保存 trapframe),然后再到达 swtch 保存上下文的。
恢复上下文都是恢复到 swtch 返回前(依然是内核态),然后返回跳转回 usertrap,再继续运行直到 usertrapret 跳转到 trampoline 读取 trapframe,并返回用户态。
也就是上下文恢复并不是直接恢复到用户态,而是恢复到内核态 swtch 刚执行完的状态。负责恢复用户态执行流的其实是 trampoline 以及 trapframe。

2.从proc.h中借鉴context结构体,用于保存ra,sp以及callee-saved registers

// uthread.c
// Saved registers for thread 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 ctx; // 在 thread 中添加 context 结构体
};
struct thread all_thread[MAX_THREAD];
struct thread *current_thread;

extern void thread_switch(struct context* old, struct context* new); // 修改 thread_switch 函数声明

3.在thread_schedule中调用thread_switch进行上下文切换

//user/uthread.c
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(&t->ctx, &next_thread->ctx);  //switch thread
  } else
    next_thread = 0;
}

4.最后实现thread_create函数

//user/uthread.c
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
  //+
  //thread_switch will return ra at its end
  t->ctx.ra = (uint64)func;    //save the return address to ra
  //stack grow from high addr to low addr,so sp is set to stack's highest addr
  t->ctx.sp = (uint64)&t->stack + (STACK_SIZE - 1);
}

添加的部分为设置上下文中 ra 指向的地址为线程函数的地址,这样在第一次调度到该线程,执行到 thread_switch 中的 ret 之后就可以跳转到线程函数从而开始执行了。设置 sp 使得线程拥有自己独有的栈,也就是独立的执行流。

实验结果

$ 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 98
thread_a 98
thread_b 98
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

Using threads[moderate]

多线程时为什么会造成数据丢失(missing keys)?

假设现在有两个线程T1和T2,两个线程都走到put函数,且假设两个线程中key%NBUCKET相等,即要插入同一个散列桶中。两个线程同时调用insert(key, value, &table[i], table[i]),insert是通过头插法实现的。如果先insert的线程还未返回另一个线程就开始insert,那么前面的数据会被覆盖

背景知识

  • pthread.h线程库
//互斥锁
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

//一些重要函数
//1.创建线程
int pthread_create(pthread_t* thread, const pthread_attr_t* attr, void*(*start_routine)(void*), void* arg);
thread 是一个指向线程标识符的指针,线程调用后,改值被设置为线程ID
attr 用来设置线程属性
start_routine 是线程函数的其实地址,即线程函数体,线程创建成功后,thread 指向的内存单元从该地址开始运行
arg 是传递给线程函数体的参数
返回值:若线程创建成功,则返回0,失败则返回错误码,并且 thread 内容是未定义的。

//2.线程阻塞函数,调用该函数则等到线程结束才继续运行
int pthread_join(pthread_t thread, void **retval);
thread 是线程表示符
retval 用来获取线程的返回值,一般是 pthread_join 方法传递出来的值

实验过程
数据丢失的原因已经知道是由于没有上锁保护数据。如果只用全局的互斥锁,可以发现,多线程执行的版本也不会丢失 key 了,说明加锁成功防止了 race-condition 的出现。
但是仔细观察会发现,加锁后多线程的性能变得比单线程还要低了,虽然不会出现数据丢失,但是失去了多线程并行计算的意义:提升性能。
这里的原因是,我们为整个操作加上了互斥锁,意味着每一时刻只能有一个线程在操作哈希表,这里实际上等同于将哈希表的操作变回单线程了,又由于锁操作(加锁、解锁、锁竞争)是有开销的,所以性能甚至不如单线程版本。
这里的优化思路,是多线程效率的一个常见的优化思路,就是降低锁的粒度。由于哈希表中,不同的 bucket 是互不影响的,一个 bucket 处于修改未完全的状态并不影响 put 和 get 对其他 bucket 的操作,所以实际上只需要确保两个线程不会同时操作同一个 bucket 即可,并不需要确保不会同时操作整个哈希表。
所以可以将加锁的粒度,从整个哈希表一个锁降低到每个 bucket 一个锁

// ph.c
//+
pthread_mutex_t locks[NBUCKET];
int
main(int argc, char *argv[])
{
  pthread_t *tha;
  void *value;
  double t1, t0;
  
  for(int i=0;i<NBUCKET;i++) {
    pthread_mutex_init(&locks[i], NULL); 
  }

  // ......
}

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

  pthread_mutex_lock(&locks[i]);
  
  // ......

  pthread_mutex_unlock(&locks[i]);
}

static struct entry*
get(int key)
{
  int i = key % NBUCKET;

  pthread_mutex_lock(&locks[i]);
  
  // ......

  pthread_mutex_unlock(&locks[i]);

  return e;
}

实验结果

$ ./ph 1
100000 puts, 4.940 seconds, 20241 puts/second
0: 0 keys missing
100000 gets, 4.934 seconds, 20267 gets/second
$ ./ph 2
100000 puts, 3.489 seconds, 28658 puts/second
0: 0 keys missing
1: 0 keys missing
200000 gets, 6.104 seconds, 32766 gets/second
$ ./ph 4
100000 puts, 1.881 seconds, 53169 puts/second
0: 0 keys missing
3: 0 keys missing
2: 0 keys missing
1: 0 keys missing
400000 gets, 7.376 seconds, 54229 gets/second

Barrier[moderate]

In this assignment you’ll implement a barrier: a point in an application at which all participating threads must wait until all other participating threads reach that point too. You’ll use pthread condition variables(条件变量), which are a sequence coordination technique similar to xv6’s sleep and wakeup.
在上个实验的基础上,再增加一些新特性:
1.先到达barrier的线程先睡眠等待其他还没到达的
2.如果全部都到达了,再唤醒线程,同时更新结构体的线程数和轮数
这两步操作分别用到下面的两个函数

int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex); // go to sleep on cond, releasing lock mutex, acquiring upon wake up
int pthread_cond_broadcast(pthread_cond_t *cond);    // wake up every thread sleeping on cond

实验过程
补全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) {
  	pthread_cond_wait(&bstate.barrier_cond, &bstate.barrier_mutex);
  }
  else {
  	bstate.nthread = 0;
  	bstate.round++;
  	pthread_cond_broadcast(&bstate.barrier_cond);
  }
  pthread_mutex_unlock(&bstate.barrier_mutex);
  
}

思考
可能出现的[lost-wake-up]:
线程 1 即将睡眠前,线程 2 调用了唤醒,然后线程 1 才进入睡眠,导致线程 1 本该被唤醒而没被唤醒
解决方法:
1.线程数递增2.递增后的结果与总线程数比较3.睡眠到达的线程。这三步操作必须是原子的
所以使用一个互斥锁 barrier_mutex 来保护这一部分代码。pthread_cond_wait 会在进入睡眠的时候原子性的释放 barrier_mutex,从而允许后续线程进入 barrier,防止死锁
实验结果

$make grade

在这里插入图片描述
多个进程阻塞测试:
在这里插入图片描述

关于pthread库参考文章
有关RISCV caller saved 和callee saved

class MainWindow(QMainWindow): def init(self, user_id): super().init() self.user_id = user_id self.initUI() # 打开串口 self.ser = serial.Serial('COM7', 9600, timeout=1) def initUI(self): # 创建用于显示员工信息的控件 self.info_label = QLabel("员工信息", self) self.info_label.move(100, 50) self.info_label.setStyleSheet("font-size: 24px; color: black; background-color: #eee; border-radius: 10px;") self.id_label = QLabel("员工ID:", self) self.id_label.move(70, 100) self.id_label.setStyleSheet("font-size: 18px; color: black;") self.name_label = QLabel("姓名:", self) self.name_label.move(70, 150) self.name_label.setStyleSheet("font-size: 18px; color: black;") self.six_label = QLabel("性别:", self) self.six_label.move(70, 200) self.six_label.setStyleSheet("font-size: 18px; color: black;") self.sfz_label = QLabel("身份证:", self) self.sfz_label.move(70, 250) self.sfz_label.setStyleSheet("font-size: 18px; color: black;") self.tel_label = QLabel("电话:", self) self.tel_label.move(70, 300) self.tel_label.setStyleSheet("font-size: 18px; color: black;") self.setFixedSize(800, 500) self.setWindowTitle('员工信息') # 查询员工信息 def query_employee(self, id): conn = pymysql.connect(host='39.99.214.172', user='root', password='Solotion.123', database='jj_tset') cursor = conn.cursor() cursor.execute("SELECT * FROM employee_table WHERE user_id='%s'" % id) result = cursor.fetchone() conn.close() return result # 读取数据 def read_data(self): data = self.ser.readline() if data: # 解析数据 id = data.decode().strip() # 查询员工信息 result = self.query_employee(id) if result: # 更新UI界面 self.id_label.setText("员工ID:" + result[0]) self.name_label.setText("姓名:" + str(result[1])) self.six_label.setText("性别:" + result[2]) self.sfz_label.setText("身份证:" + str(result[3])) self.tel_label.setText("电话:" + result[4]) print(result[0],result[1],result[2],result[3],result[4]) else: # 显示空白信息 self.id_label.setText("员工ID:") self.name_label.setText("姓名:") self.six_label.setText("性别:") self.sfz_label.setText("身份证:") self.tel_label.setText("电话:") # 定时读取数据 QTimer.singleShot(100, self.read_data) def closeEvent(self, event): # 关闭串口 self.ser.close()用多线程改写代码,防止主线程阻塞
05-27
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值