如何避免可暂停的生产者忙于纺纱?

时间:2018-09-09 14:41:58

标签: java android multithreading kotlin busy-waiting

背景

我发现了一些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是危险的,因为它仍然可能导致线程在某些罕见情况下等待。例如,当它确定应该等待,然后在开始等待之前,外部线程将其标记为不应该等待。

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

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

这里的另一个问题是使用者线程是UI线程,因为它是一个需要获取位图并进行查看的线程,因此它不能像使用者-生产者解决方案那样只是等待工作(UI绝不能等待,因为它可能导致“垃圾”。

问题

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

1 个答案:

答案 0 :(得分:0)

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

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

    }