Blog

多核CPU下自旋锁的实现原理

Question #

多核CPU下自旋锁的实现原理是怎样的?

Answer #

控制中断,在单CPU情况下,能解决多线程数据安全的问题,但现在,CPU 变成了多核心,或者主板上安装了多颗 CPU,同一时刻下系统中存在多条代码执行流,控制中断只能控制本地 CPU 的中断,无法控制其它 CPU 核心的中断。

所以,原先通过控制中断来维护全局数据安全的方案失效了,这就需要全新的机制来处理这样的情况,于是就轮到自旋锁登场了。

我们先看看自旋锁的原理,它是这样的:首先读取锁变量,判断其值是否已经加锁,如果未加锁则执行加锁,然后返回,表示加锁成功;如果已经加锁了,就要返回第一步继续执行后续步骤,因而得名自旋锁。为了让你更好理解,下面来画一个图描述这个算法。

这个算法看似很好,但是想要正确执行它,就必须保证读取锁变量和判断并加锁的操作是原子执行的。否则,CPU0 在读取了锁变量之后,CPU1 读取锁变量判断未加锁执行加锁,然后 CPU0 也判断未加锁执行加锁,这时就会发现两个 CPU 都加锁成功,因此这个算法出错了。

怎么解决这个问题呢?这就要找硬件要解决方案了,x86 CPU 给我们提供了一个原子交换指令,xchg,它可以让寄存器里的一个值跟内存空间中的一个值做交换。例如,让 eax=memlock,memlock=eax 这个动作是原子的,不受其它 CPU 干扰。

Viewpoint #

From #

08 | 锁:并发操作中,解决数据同步的四种方法

关闭和打开中断

关闭和打开中断

Question #

原子操作只适合于单体变量,如整数。操作系统的数据结构有的可能有几百字节大小,其中可能包含多种不同的基本数据类型。这显然用原子操作无法解决。下面,我们就要写代码实现关闭开启、中断了,x86 CPU 上关闭、开启中断有专门的指令,即 cli、sti 指令,它们主要是对 CPU 的 eflags 寄存器的 IF 位(第 9 位)进行清除和设置,CPU 正是通过此位来决定是否响应中断信号。

正确的关闭和打开中断的方式是怎样的?

Answer #

在关闭中断函数中先保存 eflags 寄存器,然后执行 cli 指令,在开启中断函数中直接恢复之前保存的 eflags 寄存器就行了,具体代码如下。

typedef u32_t cpuflg_t;
static inline void hal_save_flags_cli(cpuflg_t* flags)
{
     __asm__ __volatile__(
            "pushfl \t\n" //把eflags寄存器压入当前栈顶
            "cli    \t\n" //关闭中断
            "popl %0 \t\n"//把当前栈顶弹出到flags为地址的内存中
            : "=m"(*flags)
            :
            : "memory"
          );
}
static inline void hal_restore_flags_sti(cpuflg_t* flags)
{
    __asm__ __volatile__(
              "pushl %0 \t\n"//把flags为地址处的值寄存器压入当前栈顶
              "popfl \t\n"   //把当前栈顶弹出到eflags寄存器中
              :
              : "m"(*flags)
              : "memory"
              );
}

从上面的代码中不难发现,硬件工程师早就想到了如何解决在嵌套函数中关闭、开启中断的问题:pushfl 指令把 eflags 寄存器压入当前栈顶,popfl 把当前栈顶的数据弹出到 eflags 寄存器中。

...

atomic_add函数

atomic_add #

static inline void atomic_add(int i, atomic_t *v)
{
    __asm__ __volatile__("lock;" "addl %1,%0"
                : "+m" (v->a_count)
                : "ir" (i));
}

“lock;” “addl %1,%0” 是汇编指令部分, %1,%0是占位符,它表示输出、输入列表中变量或表态式,占位符的数字从输出部分开始依次增加,这些变量或者表态式会被GCC处理成寄存器、内存、立即数放在指令中。

