如何避免可暂停生产者忙于旋转?
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。
我试过的
我过去对线程有很好的了解,我知道简单地放置一个 wait
和 notify
是危险的,因为它仍然可能导致线程在一些罕见的情况下等待。例如当它确定它应该等待,然后在它开始等待之前,外部线程标记它不应该等待。
这种行为被称为"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
}
}
背景
我发现了一些 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。
我试过的
我过去对线程有很好的了解,我知道简单地放置一个 wait
和 notify
是危险的,因为它仍然可能导致线程在一些罕见的情况下等待。例如当它确定它应该等待,然后在它开始等待之前,外部线程标记它不应该等待。
这种行为被称为"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
}
}