6.1810: Operating System Engineering 2023 <Lab6: Multithreading>

news2024/11/28 14:51:38

一、本节任务

二、要点

2.1 锁(Locking)

在多 CPU 或者单 CPU 多线程并发的场景中,对临界资源(或者说共享资源)的访问如果不加以限制,可能会引发一些严重的问题,比如当两个线程同时对一个共享数据写的时候,这个共享数据的值就会变为最后一个写的内容,并且会覆盖前一个写的内容。这时候,就有人引入了一些并发控制(concurrency control)技术来避免并发场景中的这些问题。

其中最常用的就是,锁提供了互斥性,保证了在任何时刻只能有一个线程持有它,并且对于锁所保护的对象,只有持有锁的线程才能操作它,其他想要操作该对象的线程必须等待锁的释放。虽然锁能很好地保护临界资源,但是过度使用锁也会导致系统的并发性能下降(毕竟其他想要访问该资源的线程只能等待或阻塞)。

比如,在 xv6 中访问临界资源时,可以使用 acquire 和 release 来获取和释放锁,在 acquire 和 release 之间的区域被称为临界区(critical section)。

在 xv6 中有两种类型的锁,一种是自旋锁(spinlock),另一种为睡眠锁(如信号量)

自旋锁最多只能被一个可执行线程持有,持有自旋锁的线程无法让出 CPU。其他想要获取该锁的线程会一直进行忙循环-旋转-等待锁重新可用。自旋锁不应该被长时间持有,因为其他想要获取该锁的线程会循环等待。自旋锁可以用在中断处理程序中(此处不能使用信号量,因为信号量机制会导致睡眠,前面说过中断处理程序要求尽快执行完)。

信号量是一种睡眠锁。如果有一个线程试图获得一个已经被占用的信号量时,信号量会将其推进一个等待队列,然后让其睡眠。信号量适用于锁会被长时间持有的情况。信号量不能在中断程序中使用,因为在中断上下文中是不能进行调度的。在请求信号量的时候不能占用自旋锁,因为持有自旋锁时是不能睡眠的。可以使用信号量而不是自旋锁来使得在发生争用时,等待的线程能投入睡眠,而不是旋转。

在请求多个锁时,所有线程要使用相同的请求顺序,否则可能造成死锁(deadlock)的问题。

死锁详解和解决办法_避免死锁的三种方法-CSDN博客

xv6 为了避免线程在持有锁的时候进入中断处理函数,在获取自旋锁后,会关闭中断。

在某些场景中,编译器会对代码进行一些优化,比如打乱指令顺序来提高性能,这时候如果要保护的临界区域的内容被优化到临界区之外时,可能会导致一些问题,这时候可以使用内存屏障(memory barrier)来告诉编译器和 CPU 不要重新排序加载或存储。

对于需要长时间等待的操作来说,睡眠锁是十分好的,比如在需要操作一块磁盘区域的时候,该操作十分耗费时间,线程等待锁的时间也十分长,这时候使用睡眠锁可以让等待的线程进入阻塞状态,这时候就可以让其他线程先执行。

2.2 调度(Scheduling)

struct proc in proc.h

  p->trapframe holds saved user thread's registers

  p->context holds saved kernel thread's registers

  p->kstack points to the thread's kernel stack

  p->state is RUNNING, RUNNABLE, SLEEPING, &c

  p->lock protects p->state (and other things...)

在 xv6 中,有两种情况会导致进程切换:

  • 第一种,xv6 的 sleep 和 wakeup 机制可能会导致进程切换。
  • 第二种,xv6 中的定时器中断每次会导致进程切换。

从一个线程切换到另外一个线程需要保存当前线程的寄存器,并且恢复新进程的寄存器;其中 program counter stack pointer 两个寄存器意味着线程的执行位置和栈指针会被保存,因为 CPU 会切换到新进程的位置执行并且切换到新进程的栈。