“+m” (v->a_count) 是输出列表部分,“+m”表示(v->a_count)和内存地址关联。 “ir” (i) 是输入列表部分,“ir” 表示i是和立即数或者寄存器关联。

From #

08 | 锁:并发操作中,解决数据同步的四种方法 一个64位操作系统的设计与实现

GCC文档: Extended Asm - Assembler Instructions with C Expression Operands

Python协程取消与错误处理

Python协程取消与错误处理 #

如果我们想给某些协程任务限定运行时间,一旦超时就取消,又该怎么做呢?再进一步,如果某些协程运行时出现错误,又该怎么处理呢?

import asyncio
async def worker_1():
    await asyncio.sleep(1)
    return 1
async def worker_2():
    await asyncio.sleep(2)
    return 2 / 0
async def worker_3():
    await asyncio.sleep(3)
    return 3
async def main():
    task_1 = asyncio.create_task(worker_1())
    task_2 = asyncio.create_task(worker_2())
    task_3 = asyncio.create_task(worker_3())
    await asyncio.sleep(2)
    task_3.cancel()
    res = await asyncio.gather(task_1, task_2, task_3, return_exceptions=True)
    print(res)
%time asyncio.run(main())
########## 输出 ##########
[1, ZeroDivisionError('division by zero'), CancelledError()]
Wall time: 2 s

你可以看到,worker_1 正常运行,worker_2 运行中出现错误,worker_3 执行时间过长被我们 cancel 掉了,这些信息会全部体现在最终的返回结果 res 中。

不过要注意return_exceptions=True这行代码。如果不设置这个参数,错误就会完整地 throw 到我们这个执行层,从而需要 try except 来捕捉,这也就意味着其他还没被执行的任务会被全部取消掉。为了避免这个局面,我们将 return_exceptions 设置为 True 即可。

...

神经可塑性(neuroplasticity)

Content #

所谓“神经可塑性”(neuroplasticity)指的是,大脑中的轨迹可以改变并成长,也就是说,你的神经元就像黏土一样,你可以对它们进行塑造。也就是说,神经元可以改变,这就是为什么你也可以改变。

Viewpoint #

From #

引出叶京这个人物

Content #

比如微信公众号往事叉烧的文章《冯小刚王朔们的黑历史》,为了引出叶京这个人物,前面用了两段铺垫: #+begin_example 没有人见冯小刚穿过短裤,夏天或运动时他也不穿,永远是一条长裤。有人问过他,这么热不穿短裤?冯小刚说:“不穿,不好看,就跟裤子外面插两根棍似的。”

当然,穿长裤也没有多好看。那年代裤子不修身,冯小刚又竿儿瘦,老远看像是一条裤子走过来,所以得一诨号——“冯裤子”。

第一个这么叫冯小刚的人,是王朔的大哥——叶京。不仅如此,叶京还拍了电视剧,按着冯小刚写了一号人物,让他再也没能摘掉这个外号。#+end_example主角晚登场。主角往往是被慢慢引出来的。这种技巧的经典设计,就是先告诉你一个大众熟悉的人,然后再通过两者、甚至三者之间的关系引出这个幕后的人。

Viewpoint #

From #

放大礼物的力量

Question #

如何放大礼物的力量?

Answer #

根据对方当前的需求进行定制,也能放大礼物的力量。研究者在一家快餐店所做的研究,解释了这类量身定制礼物的效力。在顾客进门时,服务员会为一些顾客送上热情的问候,而为另一些顾客送上热情的问候和一个钥匙链。跟没有得到钥匙链的顾客相比,收到钥匙链的顾客的花销提高了12%,这恰恰跟一般性的互惠原则相吻合。还有一些顾客得到了热情的问候和一小杯酸奶。虽然酸奶的零售价跟钥匙链相同,但它让顾客的消费额提高了24%。为何如此呢?因为顾客是为食物而来,面对一份满足自己需求的礼物,人们会感到更为强烈的回报义务。

