【问题标题】:Swift global variables thread safeSwift 全局变量线程安全
【发布时间】:2018-02-24 22:46:17
【问题描述】:

我知道,全局变量“不性感”,但我目前的项目中很少。我玩弄了 Xcode 的 Thread Sanitizer,并在它们身上发现了数据竞争。所以我试图让它们线程安全。

因为我还希望对这些变量进行单点管理。我试图在变量的 getter 和 setter 中做 GCD 的东西。

最后我找到了一个可行的解决方案,被编译器接受,Thread Sanitizer 很高兴......但是......这个解决方案看起来很丑(见下文)并且非常慢(做了一个性能测试,它非常慢)。

是的,我知道,如果我为此使用类可能会更“迅速”,但是对于线程安全的全局变量,必须有一个简单的解决方案。

那么您会这么好心并提供提示和建议以优化此尝试吗?欢迎任何提示/想法/建议/评论!

// I used a "computed variable", to overcome the compiler errors, 
// we need a helper variable to store the actual value.
var globalVariable_Value : Int = 0

// this is the global "variable" we worked with
var globalVariable : Int {

    // the setter
    set (newValue) {
        globalDataQueue.async(flags: .barrier) {
            globalVariable_Value = newValue
        }
    }

    // the getter
    get {
        // we need a helper variable to store the result.
        // inside a void closure you are not allow to "return"
        var result : Int = 0
        globalDataQueue.sync{
            result = globalVariable_Value
        }
        return result
    }
}

// usage
globalVariable = 1
print ("globalVariable == \(globalVariable)")

globalVariable += 1
print ("globalVariable == \(globalVariable)")

// output
// globalVariable == 1
// globalVariable == 2

【问题讨论】:

  • 这看起来是正确的,并且可能是您可以获得的更简单和更好的实现之一。您的性能测试测试是什么,“非常慢”对您意味着什么?这是一个上下文问题 - 全局 var 访问,虽然通过队列可能很慢,但可能比您的应用程序可能执行的任何 I/O 快几个数量级。
  • 话虽如此,DispatchQueue.sync(execute:) 的另一个重载确实返回一个值,这可以使您的 getter 稍微好一点 (return globalDataQueue.sync { return globalVariable_Value })。
  • 你有没有尝试过类似这个答案stackoverflow.com/a/47345863/6666165?但不确定它会更快。
  • @aunnn:我尝试在性能测试中也包含“信号量解决方案”,但我没有成功,因为它是一种安静的不同方法。

标签: swift multithreading global-variables


【解决方案1】:

OOPer 要求我重做性能测试,因为发现结果很奇怪。

嗯,他是对的。我确实写了一个简单的应用程序(Performance Test App on GitHub)并附上了一些截图。

我在装有最新 IOS 的 iPhone SE 上运行该测试。该应用程序是在设备上启动的,而不是在 Xcode 中。对于所有显示的测试结果,编译器设置都是“调试”。我也使用“完全优化”(最小最快 [-Os])进行了测试,但结果非常相似。我认为在那个简单的测试中并没有太多需要优化的地方。

测试应用只运行上述答案中描述的测试。为了让它更真实一点,它在三个并行异步调度队列上进行每个测试,其 qos 类为 .userInteractive、.default 和 .background。

可能有更好的方法来测试这些东西。但就这个问题而言,我认为已经足够了。

如果有人能重新评估代码并找到更好的测试算法,我会很高兴......我们都会从中学习。我现在停止这方面的工作。

结果在我看来很奇怪。所有三种不同的方法都提供大致相同的性能。每次运行都有另一个“英雄”,所以我认为它只是受到其他后台任务等的影响。所以即使是 Itai Ferber “不错”的解决方案实际上也没有任何好处。它“只是”一个更优雅的代码。

是的,线程保存解决方案比非排队解决方案慢得多。

这是主要的学习:是的,可以使全局变量线程安全,但它存在一个重大的性能问题。

【讨论】:

  • 你也有 DispatchSemphores 的测试吗?看看(串行)队列是更快还是信号量会很有趣。
  • Cyber​​Mew:不,抱歉,我没有用信号量做那个。我确实使用“dispatchGroups”(这是一种信号量)将并行任务同步到单个触发事件,但到目前为止还没有在这里测试的用例中。
【解决方案2】:

编辑:我保留第一个答案以保留历史记录,但是 OOPer 的暗示导致了完全不同的观点(请参阅下一个答案)。

首先:让我印象深刻的是答案的速度和受过良好教育的速度(我们在周末!)

所以Itai Ferber的建议非常好,按照他的要求,我做了一些性能测试,只是为了给他一些回报;-)

我在操场上使用附加代码运行测试。正如您所看到的,这远远不是一个设计良好的性能测试,它只是一个简单的测试,可以了解性能影响的要点。我做了几次迭代(见下表)。

再次重申:我是在 Playground 中进行的,因此在“真实”应用中绝对时间会好得多,但测试之间的差异会非常相似。

