MIT 6.S081 Lab7: Multithreading

写在前面

这个实验是多线程安全和同步的内容,实验地址在这里。说实话,有点简单,用了一天时间就写完了,感觉最近的实验,从实验三之后难度就降低了很多。但是实验的内容还是很有用的,主要学习了一遍线程的切换过程,感觉受益很多,建议看一下课程里对应的多线程部分。我的实验代码在 github 上,感觉这个实验没太多代码和步骤需要分析,主要是基础知识,这里就随便写点我觉得重要的了。

线程调度

线程调度的过程大概是以下几个步骤:

  • 首先是用户线程接收到了时钟中断,强迫CPU从用户空间进程切换到内核,同时在 trampoline 代码中,保存当前寄存器状态到 trapframe 中;
  • 在 usertrap 处理中断时,切换到了该进程对应的内核线程;
  • 内核线程在内核中,先做一些操作,然后调用 swtch 函数,保存用户进程对应的内核线程的寄存器至 context 对象
  • swtch 函数并不是直接从一个内核线程切换到另一个内核线程;而是先切换到当前 cpu 对应的调度器线程,之后就在调度器线程的 context 下执行 schedulder 函数中;
  • schedulder 函数会再次调用 swtch 函数,切换到下一个内核线程中,由于该内核线程肯定也调用了 swtch 函数,所以之前的 swtch 函数会被恢复,并返回到内核线程所对应进程的系统调用或者中断处理程序中。
  • 当内核程序执行完成之后,trapframe 中的用户寄存器会被恢复,完成线程调度

线程调度过程如下图所示:

xv6线程调度过程

线程调度的过程主要是保存 contex 上下文状态,因为这里的切换全都是以函数调用的形式,因此这里只需要保存被调用者保存的寄存器(Callee-saved register)即可,调用者的寄存器会自动保存。

进程、线程和协程

进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位

在 xv6 中,一个进程只有一个线程,因此本实验中区分不大。

第一个实验的内容很像协程的概念,即用户线程切换时,不进入内核态,而是直接在用户线程上,让用户线程自己主动出让 cpu,而不是接受时钟中断

更具体的区别可以查一查其他人写的博客。

实验内容

感觉整个实验,最核心的还是任务一,后面两个实验与 xv6 没啥关系。

任务一(Uthread)

实现一个用户线程调度的方法。

这里的“线程”是完全用户态实现的,多个线程也只能运行在一个 CPU 上,并且没有时钟中断来强制执行调度,需要线程函数本身在合适的时候主动 yield 释放 CPU。

第一步,上下文切换

首先是 uthread_switch.S 中实现上下文切换,这里可以直接参考(复制) swtch.S:

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

先将一些寄存器保存到第一个参数中,然后再将第二个参数中的寄存器加载进来。

第二步,定义上下文字段

从 proc.h 中复制一下 context 结构体内容,用于保存 ra、sp 以及 callee-saved registers:

// Saved registers for user 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 contex;           /* the context of thread */
};

第三步,调度 thread_schedule

在 thread_schedule 中调用 thread_switch:

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

第四步,创建并初始化线程

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->contex.ra = (uint64)func;
  t->contex.sp = (uint64)&t->stack + (STACK_SIZE - 1);
}

线程栈是从高位到低位,因此初始化时栈指针 sp 应该指向数组底部(好像直接加 STACK_SIZE 也行??)。

返回地址 ra 直接指向该函数的地址就行,这样开始调度时,直接执行该函数(线程)。

任务二(Using threads)

利用加锁操作,解决哈希表 race-condition 导致的数据丢失问题。

主要是,在加大锁还是小锁的问题。如果只加一个锁,锁的粒度很大,会导致丢失性能,结果还不如不加锁的单线程。因此需要将锁的粒度减小,为每个槽位(bucket)加一个锁。

pthread_mutex_t lock[NBUCKET];
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]);
}

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

  pthread_mutex_lock(&lock[i]);
  struct entry *e = 0;
  for (e = table[i]; e != 0; e = e->next) {
    if (e->key == key) break;
  }
  pthread_mutex_unlock(&lock[i]);
  return e;
}

在 main 函数中初始化锁:

  for(int i = 0; i < NBUCKET; i++) {
    pthread_mutex_init(&lock[i], NULL);
  }

任务三(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);
}

总结

线程调度很牛,感觉刚好写了一下精髓部分。最后的条件变量现在还不是很懂,。。

文章同步在知乎

参考文章

  1. MIT 6.S081 lab 7:Multithreading
  2. [mit6.s081] 笔记 Lab7: Multithreading | 多线程
  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
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
Here's an example of how you could use multithreading to prevent the main thread from blocking: ```python from PyQt5.QtCore import Qt, QThread, QTimer, pyqtSignal class SerialThread(QThread): dataReceived = pyqtSignal(str) def __init__(self, parent=None): super().__init__(parent) self.ser = serial.Serial('COM7', 9600, timeout=1) self.running = False def run(self): self.running = True while self.running: data = self.ser.readline().decode().strip() if data: self.dataReceived.emit(data) def stop(self): self.running = False self.wait() self.ser.close() class MainWindow(QMainWindow): def __init__(self, user_id): super().__init__() self.user_id = user_id self.initUI() self.serialThread = SerialThread(self) self.serialThread.dataReceived.connect(self.handle_data) self.serialThread.start() def initUI(self): # Create UI elements here def handle_data(self, data): # Handle incoming data here result = self.query_employee(data) if result: 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("电话:") def query_employee(self, id): # Query employee information from database pass def closeEvent(self, event): self.serialThread.stop() ``` In this example, a `SerialThread` is created to handle the serial communication. The `dataReceived` signal is emitted whenever new data is available. The `handle_data` method is called whenever the signal is emitted, and this method updates the UI with the relevant employee information. The `SerialThread` is started in the `MainWindow` constructor, and stopped in the `closeEvent` method.

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值