【问题标题】:How to avoid busy spinning of a pause-able producer?如何避免忙碌的可暂停生产者旋转?
【发布时间】:2018-09-09 14:41:58
【问题描述】:

背景

我发现了一些GIF animation library,它有一个后台线程,不断将当前帧解码为位图,作为其他线程的生产者:

@Volatile
private var mIsPlaying: Boolean = false

...
while (mIsRunning) {
    if (mIsPlaying) {
        val delay = mGifDecoder.decodeNextFrame()
        Thread.sleep(delay.toLong())
        i = (i + 1) % frameCount
        listener.onGotFrame(bitmap, i, frameCount)
    }
}

我为此制作的示例 POC 可用 here

问题

这是低效的,因为当线程到达mIsPlaying 为假的点时,它只是在那里等待并不断检查它。事实上,它会导致这个线程以某种方式使用更多的 CPU(我通过分析器进行了检查)。

实际上,它从 CPU 的 3-5% 变为 12-14%。

我尝试过的

我过去对线程有很好的了解,并且我知道简单地放置 waitnotify 是危险的,因为它仍然可能导致线程在一些罕见的情况下等待。例如,当它确定它应该等待时,然后在它开始等待之前,外部线程将它标记为它不应该等待。

这种行为称为“忙转”或“忙等待”,实际上有一些解决方案,在多个线程需要协同工作的情况下,here

但在这里我觉得有点不同。等待不是为了某个线程完成它的工作。暂时等待。

这里的另一个问题是消费者线程是 UI 线程,因为它需要获取位图并查看它,所以它不能像消费者-生产者解决方案那样等待工作(UI 绝不能等待,因为它会导致“卡顿”)。

问题

避免在此处旋转的正确方法是什么?

【问题讨论】:

  • wait 和 notify 只能在临界区调用,例如 synchronized(object)。在这里,当您在对象上调用 wait/notify 时,其他线程不会访问这部分代码,因为它没有锁。所以基本上这是避免不必要的 CPU 周期的正确方法。
  • 如果您可以定义临时等待,我们或许可以在这里为您提供正确的解决方案。你对 UI 线程也是正确的,但我认为我们需要明确为什么实际上需要等待。
  • @Vipin 我说的是mIsPlaying 为假的情况,所以它一直在做它while(true) {} ,这意味着它无需等待即可旋转,这需要大量CPU没有什么。我认为它应该有某种信号,使用wait
  • @Vipin 在 99% 的情况下 wait / notify 是多余的,因为 java.util.concurrent 包中有很多有用的类,例如 CountDownLatch / CyclicBarrier / Phaser 等- 但老实说,这一切都可以通过使用HandlerThread class 以最小的努力完成
  • 我说这段代码在使用HandlerThread且不使用任何锁的情况下可以简化一半

标签: java android multithreading kotlin busy-waiting


【解决方案1】:

所以我决定使用等待通知机制,因为我找不到任何好的类来处理这种情况。这需要仔细思考,因为以错误的方式使用线程会导致(在极少数情况下)无限等待和其他奇怪的事情。

我决定即使在 UI 线程上也使用synchronized,但我在使用它的同时保证它不会在那里很久,永远。这是因为 UI 线程通常不应该等待其他线程。我可以为此使用线程池(大小为 1),以避免 UI 线程等待同步部分,但我认为这已经足够了。

这是我为 gifPlayer 修改的代码:

class GifPlayer(private val listener: GifListener) : Runnable {
    private var playThread: Thread? = null
    private val gifDecoder: GifDecoder = GifDecoder()
    private var sourceType: SourceType? = null
    private var filePath: String? = null
    private var sourceBuffer: ByteArray? = null
    private var isPlaying = AtomicBoolean(false)

    interface GifListener {
        fun onGotFrame(bitmap: Bitmap, frame: Int, frameCount: Int)

        fun onError()
    }

    @UiThread
    fun setFilePath(filePath: String) {
        sourceType = SourceType.SOURCE_PATH
        this.filePath = filePath
    }

    @UiThread
    fun setBuffer(buffer: ByteArray) {
        sourceType = SourceType.SOURCE_BUFFER
        sourceBuffer = buffer
    }

    @UiThread
    fun start() {
        if (sourceType != null) {
            playThread = Thread(this)
            synchronized(this) {
                isPlaying.set(true)
            }
            playThread!!.start()
        }
    }

    @UiThread
    fun stop() {
        playThread?.interrupt()
    }

    @UiThread
    fun pause() {
        synchronized(this) {
            isPlaying.set(false)
            (this as java.lang.Object).notify()
        }
    }

