美文网首页
令人迷惑的Swift行为

令人迷惑的Swift行为

作者: 行知路 | 来源:发表于2020-09-07 10:32 被阅读0次

    一、引言

            所有的程序员都知道,多线程可能导致程序发生逻辑错误。此类问题的一般原因是,对相同内存的访问导致脏数据。现在,语言、工具、库等的不断改善,大幅降低多线程导致的问题的可能性。如,在OC当中,GCD的出现,大幅降低了多线程操作的难度。但是,多线程问题依然会不时发生。
            虽然,这类问题不时发生,也不一定好排查、修改。但是,从理论角度上来说,我们对此种问题原理、本质是理解比较清楚的。
            但是,在Swift之中存在类似的问题,此类问题令人迷惑的行为。这种行为与上面所说的多线程导致脏数据的问题结果相同,但是又不像多线程问题那样让人明晰其背后的原理。

    二、废多看码

    // 这是一个函数,其有两个输入输出参数
    // 此函数的功能也较简单:把输入参数求和后,把参数修改为和的一半
    // 如果不明白inout是什么意思,请自行百度,或者查看笔者的相关swift语法文章
    func balance(_ x: inout Int, _ y: inout Int) {
        let sum = x + y
        x = sum / 2
        y = sum - x
    }
    
    // 这里定义了一个元组
    var playerInformation = (health: 10, energy: 20)
    
    // 期望此行代码执行之后结果是
    // playerInformation.health=15
    // playerInformation.energy=15
    balance(&playerInformation.health, &playerInformation.energy)
    

            请诸位读者先不要往下看,此时以自己的理解,来看看上面的代码是否有问题。

            以笔者对C、C++、OC等语言的理解,以及对Swift的理解,认为以上代码是没有什么问题的。但是,世事难料,调用balance函数的那一行语句居然导致运行时崩溃!!!


    i报错截图
    • 右侧绿色框所标识的编译器的报错信息:同时访问地址0x0000000100002068,但是写访问需要是独占的。
    • 再看左侧两个红框所标识出来的,对地址0x0000000100002068的修改,他们都指向了调用balance的语句。
    • 0x0000000100002068就是playerInformation变量的地址

    \color{#FF0000}{按照我们的理解,上述代码不会出错,也不应该出错!但,它就是出错了!}
    \color{#FF0000}{------是不是让人很迷惑!!!------}

    三、追根溯源

            上述代码抛出了一个令人迷惑的Swift行为。此问题是由Swift对于内存的访问控制策略导致的。

    3.1 Swift内存访问控制

    3.1.1 问题发生的前提

    • 读写(至少有一个写内存)内存
    • 涉及同一块内存
    • 读写同时发生

    这里的同时,不是指多线程的同时(此种同时已经为大家所熟知),而是指单线程!

    3.1.2 问题发生的表现

    • 运行时崩溃(如“废多看码”章节所示)
    • 编译期错误(如下图所示)


      image.png

    3.1.2 内存问题出现的场景

    • 调用含有inout参数的方法(从进入方法一直到方法结束,都会对参数进行访问)
    • 调用有mutating修饰的方法(从进入方法一直到方法结束,都会隐含对self进行访问)

            以上方法操作的是值类型(结构体、元组)。


    非值类型不会出现问题 值类型出现问题

    对于值类型来说,访问其中的一个成员就会导致对整个实例的独占访问。

    3.2 “废多看码”章节的解释

    func balance(_ x: inout Int, _ y: inout Int) {
        let sum = x + y
        x = sum / 2
        y = sum - x
    }
    
    var playerInformation = (health: 10, energy: 20)
    
    // 此语句输出playerInformation的地址
    withUnsafePointer(to: &playerInformation) {ptr in print(ptr)}
    
    balance(&playerInformation.health, &playerInformation.energy)
    

            对于上述代码,对balance函数的调用,我们进行分析。

    • 含有inout参数
    • 针对第一个参数,需要对playerInformation进行独占访问
    • 针对第二个参数,需要对playerInformation进行独占访问
    • 因为同时需要对playerInformation进行独占访问,所以出问题。
    • 满足了:同时(在balance函数内),访问同一块内存(playerInformation所代表的内存),且有写操作(其实两个都是对playerInformation进行写操作)

    也许从读者角度来看,两次对playerInformation的独占访问,不应该出问题,但是Swift的编译器不这么看。

    3.3 未完待续

            看了以上内容之后,不知道读者是明白了呢,还是更糊涂了,还是有些明白了。笔者看了之后,对这方面有了一些了解,所以就写了这篇文章。虽然笔者知道这是Swift编译器在背后做的一些工作,但是依然不是很理解这么做的原理。
            除了上述例子外,其实还有更令人疑惑的一些内容。

    • 当变量是全局变量时会出错


      全局变量出错
    • 当变量是局部变量不会出错


      局部变量不会出错

    从应用角度来看,以上的理解已经可以了,碰到问题时不以至于十分惊讶。但是对于编译器在背后捣的鬼,还是希望能够有深入理解。如果哪位读者理解了,请不吝赐教!

            如果读者想看更多的资料,可以查看内存安全

    相关文章

      网友评论

          本文标题:令人迷惑的Swift行为

          本文链接:https://www.haomeiwen.com/subject/xzlkektx.html