本文作者:包子也沉默

你真的知道并发问题产生的源头吗?

包子也沉默 3年前 (2019-10-23) ( 10-23 ) 854 0条评论
摘要: 通常使用抢占的方式来获取操作系统的时间片。Java的并发编程中都是基于多线程,线程的切换时机通常在一条cpu指令执行完毕之后,而Java作为一门高级编程语言,通常一条语句可能由多个cpu指令来完成。例如:count+=1,至少需要三条指令。指令1:首先,需要把变量count从该内存加载到cpu的寄存器指令2:之后,在寄存器中执行+1操作指令3:最后,将结果写入内存(忽略缓存机制)假设count=0

本文从计算机系统层面来讲述在提升性能的过程中,引发的一系列问题。读完本文你将get到并发编程过程中的原子性,可见性,有序性三大问题的来源。

cpu指令不可中断。在高级语言中,我们为了避免以上情况发生,我们把一个或多个操作在cpu执行过程中不被中断的特性称为原子性。编译优化带来的有序性问题为了提高程序的执行效率,编译器有时会在编译过程中对程

随着硬件发展速度的放缓,摩尔定律已经不在生效,各个硬件似乎已经到了瓶颈;然而随着互联网的普及,网民数量不断增加,对系统的性能带来了巨大的挑战。因此我们要通过各种方式来压榨硬件的性能,从而提高系统的性能进而提升用户体验,提升企业的竞争力。

M"s libc++ implementation of std::sort.这个排序算法是基于LLVM的libc++中的std::sort实现的,算是快排的优化版,当元素数小于等于16时有特

由于CPU,内存,IO三者之间速度差异,为了提高系统性能,计算机系统对这三者速度进行平衡。

