是否需要使用原子或其他同步

Is there a need to use atomics or other synchronization

提问人:Sebastian Lore 提问时间:8/27/2022 更新时间:9/1/2022 访问量:246

问:

我是 Compose 和 Android 开发的新手。在使用 Compose 构建玩具项目时,我遇到了一个问题。我需要从资产中加载 ~100 个短声音才能通过 . 我想异步加载(否则为什么我们需要回调)。SoundPoolSoundPool.load()

viewmodel 中的代码:

    private val TAG = javaClass.simpleName

    private val soundPool: SoundPool
    private val soundsInProgress = AtomicInteger(0)

    var sounds: List<Sound> by mutableStateOf(listOf())
        private set


    init {
        val audioAttributes = AudioAttributes.Builder()
            .setUsage(AudioAttributes.USAGE_MEDIA)
            .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
            .build()
        soundPool = SoundPool.Builder()
            .setMaxStreams(3)
            .setAudioAttributes(audioAttributes)
            .build()
        loadSoundsFromAssets()
    }

    private fun loadSoundsFromAssets() {
        val assetManager = getApplication<MyApp>().applicationContext.assets
        val sounds = assetManager.list("sounds/")!!.map(this::parseSound)
        soundsInProgress.set(sounds.size)
        soundPool.setOnLoadCompleteListener { _, id, status ->
            if (status == 0) {
                val left = soundsInProgress.decrementAndGet()
                Log.i(TAG, "Loaded 1 sound, $left in progress")
                if (left == 0) {
                    sounds = sounds
                }
            } else {
                Log.e(TAG, "Could not load sound $id, status is $status")
            }
        }
        sounds.forEach {
            val soundId = soundPool.load(assetManager.openFd("sounds/" + it.fileName), 0)
            it.soundId = soundId
        }
    }

我在这里的目标是跟踪声音预加载的进度。

问题是:在这里使用原子是矫枉过正,还是没有原子是安全的?

另外,我如何观察 ?MutableState 不会按预期工作。soundsInProgress

Android Kotlin 线程安全 android-jetpack-compose Soundpool

评论


答:

-1赞 AagitoEx 8/30/2022 #1

这取决于此方法是阻塞调用还是非阻塞调用。soundPool.load()

val soundId = soundPool.load(assetManager.openFd("sounds/" + it.fileName), 0)

如果这是一个阻塞调用,那么你不需要原子整数,否则你需要原子整数。

我查找了SoundPool的源代码,似乎它有一个本机(C / C++)实现,可能是一个阻塞调用。所以你可能不需要原子整数。- 请务必先验证这一点。

您可以通过在 setOnLoadCompleteListener 中打印声音文件名来验证这一点,并检查它是否按照从 assetmanager 加载时在列表中的顺序打印。sounds

2赞 Emanuel Moecklin 9/1/2022 #2

SoundPool.load()确实异步加载,即使此处未记录。我根据经验对其进行了测试(在触发回调之前调用返回),并且文档肯定会提到需要从主线程中调用(另外,正如您提到的,如果它是同步调用,回调就没有意义)。loadload

可以假设回调一个接一个地发生,因为它们发生在主线程上(同样没有记录!),所以理论上你不需要一个,但一旦我解释了我对 ui 的建议,我就会回到那个语句。AtomicInteger

对于撰写 UI,我将使用 一个将回调的进度传达给 UI。mutableStateOf

像这样的东西:

private data class SoundProgress(val loaded: Int, val total: Int)
private var progress by mutableStateOf(SoundProgress(0, 0))

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    setContent {
        Text(text = "${progress.loaded}/${progress.total}")
    }

在回调中,你有:

val count = AtomicInteger()
soundPool.setOnLoadCompleteListener { _, id, status ->
    if (status == 0) {
        progress = SoundProgress(count.incrementAndGet(), sounds.size)
    } else {
        Log.e(TAG, "Could not load sound $id, status is $status")
    }
}