在 xv6 中,swtch 函数(kernel/swtch.S)实现内核线程的切换(如上图所示),swtch 会保存和恢复一系列的寄存器,这些寄存器被称为上下文(contexts)。当当前进程需要放弃 CPU 的时候,这个进程的内核线程调用 swtch 函数来保存当前上下文并且恢复调度器(scheduler)的上下文。swtch 函数的代码如下:

.globl swtch
swtch:
        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

swtch 函数有两个参数,一个是 struct context *old,也就是当前线程的上下文,另外一个是 struct context *new,即新线程的上下文。从上述代码可以看出,swtch 函数仅仅保存了函数调用约定中 callee 需要保存的寄存器(保存 ra 的目的是为了在 ret 指令后能返回到新线程对应位置执行,因此也不需要保存 pc 寄存器),而返回的位置一般是调用 swtch 函数之后:

返回之后程序会恢复函数调用约定中 caller 需要保存的一系列寄存器(C 编译器在 caller 中生成代码,以在栈上保存 caller 保存的寄存器),从而完整恢复线程的上下文。

切换到当前 CPU 运行的 scheduler 线程后,由 scheduler 线程选择下一个需要运行的线程,然后调用 swtch 切换至该进程:

2.2 Sleep and wakeup

调度和锁能帮我们向其他进程隐藏执行细节,但是外面需要一些手段来帮助进程来进行有意地交互。例如,一个 pipe 的读进程必须等待写进程产生数据;一个父进程的 wait 必须等待子进程 exit;进程读写磁盘则需要磁盘硬件完成读写操作。在 xv6 中使用 sleep 和 wakeup 来解决这些问题,sleep 可以让内核线程等待某个特定事件的发生,而其他线程能使用 wakeup 来告诉另外一个线程其等待的某个事件需要被处理。sleep 和 wakeup 机制也被称为 sequence coordination 或者 conditional synchronization 机制(其实就是一种同步机制,并且和 spinlock 不一样,这种同步机制在等待的时候会让出 CPU,而不是轮询)。 

下面是 sleep 函数的源码:

// Atomically release lock and sleep on chan.
// Reacquires lock when awakened.
void
sleep(void *chan, struct spinlock *lk)
{
  struct proc *p = myproc();
  
  // Must acquire p->lock in order to
  // change p->state and then call sched.
  // Once we hold p->lock, we can be
  // guaranteed that we won't miss any wakeup
  // (wakeup locks p->lock),
  // so it's okay to release lk.

  acquire(&p->lock);  //DOC: sleeplock1
  release(lk);

  // Go to sleep.
  p->chan = chan;
  p->state = SLEEPING;

  sched();

  // Tidy up.
  p->chan = 0;

  // Reacquire original lock.
  release(&p->lock);
  acquire(lk);
}

wakeup 函数的源码:

// Wake up all processes sleeping on chan.
// Must be called without any p->lock.
void
wakeup(void *chan)
{
  struct proc *p;

  for(p = proc; p < &proc[NPROC]; p++) {
    if(p != myproc()){
      acquire(&p->lock);
      if(p->state == SLEEPING && p->chan == chan) {
        p->state = RUNNABLE;
      }
      release(&p->lock);
    }
  }
}

想象一下,如果在 sleep 函数将线程设置为 SLEEPING 状态之前调用 wakeup 函数,会发生什么问题。很明显,这会导致被 sleep 线程永远都无法被唤醒。

xv6 使用锁来避免这种情况,如下图,在调用 sleep 函数之前,会先申请请求对象的锁 s->lock:

然后 sleep 再去获取线程的锁 p->lock,获取到线程的锁之后,这时候再释放请求对象的锁 s->lock 就是安全的了,因为就算此时 wakeup 函数开始执行,也会卡在 acquire(&p->lock); 这一步,只有等待当前线程设置状态为 SLEEPING,并且切换到其他线程释放 p->lock 后,wakeup 函数才能开始判断线程状态,并且将其唤醒。

三、Lab: Multithreading

这个 lab 会让你熟悉多线程,实现一个用户级别的线程包,并且实现一个屏障(barrier)。

3.1 Uthread: switching between threads (moderate) 