主要发现:

  • 交互显示线性行为(如预期)

  • “我的”解决方案 (test1) 比“未排队”的全局变量 (test0) 慢约 15 倍

  • 我做了一个测试,是否使用了一个额外的全局变量作为辅助变量(test2),这稍微快了一点,但不是真正的突破

  • Itai Ferber (test3) 建议的解决方案比纯全局变量 (test0) 慢大约 6 到 7 倍...所以它是“我的”解决方案的两倍

    李>

所以替代方案 3 不仅看起来更好,因为它不需要辅助变量的开销,而且速度也更快。

// the queue to synchronze data access, it's a concurrent one
fileprivate let globalDataQueue = DispatchQueue(
    label: "com.ACME.globalDataQueue",
    attributes: .concurrent)

// ------------------------------------------------------------------------------------------------
// Base Version: Just a global variable
// this is the global "variable"  we worked with
var globalVariable : Int = 0


// ------------------------------------------------------------------------------------------------
// Alternative 1:  with concurrent queue, helper variable insider getter
// As I used a calculated variable, to overcome the compiler errors, we need a helper variable
// to store the actual value.
var globalVariable1_Value : Int = 0

// this is the global "variable"  we worked with
var globalVariable1 : Int {
    set (newValue) {
        globalDataQueue.async(flags: .barrier) {
            globalVariable1_Value = newValue
        }
    }

    get {
        // we need a helper variable to store the result.
        // inside a void closure you are not allow to "return"
        var globalVariable1_Helper : Int = 0
        globalDataQueue.sync{
            globalVariable1_Helper = globalVariable1_Value
        }
        return globalVariable1_Helper
    }
}

// ------------------------------------------------------------------------------------------------
// Alternative 2:  with concurrent queue, helper variable as additional global variable
// As I used a calculated variable, to overcome the compiler errors, we need a helper variable
// to store the actual value.
var globalVariable2_Value : Int = 0
var globalVariable2_Helper : Int = 0

// this is the global "variable"  we worked with
var globalVariable2 : Int {

    // the setter
    set (newValue) {
        globalDataQueue.async(flags: .barrier) {
            globalVariable2_Value = newValue
        }
    }

    // the getter
    get {
        globalDataQueue.sync{
            globalVariable2_Helper = globalVariable2_Value
        }
        return globalVariable2_Helper
    }
}

// ------------------------------------------------------------------------------------------------
// Alternative 3:  with concurrent queue, no helper variable as Itai Ferber suggested
// "compact" design
var globalVariable3_Value : Int = 0
var globalVariable3 : Int {

    set (newValue) { 
        globalDataQueue.async(flags: .barrier) { globalVariable3_Value = newValue } 
    }

    get { 
        return globalDataQueue.sync { globalVariable3_Value } 
    }
}


// ------------------------------------------------------------------------------------------------
// -- Testing
// variable for read test
var testVar = 0
let numberOfInterations = 2


// Test 0
print ("\nStart test0: simple global variable, not thread safe")
let startTime = CFAbsoluteTimeGetCurrent()

for _ in 0 ..< numberOfInterations {
    testVar = globalVariable
    globalVariable += 1
}

let endTime = CFAbsoluteTimeGetCurrent()
let timeDiff = endTime - startTime
print("globalVariable == \(globalVariable), test0 time needed \(timeDiff) seconds")


// Test 1
testVar = 0
print ("\nStart test1: concurrent queue, helper variable inside getter")
let startTime1 = CFAbsoluteTimeGetCurrent()

for _ in 0 ..< numberOfInterations {
    testVar = globalVariable1
    globalVariable1 += 1
}

let endTime1 = CFAbsoluteTimeGetCurrent()
let timeDiff1 = endTime1 - startTime1
print("globalVariable == \(globalVariable1), test1 time needed \(timeDiff1) seconds")


// Test 2
testVar = 0
print ("\nStart test2: with concurrent queue, helper variable as an additional global variable")
let startTime2 = CFAbsoluteTimeGetCurrent()

for _ in 0 ..< numberOfInterations {
    testVar = globalVariable2
    globalVariable2 += 1
}

let endTime2 = CFAbsoluteTimeGetCurrent()
let timeDiff2 = endTime2 - startTime2
print("globalVariable == \(globalVariable2), test2 time needed \(timeDiff2) seconds")


// Test 3
testVar = 0
print ("\nStart test3: with concurrent queue, no helper variable as Itai Ferber suggested")
let startTime3 = CFAbsoluteTimeGetCurrent()

for _ in 0 ..< numberOfInterations {
    testVar = globalVariable3
    globalVariable3 += 1
}

let endTime3 = CFAbsoluteTimeGetCurrent()
let timeDiff3 = endTime3 - startTime3
print("globalVariable == \(globalVariable3), test3 time needed \(timeDiff3) seconds")

【讨论】:

  • 但测试之间的差异会非常相似。不,请在命令行工具项目中尝试您的基准测试。
  • @OOPer:好吧,也许我今晚会构建一个应用程序,只是为了测试它......我们会看到
  • @OOPer:你是对的。结果完全不一样……很奇怪……写了一个简单的app……明天会显示结果……现在太累了
猜你喜欢
  • 1970-01-01
  • 2017-03-07
  • 2015-07-07
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-06-13
  • 1970-01-01
相关资源
最近更新 更多