请注意,我使用了 for the count 变量。由于所有这些都在主线程上运行(实际的加载操作除外),因此简单的操作很可能会这样做。在我编写异步代码的漫长岁月中,我经常遇到意想不到的行为,所以虽然我会说 inc 操作总是以原子方式发生,但为了安全起见,我使用 regardless 只是为了安全起见。由于使用它不会带来(性能/内存)损失,并且由于无法消除 SoundPool 在后台使用线程池的可能性,因此我会采用保守的方法。AtomicIntegerintAtomicIntegerint

顺便说一句,尽量省略!运营商。虽然它们很方便,但它也是一种反模式。

而不是:

val sounds = assetManager.list("sounds/")!!.map(this::parseSound)

使用这个:

val sounds = assetManager.list("sounds")?.map(this::parseSound) ?: listOf()

有点啰嗦,但你永远不会得到 NPE。

0赞 Richard Onslow Roper 9/1/2022 #3

你不需要这里。无论如何,变量都会在负载时递增,并且由于执行的唯一操作是递增,因此整个点都着火了,不是吗?当你需要观察一个特定的变量,以精确测量它的增量-递减时,你就会使用这些东西,而主流过程本质上取决于它的值。AtomicIntegerAtomicInteger

在您的情况下,您所要做的就是记录/显示已成功加载的文件数量。它不会对任何事情产生任何影响。无论如何,我看不出任何理由标准在某种情况下无法正确更新,但同样,不会花费你任何费用,所以随心所欲地拥抱你的偏执狂。intAtomicInteger

就观察部分而言,其中一个答案鼓励您使用 .我不鼓励这样做,因为最简单的方法几乎总是最好的方法。MutableStateFlow

我假设,从调用 ,您提供的有关加载资产部分的逻辑可以方便地放置在您的 viewModel 中。现在,只需在模型中创建一个对象。然后,像这样更新它initMutablestate<T>

var progress by mutableStateOf(0f)

private fun loadSoundsFromAssets() {
    val assetManager = getApplication<MyApp>().applicationContext.assets
    val sounds = assetManager.list("sounds/")!!.map(this::parseSound)
    soundsInProgress.set(sounds.size)
    soundPool.setOnLoadCompleteListener { _, id, status ->
        if (status == 0) {
            val left = soundsInProgress--.toFloat()
            progress = left / sounds.size
            if (left == 0) {
                sounds = sounds // WHAT DOES THIS EVEN MEAN?
            }
        } else {
            Log.e(TAG, "Could not load sound $id, status is $status")
        }
    }
    sounds.forEach {
        val soundId = soundPool.load(assetManager.openFd("sounds/" + it.fileName), 0)
        it.soundId = soundId
    }
}

然后,在可组合作用域内观察这一点。我不明白为什么这行不通。

{
  Text("${viewModel.progress}")
}

评论

0赞 Emanuel Moecklin 9/1/2022
如果回调是从不同的线程完成的(SoundPool 可能使用线程池,文档不排除这种可能性),则可能会导致不正确的结果,因为它是递减、获取和最后的转换操作。每次操作后都可能切换到另一个线程。正如我所说,线程可能不会在这里发生,但安全总比后悔好。val left = soundsInProgress--.toFloat()
0赞 Richard Onslow Roper 9/1/2022
我的观点是,他没有减少。是的,其他操作可能会发生在非原子增量之间,但它总是递增,这就是我们在这里需要跟踪的全部内容。
0赞 Emanuel Moecklin 9/1/2022
最坏的情况是,它会递减(而不是递增),然后读取较低的值,因为另一个线程可能在此期间递减,因此进度将始终指示要处理的项目递减,但它可能会跳过某些值。现在,如果回调确实是多线程的,那么分配进度也可能导致竞争条件 - >进度可能会增加或减少。为了使这个防弹,应该围绕这两个语句进行一些同步。
0赞 Richard Onslow Roper 9/1/2022
这就是我的观点。此变量的唯一用途是跟踪加载的声音数量。由于声音无法“卸载”,因此根本没有执行递减操作。这意味着,它永远不会通过降低值来干扰。如果操作都是递增的,则它们是否以正确的顺序更新并不重要,因为所有操作都是相同的。