在这部分你需要为用户级别的线程设计一个上下文切换机制,并且实现它。在 user 目录下有 uthread.c 和 uthread_switch.S,uthread.c 里包括了用户级别的线程包的实现(部分代码要你自己写),uthread_switch.S 则为上下文切换的汇编(也需要你自己实现)。

实现:

首先在 uthread.c 中声明线程上下文结构体(之所以不需要全部寄存器的原因在上面已经说到了,和内核实现 swtch 的原理是一样的),然后到 thread 结构体中添加 context 成员:

struct thread_context {
  /*   0 */ uint64 ra;
  /*   8 */ 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 thread_context context;
};

thread_create 函数会初始化线程, sp 寄存器指向线程的栈顶,ra 寄存器指向线程的入口函数:

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->context.sp = (uint64)((char *)t->stack + STACK_SIZE);
  t->context.ra = (uint64)func;
}

thread_schedule 函数在找到可执行的线程后,使用 thread_switch() 函数切换到下一个线程执行:

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

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

使用 gdb 调试:

file user/_uthread
b uthread.c:60   // thread_switch 函数的位置
p/x *next_thread // p/x 表示以 16 进制的格式打印 next_thread 指向的内容
x/x next_thread->stack // x/x 表示以 16 进制打印 next_thread->stack 的地址

b thread_switch // 在 thread_switch 函数处设置断点

3.2 Using threads (moderate)

这部分给我们一个程序 ph.c,里面程序会 put 数据到一个 hashtable 中,然后再从 hashtable 中 get 数据,单线程运行的时候是正常的,但是在多线程运行的时候会出现 key missing 的情况,需要我们使用锁来保护多线程中对 hashtable 的修改,并且修改后还要保证多线程相比于单线程的加速。代码如下:

每个 hash bucket 都使用一个锁来保护,使用 init_locks() 函数来初始化该锁:

pthread_mutex_t lock[NBUCKET];

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

在 put 中使用对应的锁来保护对 hashtable 的写操作,读和写并不冲突(因为该例中并不关心 value 的内容,只关心 key 是否插入) ,当两个线程同时对一个 hashtable 进行 insert 操作时,可能会由于并发导致某个 key 丢失,所以对这部分加锁即可:

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

  // is the key already present?
  struct entry *e = 0;
  for (e = table[i]; e != 0; e = e->next) {
    if (e->key == key)
      break;
  }

  pthread_mutex_lock(&lock[i]);
  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]);
}

pthread_create() 可以创建线程,pthread_join() 会挂起调用线程,等待被调用线程被终止,pthread_mutex_lock() 则是请求互斥锁的函数。

​​​​​pthread_create()

pthread_join() 

pthread_mutex_lock() 

3.3 Barrier(moderate)

这部分需要我们实现一个屏障,即在所有线程到达屏障之前,都必须等待,直到所有线程到达才能继续往下执行,其实就是一种同步方法。代码如下,使用互斥锁 bstate.barrier_mutex 来保证对 barrier 修改是原子的,使用 pthread_cond_wait() 函数让当前线程在 cond 上睡眠,等待最后一个线程到来使用 pthread_cond_broadcast() 来将所有在 cond 上睡眠的线程唤醒:

static void
barrier()
{
  // YOUR CODE HERE
  //
  // Block until all threads have called barrier() and
  // then increment bstate.round.
  //
  pthread_mutex_lock(&bstate.barrier_mutex);
  bstate.nthread++;
  if(bstate.nthread == nthread){
    pthread_cond_broadcast(&bstate.barrier_cond);
    bstate.round++;
    bstate.nthread = 0;
  }
  else
    pthread_cond_wait(&bstate.barrier_cond, &bstate.barrier_mutex);
  pthread_mutex_unlock(&bstate.barrier_mutex);
}

最后成功通过测验。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1356815.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Python初探:从零开始的编程奇妙之旅

