MIT6.828_Lecture9:Multiprocessors and locking

1.为什么要讨论locking呢?
应用程序希望使用多核处理器并行加速
因此内核必须处理并行系统调用
从而并行访问内核数据(缓存、进程和c)
    locks有助于正确地共享数据
    locks可以限制并行加速

2.Locking homework
recall: ph.c, multi-threaded hash table, put(), get()
vi ph0.c
:为什么要在多核处理器上运行ph.c ?
    diagram: CPUs, bus, RAM
    假设:CPU是瓶颈,可以在CPU之间划分工作
:我们能打败单核put()时间吗?单核get()的时间吗?
     ./ph0 1
:如何测量并行加速?
    ./ph0 2
单位时间的工作量是原来的两倍!
:the missing keys在哪里?
:具体情况?
    diagram…
    table[0] = 15
    (并发)concurrent put(5), put(10)
两个insert()都分配新entry,指向15
两者都将表[0]设置为它们的新entry
最后一个插入方赢了,另一个输了!
称为“lost update”;
“race”的例子。race = concurrent accesses;至少写一个
:lock/unlock放在哪里?
一个lock覆盖整个hash table?
why? why not?
称为“big”或“coarse-grained”(粗粒度)lock
./ph1 2
faster? slower? why?
:每个table[] entry一个锁?
这个锁是“finer grained”(细粒度)的
为什么这是好事呢?
./ph2 2
faster? slower? why?
对于per-bucket locks,什么可能更困难?
我们会得到一个良好的有着10核心的加速?NBUCKET = 5…
:每个struct entry一个lock,保护下一个指针?
why? why not?
:get()需要锁定吗?
问:如果存在并发的put(), get()是否需要锁定吗?

3.The lock abstraction:

lock l
acquire(l)
	x = x + 1 -- "critical section"
release(l)

a lock 本身就是一个对象
如果多个线程调用acquire(l)
    只有一个会马上return
    其他的将等待release()——“block”
一个程序通常有很多数据,很多锁
    如果不同的线程使用不同的数据,
    那么他们可能持有不同的锁,
    所以它们可以并行执行——完成更多的工作。
注意,锁l并没有特别绑定到数据x
    程序员有一个通信计划

4.决定何时需要lock的保守规则:
任何时候,只要两个线程使用一个内存位置,并且至少有一个线程是写线程
除非您持有正确的锁,否则不要触摸共享数据!
(太严格:程序逻辑有时可能排除共享;无锁)
(过于宽松:printf ();并不总是简单的lock/data通信)

5.锁是自动的吗?
也许该语言可以将锁与每个数据对象关联起来,编译器在每次使用时都会添加获取/释放让程序员不容易忘记!
这种想法往往过于死板:
    rename(“d1/x”, “d2/y”):
        lock d1, erase x, unlock d1
        lock d2, add y, unlock d2
    问题:文件暂时不存在!
        rename()应该是原子的
            其他系统调用应该在调用之前或之后查看,而不是在调用之间,否则很难编写程序
    我们需要:
        lock d1 ; lock d2
        erase x, add y
        unlock d2; unlock d1
也就是说,程序员经常需要显式控制持有锁的代码区域,为了隐藏尴尬的中间状态

6.思考锁实现什么功能?

锁有助于避免丢失更新
锁帮助您创建原子多步骤操作(atomic multi-step operations)——隐藏中间状态
锁帮助操作维护数据结构上的不变量(invariants)
    假设不变量在操作开始时为真,操作使用锁来隐藏对不变量的临时改变,操作在释放锁之前恢复不变量

7.Problem: deadlock死锁

notice rename() held two locks
  what if:
    core A              core B
    rename(d1/x, d2/y)  rename(d2/a, d1/b)
      lock d1             lock d2
      lock d2 ...         lock d1 ...

解决方案:
    程序员计算出所有锁的顺序,所有代码都必须按照这个顺序获取锁
     i.e. predict locks(预测锁), sort, acquire – complex!

8.Locks 对比 modularity(模块化)
    锁使得在模块中隐藏细节变得困难,为了避免死锁,我需要知道调用函数获得的锁,即使我不使用它们,我也可能需要在calling之前获得它们
    例如:锁通常不是单个模块的私有业务

9.如何考虑在哪里放置锁?
下面是一个简单的新代码计划:
1). 在串行执行(serial execution)下编写正确的模块
假设一个CPU,一个线程
insert(){e->next = l;l = e;}
但如果并行执行则不正确
2). 添加锁来强制串行执行
因为acquire/release一次只允许一个CPU执行
要点:
    程序员更容易推断出串行代码,锁可以使您的串行代码正确,尽管并行

