0

Background

I've found some GIF animation library, which has a background thread that constantly decodes the current frame into a bitmap, being a producer to other thread :

@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)
    }
}

The sample POC I've made for this is available here.

The problem

This is inefficient, because when the thread gets to a point that mIsPlaying is false, it just waits there and constantly checks it. In fact, it causes this thread to do more CPU usage somehow (I checked via the profiler).

In fact, it goes from 3-5% of CPU, to 12-14% CPU.

What I've tried

I had a good knowledge about threads in the past, and I know that simply putting a wait and notify is dangerous as it could still cause the thread to wait on some rare cases. For example when it identified that it should wait, and then before it starts to wait, the outside thread marks it that it shouldn't wait.

This behavior is called "busy spinning" or "Busy Waiting" , and there are actually some solutions about it, in the case of multiple threads that need to work together, here .

But here I think it's a bit different. The wait isn't for some thread to finish its work. It's for temporary waiting.

Another issue here is that the consumer thread is the UI thread, as it is the one that needs to get the bitmap and view it, so it can't just wait work like a consumer-producer solution (UI must never wait, as it can cause "jank") .

The question

What's the proper way to avoid spinning here?

android developer
  • 114,585
  • 152
  • 739
  • 1,270
  • wait and notify can be called in critical section only, for example synchronized(object). Here when you call wait/notify on object other thread is not accessing this section of code because that doesnt have lock. So basically this is right way to avoid unnecessary cpu cycles. – Vipin Sep 09 '18 at 18:44
  • If you could define temporary waiting, we may be able to give you right solution here. Also you are right about UI thread but i think we need clarity why wait is needed actually. – Vipin Sep 09 '18 at 18:49
  • @Vipin I'm talking about the situation that `mIsPlaying` is false, so what it constantly does it `while(true) {}` , meaning it spins on it without waiting, and this takes a lot of CPU for nothing. I think it should have some sort of signal instead, using `wait` – android developer Sep 09 '18 at 19:59
  • simple answer is use wait/notify mechanism, I tried to clarify how it should be used in my first comment. – Vipin Sep 10 '18 at 02:56
  • @Vipin Yes I thought so. I've created an answer to this scenario. Can you please check it out and tell me if you think there is a scenario it could be wrong? – android developer Sep 10 '18 at 05:18
  • 1
    @Vipin in 99% of cases `wait` / `notify` is overkill as there is a lot of useful classes in `java.util.concurrent` package like `CountDownLatch` / `CyclicBarrier` / `Phaser` etc - but honestly it all can be done with minimal effort by using `HandlerThread` class – pskink Sep 10 '18 at 09:30
  • @pskink Do you think one of those can be used here? If so, how? Also, as a side note, I'm pretty sure that behind the scenes those classes use similar API. – android developer Sep 10 '18 at 13:40
  • 1
    i said that this code can be simplified by half when using `HandlerThread` and without using any locks – pskink Sep 10 '18 at 13:42
  • Can you please show how and what you mean, in a new answer? – android developer Sep 10 '18 at 21:27
  • `HandlerThread`: *"Handy class for starting a new thread that has a looper. The looper can then be used to create handler classes. Note that start() must still be called."* - so use it for sending delayed messages, one after another - thus no loops, locks and synchronization needed at all – pskink Sep 11 '18 at 05:28
  • @pskink What will replace the loop exactly? It has a delay amount for when to get the new bitmap. – android developer Sep 11 '18 at 09:07
  • i already answered: `"so use it for sending **delayed** messages, one after another - thus no loops, locks and synchronization needed at all"` – pskink Sep 11 '18 at 09:21
  • @pskink I see. I will try it, but why the downvote? – android developer Sep 11 '18 at 10:50
  • are you asking me? i dont know – pskink Sep 11 '18 at 10:53
  • @pskink I mean. Other APIs are possible, but the basic thing is using wait-notify solution, and if implemented well, it should be fine to use and cross platform. – android developer Sep 11 '18 at 15:25
  • what cross platform? on what other platforms do you have packages: `android.graphics` and `android.widget`? – pskink Sep 12 '18 at 16:17
  • @pskink I was just showing a point. In general. The package of `android.widget` isn't used . Only the bitmap class is used there behind the scenes, in the library itself. – android developer Sep 12 '18 at 19:26
  • it is used in `com.hzy.libnsgif.NsGifView` – pskink Sep 12 '18 at 19:44
  • @pskink I don't use it in the sample, and as I wrote in the question, I also don't intend on using it. I just wanted a way to go over the frames easily, one after another, with the correct time to wait between them. – android developer Sep 13 '18 at 05:25
  • so I already said it twice: use `HandlerThread` – pskink Sep 13 '18 at 05:39
  • @pskink I know. And I wrote that wait-notify is a more general, basic usage, which should also be considered fine. I also wrote I will try to use it. You don't have to repeat it and get upset. – android developer Sep 13 '18 at 07:07
  • @pskink I think I got a working sample for this and updated my own answer to have it too. Thank you for your tip about this. Gave you +1 . :) – android developer Sep 14 '18 at 11:34

1 Answers1

0

So I decided to use wait-notify mechanism, as I couldn't find any nice class to handle this case. This requires delicate thinking, as using threads in the wrong way can cause (on very rare cases) infinite waiting and other weird things.

I've decided to use synchronized even on the UI thread, but I use it while promising it won't be long there, ever. That's because the UI thread should not wait for other threads, in general. I could use a thread-pool (of size 1) for this, to avoid the UI thread from waiting on the synchronized part, but I think it's good enough.

Here's my modified code for the 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
    }

}

After some work, I've got a nice way to do it using HandlerThread. I think it's nicer and probably has better stability. Here's the code:

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
        }

    }
android developer
  • 114,585
  • 152
  • 739
  • 1,270