一、Python是什么 Python是一门多用途的高级编程语言&#xff0c;以其简洁、易读的语法而脱颖而出。在深度学习领域&#xff0c;Python扮演着至关重要的角色。其丰富的科学计算库&#xff08;如NumPy、Pandas、Matplotlib&#xff09;和强大的深度学习框架&#xff08;如Tenso…

jmeter参数化的三种方式

1.用户定义变量 使用变量&#xff1a; ${变量名} 这个变量是全局变量&#xff0c;也就是在下面子节点中都可以使用&#xff1b; 使用场景&#xff1a;两个账号分别有不同的权限&#xff0c;A经办&#xff0c;B审核。等。。。 2.CSV数据文件设置 3.函数

案例071:基于微信小程序的汽车预约维修系统

文末获取源码 开发语言&#xff1a;Java 框架&#xff1a;SSM JDK版本&#xff1a;JDK1.8 数据库&#xff1a;mysql 5.7 开发软件&#xff1a;eclipse/myeclipse/idea Maven包&#xff1a;Maven3.5.4 小程序框架&#xff1a;uniapp 小程序开发软件&#xff1a;HBuilder X 小程序…

jupyter更改默认路径到其它的目录或者到其它的盘 比如D盘

1.打开终端 输入jupyter notebook --generate-config 如下 2.在C:\Users\mb5958\.jupyter路径下 3.用记事本打开它&#xff0c;搜索directory 4.在你想要的路径下新建一个文件夹&#xff0c;如‘D:\jupyterFile’&#xff0c;然后将路径名放在c.NotebookApp.notebook_dir"…

卷麻了,00后测试用例写的比我还好,简直无地自容...........

经常看到无论是刚入职场的新人&#xff0c;还是工作了一段时间的老人&#xff0c;都会对编写测试用例感到困扰&#xff1f;例如&#xff1a; 如何编写测试用例&#xff1f; 作为一个测试新人&#xff0c;刚开始接触测试&#xff0c;对于怎么写测试用例很是头疼&#xff0c;无法…

【PCB专题】Allegro封装更新焊盘

在PCB封装的绘制中&#xff0c;有时会出现需要更新焊盘的情况。比如在制作封装的过程中发现焊盘做的不对而使用PAD_Designer重新更新了焊盘。 那在PCB中如何更新已经修改过的焊盘呢&#xff1f; 打开封装&#xff0c;选择Tools->Padstack->Refresh... 选择Refresh all …

让 sdk 包静默升级的 SAO 操作,你见过几种?

拓展阅读 让 sdk 包静默升级的 SAO 操作&#xff0c;你见过几种&#xff1f; 业务背景 有时候为业务方提供了基础的 sdk 包&#xff0c;为了保证稳定性&#xff0c;一般都是 release 包。 但是每一次升级都非常痛苦&#xff0c;也不可能写一个一步到位的 jar 包&#xff0c…

javascript 常见工具函数(三)

21.克隆数组的几种方法&#xff1a; &#xff08;1&#xff09;slice方法&#xff1a; let arr [1,2,3,4] let arr1 arr.slice() //或者是 let arr1 arr.slice(0) arr[0] 6 console.log(arr) // [6, 2, 3, 4] console.log(arr1) // [1, 2, 3, 4] &#xff08;2&…

Android Jetpack学习系列——Navigation

写在前面 Google在2018年就推出了Jetpack组件库&#xff0c;但是直到今天我才给重视起来&#xff0c;这真的不得不说是一件让人遗憾的事。过去几年的空闲时间里&#xff0c;我一直在尝试做一套自己的组件库&#xff0c;帮助自己快速开发&#xff0c;虽然也听说过Jetpack&#…

Stable Diffusion模型概述

Stable Diffusion 1. Stable Diffusion能做什么&#xff1f;2. 扩散模型2.1 正向扩散2.2 反向扩散 3. 训练如何进行3.1 反向扩散3.2 Stable Diffusion模型3.3 潜在扩散模型3.4 变分自动编码器3.5 图像分辨率3.6 图像放大 4. 为什么潜在空间是可能的&#xff1f;4.1 在潜在空间中…

【智慧地球】星图地球 | 星图地球超算数据工场