10.Locks and parallelism
prevent并行执行
为了获得并行性,通常需要分割数据和锁,让每个核心使用不同的数据和不同的锁。“fine grained locks”(细粒度锁)
选择最佳的data/locks 分割是一个设计挑战
    whole ph.c table; each table[] row; each entry
    whole FS; directory/file; disk block
    whole kernel; each subsystem; each object
您可能需要重新设计代码,使其能够很好地并行工作
    示例:将单个空闲内存列表分解为每个内核的空闲列表,如果线程为单个free list在锁上等待了很长时间,那么这是有帮助的

Let’s look at locking in xv6.

11.A typical use of locks: ide.c
典型的许多O/S的设备驱动程序安排
diagram:
    user processes, kernel, FS, iderw, append to disk queue(添加到磁盘队列)
    IDE disk hardware
    ideintr
并发性的来源: processes, interrupt
only one lock in ide.c:idelock – fairly coarse-grained
    iderw() – what does idelock protect?
        1). idequeue操作中没有竞争
        2). 如果队列不是空的,IDE h/w将执行队列的头部
        3).没有对IDE寄存器的并发访问
    ideintr() – interrupt handler
获取锁——可能必须在中断级别等待!
        使用idequeue (1)
        将下一个排队的请求提交给IDE h/w (2)
        touches IDE h/w寄存器(3)

12.How to implement locks?

  why not:
    struct lock { int locked; }
    acquire(l) {
      while(1){
        if(l->locked == 0){ // A
          l->locked = 1;    // B
          return;
        }
      }
    }
  oops: race between lines A and B
  how can we do A and B atomically?

13.Atomic exchange instruction:

mov $1, %eax
xchg %eax, addr
does this in hardware:
  lock addr globally (other cores cannot use it)
  temp = *addr
  *addr = %eax
  %eax = temp
  unlock addr

x86 h/w提供了锁定内存位置的概念
    diagram: cores, bus, RAM, lock thing
    所以我们真的把问题推到了硬件上,h/w实现缓存线或整个总线的粒度
内存锁强制并发xchg一次运行一个,而不是交错运行

Now:
  acquire(l){
    while(1){
      if(xchg(&l->locked, 1) == 0){
        break
      }
    }
  }
如果l->locked已经是1,xchg设置为1(再次),返回1,循环继续
如果l->locked为0,最多有一个xchg会看到0;它将它等于1,然后返回0;其他xchgs将返回1
这是一个"spin lock"(自旋锁),因为在获取循环中等待内核“自旋”

Look at xv6 spinlock implementation
  spinlock.h -- you can see "locked" member of struct lock
  spinlock.c / acquire():
    see while-loop and xchg() call
    what is the pushcli() about?
      why disable interrupts?
  release():
    sets lk->locked = 0
    and re-enables interrupts

Detail: memory read/write ordering
  假设两个核使用锁来保护计数器x
  我们有一个简单的锁实现
  Core A:          Core B:
    locked = 1
    x = x + 1      while(locked == 1)
    locked = 0       ...
                   locked = 1
                   x = x + 1
                   locked = 0
                   
编译器和CPU重新排序内存访问
也就是说,它们不遵守源程序的内存引用顺序
例如:编译器可能会为核心A生成以下代码:
	locked = 1
    locked = 0
    x = x + 1
即将增量移出临界区!
release()调用_sync_synchronize()可以防止重新排序
	编译器不会将内存引用移过一个_sync_synchronize(),(可能)发出"memory barrier"指令告诉CPU
acquire()xchg()的调用具有类似的效果:
	intel承诺不会重新使用过去的xchg指令。x86.h xchg()中的一些垃圾代码告诉C编译器不要删除或重新排序
	(volatile asm说不要删除,"m"说不要重新排序)

14.为什么 spin locks(自旋锁)?
它们在等待时不会浪费CPU吗?
为什么不放弃CPU,切换到另一个进程,让它运行呢?
如果持有线程需要运行;等待线程不应该产生CPU吗?
旋转锁指南:
    持有旋转锁很短的时间
    持有自旋锁时,不要释放CPU
系统通常为较长的临界段提供“blocking”(阻塞)锁
    等待线程会产生CPU
    但一般来说,管理费用较高
    稍后您将看到一些xv6阻塞方案

15.建议:

如果没有必要,就不要share
从一些coarse-grained locks(粗粒度的锁)开始
测试代码——哪些锁防止并行?
仅在并行性能需要时才使用fine-grained locks(细粒度锁)
使用自动 race 检测器

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值