钥匙链与酸奶带来的购买率的变化情况和一项超市研究颇为相似,后者向进店顾客赠送非食品礼物(钥匙链)或与食品相关的礼物(品客薯片),使整体购买量分别增加了28%和60%。而一家连锁酒店的完美客户体验项目,在提高客户满意度方面的结果却令人失望。按需定制礼物的效力并不仅适用于商业环境,在人际关系中同样如此,只有当“礼物”符合接受者当下的需求时,它才能带来更大的关系满意度。

From #

影响力

JDK9之前没有基于包的依赖关系的访问控制规则

Content #

JDK 9 之前的 Java 语言没有描述和定义包之间的依赖关系,也没有描述和定义基于包的依赖关系的访问控制规则。 这是一个缺失的访问控制。

比如 public 修饰的 Socket 类。

package java.net;
public class Socket implements java.io.Closeable {
    // snipped
}

用来实现公开接口 Socket 类的 PlatformSocketImpl 类,就是一个使用 public 修饰的类。

package sun.net;
public interface PlatformSocketImpl {
    // snipped
}

Socket 类和 PlatformSocketImpl 类就位于不同的 Java 包。

虽然 PlatformSocketImpl 是一个 public 修饰的类,但是我们并不期望所有的开发者都能够使用它。这是一个用来支持公开接口 Socket 实现的类。除了实现公开接口 Socket 的代码之外,它不应该被任何其他的代码和开发者调用。然而, PlatformSocketImpl 是一个 public 修饰的类。这也就意味着任何代码和开发者都可以使用它。这显然是不符合设计者的预期的。

在 JDK 8 及以前的版本里,一个对象在两个包之间的访问控制,要么是全封闭的,要么是全开放的。所以,JDK 9 之前的 Java 世界里,它的设计者没有办法强制性地设定 PlatformSocketImpl,给出一个恰当的访问控制范围。

Viewpoint #

From #

17 | 模块系统:为什么Java需要模块化?

...

谈判时的共鸣

Question #

哥伦比亚大学的心理学家亚当·加林斯基(Adam Galinsky)开展的一系列研究表明,当我们在谈判桌前产生共鸣时,过于关注我们对手的情绪和感受,会让我们面临什么样的风险?更好的做法是什么?

Answer #

会让我们面临着过度付出的风险。但是,如果我们换位思考,考虑对手的想法和利益,那么就更有可能找到办法满足对手,而不必牺牲自己的利益。

皮特之所以能够完成策略,是因为他进入了里奇的头脑,而不是内心。皮特如果继续同情里奇,那么他就永远也不会找到这个解决方案。通过将他的注意力从里奇的感受转移到想法上,皮特得以从获取者的眼中看待问题,并据此调整自己的策略。

From #

synchronized和ReentrantLock的区别

Question #

Java的 synchronized 和 ReentrantLock 有什么区别?有人说 synchronized 最慢,这话靠谱吗?

Answer #

Locking,它提供了互斥的语义和可见性,当一个线程已经获取当前锁时,其他试图获取的线程只能等待或者阻塞在那里。在 Java 5 以前,synchronized 是仅有的同步手段,在代码中, synchronized 可以用来修饰方法,也可以使用在特定的代码块儿上,本质上 synchronized 方法等同于把方法全部语句用 synchronized 块包起来。

ReentrantLock,通常翻译为再入锁,是 Java 5 提供的锁实现,它的语义和 synchronized 基本相同。再入锁通过代码直接调用 lock() 方法获取,代码书写也更加灵活。与此同时,ReentrantLock 提供了很多实用的方法,能够实现很多 synchronized 无法做到的细节控制,比如可以控制 fairness,也就是公平性,或者利用定义条件等。但是,编码中也需要注意,必须要明确调用 unlock() 方法释放,不然就会一直持有该锁。

synchronized 和 ReentrantLock 的性能不能一概而论,早期版本 synchronized 在很多场景下性能相差较大,在后续版本进行了较多改进,在低竞争场景中表现可能优于 ReentrantLock。

From #

第15讲 | synchronized和ReentrantLock有什么区别呢?