当前空天信息处理涉及并发并行的大量计算问题&#xff0c;需要高性能计算、智能计算联合调度&#xff0c;以此来实现多算力融合&#xff1b;而我国算力产业规模快速增长&#xff0c;超算算力资源正需要以任务驱动来统筹。 基于此&#xff0c;中科星图与郑州中心展开紧密合作&a…

Qt学习_17_一些关于QTableWidget的记录

1 QTableWidget::clear() 程序异常退出 近日&#xff0c;项目中使用到QTableWidget&#xff0c;遇到一个问题&#xff0c;项目需要清空这个表格&#xff0c;但是无论调用clear()&#xff0c;clearContents()&#xff0c;程序都报&#xff1a;程序异常退出。 而且项目程序还比较…

OpenVINS学习5——VioManager.cpp/h学习与注释

前言 之前又看到说VioManager.cpp/h是OpenVINS中的核心程序&#xff0c;这次就看看这里面都写了啥&#xff0c;整体架构什么样&#xff0c;有哪些函数功能。具体介绍&#xff1a; VioManager类 整体分析 VioManager类包含 MSCKF 工作所需的状态和其他算法。我们将测量结果输…

二维码地址门牌管理系统:物业管理的未来趋势

文章目录 前言一、数字化管理与便捷服务二、身份认证与安全保障三、业主便利与贴心服务四、未来发展趋势 前言 在数字化时代&#xff0c;物业管理面临着不断增加的挑战。为了提高管理效率、服务业主&#xff0c;二维码门牌管理系统应运而生。本文将探讨这一新型管理方式&#…

【OpenBMC】的内部README 模板

OpenBMC 本项目的AST2500分支核心代码的机型是ast2500-default&#xff0c;克隆代码后进入编译环境的命令为&#xff1a; source setup ast2500-default 一、源码下载、配置以及编译 重要&#xff1a;请参阅confluence 详细步骤 二、代码使用方法 目前所有自定义修改的代码…

虚拟机添加显示屏

1、关闭虚拟机&#xff0c;虚拟机在为关机的情况下&#xff0c;虚拟机设置->显示器->监视器 都是灰色的&#xff0c;不能设置&#xff1b; 2、虚拟机设置->显示器->监视器 “监视器数量” 设置为2 “拉伸模式” 不要勾选 点确定 3、点击 查看->循环使用多个…

蜥蜴目标检测数据集VOC格式1400张

蜥蜴&#xff0c;一种爬行动物&#xff0c;以其独特的形态和习性&#xff0c;成为了人们关注的焦点。 蜥蜴的外观多样&#xff0c;体型大小不一。它们通常拥有长条的身体、四肢和尾巴&#xff0c;鳞片覆盖全身&#xff0c;这使得它们能够在各种环境中轻松移动。大多数蜥蜴拥有…

深度学习 | 多模态算法

AIGC也就是AI内容生成已经成为新一轮人工智能发展的热点和必然趋势&#xff0c;它使得大规模高质量的创作变得更加容易。 一 、InstructGPT模型 1、GPT系列回顾 chatGPT和InstructGPT都使用了指示学习和基于人工反馈的强化学习来指导模型的训练&#xff0c;不同点仅仅是在采集数…

计算机视觉技术-单发多框检测(SSD)

单发多框检测&#xff08;SSD&#xff09;&#xff08;Liu et al., 2016&#xff09;。 该模型简单、快速且被广泛使用。尽管这只是其中一种目标检测模型&#xff0c;但本节中的一些设计原则和实现细节也适用于其他模型。 下图描述了单发多框检测模型的设计。 此模型主要由基础…

GitHub项目推荐:IDE-3D

项目地址 GitHub - MrTornado24/IDE-3D: [SIGGRAPH Asia 2022] IDE-3D: Interactive Disentangled Editing For High-Resolution 3D-aware Portrait Synthesisx 项目简述 这是一个交互式的3D画图工具。无论是改图还是成图&#xff0c;都能使用。是一个很有意思的项目。 项目…