如何避免可暂停生产者忙于旋转?

How to avoid busy spinning of a pause-able producer?

背景

我发现了一些 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% CPU。

我试过的

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

这种行为被称为"busy spinning"或"Busy Waiting",其实也有一些解决方案,在需要多线程协同工作的情况下,here.

但这里我觉得有点不一样。等待不是等待某个线程完成其工作。暂时等待。

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

问题

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

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

我决定甚至在 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
        }

    }