    @UiThread
    fun resume() {
        synchronized(this) {
            isPlaying.set(true)
            (this as java.lang.Object).notify()
        }
    }

    @UiThread
    fun toggle() {
        synchronized(this) {
            isPlaying.set(!isPlaying.get())
            (this as java.lang.Object).notify()
        }
    }

    override fun run() {
        try {
            val isLoadOk: Boolean = if (sourceType == SourceType.SOURCE_PATH) {
                gifDecoder.load(filePath)
            } else {
                gifDecoder.load(sourceBuffer)
            }
            val bitmap = gifDecoder.bitmap
            if (!isLoadOk || bitmap == null) {
                listener.onError()
                gifDecoder.recycle()
                return
            }
            var i = -1
            val frameCount = gifDecoder.frameCount
            gifDecoder.setCurIndex(i)
            while (true) {
                if (isPlaying.get()) {
                    val delay = gifDecoder.decodeNextFrame()
                    Thread.sleep(delay.toLong())
                    i = (i + 1) % frameCount
                    listener.onGotFrame(bitmap, i, frameCount)
                } else {
                    synchronized(this@GifPlayer) {
                        if (!isPlaying.get())
                            (this@GifPlayer as java.lang.Object).wait()
                    }
                }
            }
        } catch (interrupted: InterruptedException) {
        } catch (e: Exception) {
            e.printStackTrace()
            listener.onError()
        } finally {
        }
    }


    internal enum class SourceType {
        SOURCE_PATH, SOURCE_BUFFER
    }

}

经过一些工作,我有了一个使用 HandlerThread 的好方法。我认为它更好,并且可能具有更好的稳定性。代码如下:

open class GifPlayer(private val listener: GifListener) {
        private val uiHandler = Handler(Looper.getMainLooper())
        private var playerHandlerThread: HandlerThread? = null
        private var playerHandler: Handler? = null
        private val gifDecoder: GifDecoder = GifDecoder()
        private var currentFrame: Int = -1
        var state: State = State.IDLE
            private set
        private val playRunnable: Runnable

        enum class State {
            IDLE, PAUSED, PLAYING, RECYCLED, ERROR
        }

        interface GifListener {
            fun onGotFrame(bitmap: Bitmap, frame: Int, frameCount: Int)

            fun onError()
        }

        init {
            playRunnable = object : Runnable {
                override fun run() {
                    val frameCount = gifDecoder.frameCount
                    gifDecoder.setCurIndex(currentFrame)
                    currentFrame = (currentFrame + 1) % frameCount
                    val bitmap = gifDecoder.bitmap
                    val delay = gifDecoder.decodeNextFrame().toLong()
                    uiHandler.post {
                        listener.onGotFrame(bitmap, currentFrame, frameCount)
                        if (state == State.PLAYING)
                            playerHandler!!.postDelayed(this, delay)
                    }
                }
            }
        }

        @Suppress("unused")
        protected fun finalize() {
            stop()
        }

        @UiThread
        fun start(filePath: String): Boolean {
            if (state != State.IDLE)
                return false
            currentFrame = -1
            state = State.PLAYING
            playerHandlerThread = HandlerThread("GifPlayer")
            playerHandlerThread!!.start()
            playerHandler = Handler(playerHandlerThread!!.looper)
            playerHandler!!.post {
                gifDecoder.load(filePath)
                val bitmap = gifDecoder.bitmap
                if (bitmap != null) {
                    playRunnable.run()
                } else {
                    gifDecoder.recycle()
                    uiHandler.post {
                        state = State.ERROR
                        listener.onError()
                    }
                    return@post
                }
            }
            return true
        }

        @UiThread
        fun stop(): Boolean {
            if (state == State.IDLE)
                return false
            state = State.IDLE
            playerHandler!!.removeCallbacks(playRunnable)
            playerHandlerThread!!.quit()
            playerHandlerThread = null
            playerHandler = null
            return true
        }

        @UiThread
        fun pause(): Boolean {
            if (state != State.PLAYING)
                return false
            state = State.PAUSED
            playerHandler?.removeCallbacks(playRunnable)
            return true
        }

        @UiThread
        fun resume(): Boolean {
            if (state != State.PAUSED)
                return false
            state = State.PLAYING
            playerHandler?.removeCallbacks(playRunnable)
            playRunnable.run()
            return true
        }

        @UiThread
        fun toggle(): Boolean {
            when (state) {
                State.PLAYING -> pause()
                State.PAUSED -> resume()
                else -> return false
            }
            return true
        }

    }

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2014-12-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-08-11
    • 2010-10-10
    • 1970-01-01
    相关资源
    最近更新 更多