***获取Singleton对象*/publicstaticSingletongetInstance(){if(instance==null){synchronized(Singleton.class

  • CPU 增加了缓存,以均衡与内存的速度差异;
  • 操作系统增加了进程、线程,以分时复用 CPU,进而均衡 CPU 与 I/O 设备的速度差异;
  • 编译程序优化指令执行次序,使得缓存能够得到更加合理地利用。

缓存导致得可见性的问题

一个线程对共享变量的修改,另外一个线程能够立刻看到,我们称为可见性。

多核时代,每颗 CPU 都有自己的缓存,这时 CPU 缓存与内存的数据一致性就没那么容易解决了,当多个线程在不同的 CPU 上执行时,这些线程操作的是不同的 CPU 缓存。

线程切换带来的原子性问题

由于IO和cpu执行速度的差别巨大,所以在早期操作系统中就发明的多线程,即使在单核的cpu上我们也可以一遍听着歌,一边写着bug,这就是多线程。

ue;        arTmp[i].b = *p;        arTmp[i].i = i;        i++;    }    ZVAL_UNDEF(&arTmp[i].b.va

早期操作系统基于进程来调度cpu, 不同进程间是不共享内存空间的,所以进程要做任务切换要切换内存映射地址,而一个进程创建的所有线程都是共享一个内存空间,所以线程做任务切换成本很低。现代操作系统都基于更轻量级的线程来调度,现在我们提到的“任务切换”都是指“线程切换”。

性问题为了提高程序的执行效率,编译器有时会在编译过程中对程序的进行优化,从而改变程序的执行顺序。如程序“a=4;b=5”,在优化后执行顺序可能变成“b=5;a=4”。通常进行一项优化过程中可能会带来另

因为式单核cpu,所以同一时刻只能执行一个任务,所以多线程通常使用抢占的方式来获取操作系统的时间片。

Java的并发编程中都是基于多线程,线程的切换时机通常在一条cpu指令执行完毕之后,而Java作为一门高级编程语言,通常一条语句可能由多个cpu指令来完成。例如:count += 1, 至少需要三条指令。

stance是否为空,如果为空则锁住Singleton.class对象并再次检查instance是否为空,如果仍然为空则创建Singleton的一个实例。publicclassSingleton{st

  • 指令1:首先,需要把变量count从该内存加载到cpu的寄存器
  • 指令2:之后,在寄存器中执行+1操作
  • 指令3: 最后,将结果写入内存(忽略缓存机制)
    假设count = 0, 有2个线程同时执行count+=1这段代码。线程A执行完指令1将count = 0加载到cpu寄存器,进行了任务切换到了线程B执行,线程B执行完之后将count = 1写入到内存,然后再切换到线程A执行,此时线程A获取寄存器中的count=0进行+1操作得到结果也是1,所以最终内存中的count = 1,而我们所期望的是2.

    CPU层面的原子操作仅仅是在指令级别的,既一条cpu指令不可中断。在高级语言中,我们为了避免以上情况发生,我们把一个或多个操作在cpu执行过程中不被中断的特性称为原子性

    rt_type, 0);的第二个参数,即参数reverse的值为0,也就是当sort_type为PHP_SORT_STRING时,调用的是php_array_data_compare_string函数

编译优化带来的有序性问题

为了提高程序的执行效率,编译器有时会在编译过程中对程序的进行优化,从而改变程序的执行顺序。如程序“a = 4; b = 5”,在优化后执行顺序可能变成“b = 5; a = 4”。通常进行一项优化过程中可能会带来另一项问题,改变程序的执行顺序通常也会导致让人意想不到的bug。

UNDEF) continue;        if (Z_TYPE(p->val) == IS_INDIRECT && Z_TYPE_P(Z_INDIRECT(p->va

Java领域中一个经典的案例就是利用双重检查创建单例对象,代码如下:在获取实例getInstance()方法中,我们首先判断instance是否为空,如果为空则锁住Singleton.class对象并再次检查instance是否为空,如果仍然为空则创建Singleton的一个实例。

是1,所以最终内存中的count=1,而我们所期望的是2.CPU层面的原子操作仅仅是在指令级别的,既一条cpu指令不可中断。在高级语言中,我们为了避免以上情况发生,我们把一个或多个操作在cpu执行过程

public class Singleton {
static Singleton instance;
/**
  * 获取Singleton对象
  */
public static Singleton getInstance(){
    if (instance == null) {
        synchronized(Singleton.class) {
            if (instance == null)
                instance = new Singleton();
            }
        }
    return instance;
    }
}

假设有两个线程 A、B 同时调用 getInstance() 方法,他们会同时发现 instance == null,于是同时对 Singleton.class 加锁,此时 JVM 保证只有一个线程能够加锁成功(假设是线程 A),另外一个线程则会处于等待状态(假设是线程 B);线程 A 会创建一个 Singleton 实例,之后释放锁,锁释放后,线程 B 被唤醒,线程 B 再次尝试加锁,此时是可以加锁成功的,加锁成功后,线程 B 检查 instance == null 时会发现,已经创建过 Singleton 实例了,所以线程 B 不会再创建一个 Singleton 实例。

_type)    ZEND_PARSE_PARAMETERS_END();ZEND_PARSE_PARAMETERS_START(1, 2),第一个参数表示必传参数个数,第二个参数表示最多参数个数,

以上过程仅仅是我们的理想情况下,但是实际过程中往往会创建多次Singleton实例。原因是创建一个对象需要多条cpu指令,且编译器可能对这几条指令进行了排序。在执行new语句创建一个对象时,通常会包含一下三个步骤(此处进行了简化,实际实现过程会比此过程复杂):

               }            } else {                if (reverse) {                    return php_arr

  1. 在堆内存为对象分配一块内存M
  2. 在内存M区域进行Singleton对象的初始化
  3. 将内存M地址赋值给instance变量。
    但是实际优化后的执行顺序可能时以下这种情况:
  4. 在堆内存为对象分配一块内存M
  5. 将内存M地址赋值给instance变量。
  6. 在内存M区域进行Singleton对象的初始化
    假设A,B线程同时执行到了getInstance()方法,线程A执行完instance = $M(将内存M地址赋值给instance变量,但是未将对象进行初始化)后切换到B线程,当B线程执行到instance == null时,由于instance已经指向了内存M的地址,所以会返回false,直接返回instance,如果我们这是访问instance中的成员变量或者方法时就可能会出现NullPointException。

总结

在操作系统平衡CPU,内存,IO三者速度差异过程中进行了一系列的优化。

 (struct bucketindex *) pemalloc((Z_ARRVAL_P(array)->nNumOfElements + 1) * sizeof(struct bucketin

  • CPU 增加了缓存,以均衡与内存的速度差异;
  • 操作系统增加了进程、线程,以分时复用 CPU,进而均衡 CPU 与 I/O 设备的速度差异;
  • 编译程序优化指令执行次序,使得缓存能够得到更加合理地利用。

这三个不同方面的优化也带来了可见性,原子性,有序性等问题,他们通常是并发程序的bug的源头。
笔者的个人博客网站

文章版权声明:除非注明,否则均为本站原创文章,转载或复制请以超链接形式并注明出处。
分享到:
赞 (0

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏

发表评论

快捷回复:

评论列表 (有 0条评论, 854人围观) 参与讨论