I have big problem with understanding multi threading in my application and because of that finding a bug. I've checked I think all possibilities and still I am getting various (sometimes unexpected) errors.
Maybe someone here will be able to advice me, what I should do.
In my project I am using two external libraries:
As for the app it has the structure like this:
MainActivity
/ \
/ \
Thread Fragment
(ProcessThread) (GraphFragment)
The idea is that ProcessThread
computes data and provides constant stream of values to GraphFragment
throught EventBus
. In GraphFragment
I have one Series
required by GraphView
.
To update graphs in real time according to the example I need to make a new Runnable
so I did one:
private class PlotsRun implements Runnable{
@Override
public void run() {
mSeries1.appendData(new DataPoint(counter, getRandom()), true, 100);
counter++;
mHandler.post(this);
}
}
and when I start it from fragment's onResume()
method everything is working like a charm.
Unfortunately as I've mentioned I am using external data from another thread. To get it inGraphFragment
I am using (according to the documentation) onEventMainThread()
method.
And in here no matter what I'll do I can't pass data to update my graph in PlotsRun
object. So far I've tried:
Queue
- add value in onEventMainThread
and get in PlotsRun
. It turned out that runnable is reading faster than method is able to update queue.Queue
.mSeries1.appendData(new DataPoint(counter, getRandom()), true, 100);
directly from onEventMainThread
- at some point it gets freez.onEvent()
method inside my runnable and call from there mHandler.post()
- it is blocking UI and updates looks like snapshots.synchronized()
block.What is quite difficult for me to understand is this runnable which is working correctly (at some point).
As it is said on official Android blog you can't update UI from non UI thread. This is why I can't use another thread inside GraphFragment
. But when I've checked my runnable it is running on main thread (UI). This is why I can't create infinite while loop
there instead have to call mHandler.post(this)
.
And still it behaves like another thread because it is faster (called more frequently) then onEventMainThread
method.
What can I do to be able to update my graphs (or where I should look) using data from ProcessThread
?
EDIT1:
Answering on @Matt Wolfe request I am including what I think is the most important part of a code for this problem with all required variable shown how they are declared. It is very simplified example:
MainActivity
:
private ProcessThread testThread = new ProcessThread();
@Override
protected void onResume() {
super.onResume();
testThread.start();
}
private class ProcessThread extends Thread{
private float value = 0f;
private ReadingsUpdateData updater = new ReadingsUpdateData(values);
public void run() {
while(true) {
value = getRandom();
updater.setData(value);
EventBus.getDefault().post(updater);
}
}
}
GraphFragment
:
private LineGraphSeries<DataPoint> mSeries1;
long counter = 0;
private Queue<ReadingsUpdateData> queue;
@Override
public void onResume() {
super.onResume();
mTimer2.run();
}
public void onEventMainThread(ReadingsUpdateData data){
synchronized(queue){
queue.add(data);
}
}
private class PlotsRun implements Runnable{
@Override
public void run() {
if (queue.size()>0) {
mSeries1.appendData(new DataPoint(counter, queue.poll()), true, 100);
counter++;
}
mHandler.post(this);
}
}
The if in runnable is added for protection because of this to fast reading problem. But it shouldn't be here because there should always be something (at least I expect that).
One more thing to add - when I put simple Log.d
and counting variable inside onEventMainThread
it was updating and displaying it's value correctly, but unfortunately logcat isn't main UI.
EDIT2:
This is mainly response for @MattWolfe comment
The mHandler is just variable declared and created in GrapgFragment:
private final Handler mHandler = new Handler();
private Runnable mTimer2;
Yes, that is right I am using mHandler.post()
without any delay. I'll try using some delay to see if there is any difference.
What I didn't mention earlier is that the ProcessThread
is providing also data to other fragments - don't worry they don't interfere with each other or share any resources. This is why I am using EventBus
.
EDIT3:
This a code that I've used as my another idea with another thread in GraphFragment
and runOnMainThread
method:
private MyThread thread = new MyThread();
private class MyThread extends Thread {
Queue<ReadingsUpdateData> inputList;
ReadingsUpdateData msg;
public MyThread() {
inputList = new LinkedList<>();
}
public void run() {
while(true) {
try{
msg = inputList.poll();
} catch(NoSuchElementException nse){
continue;
}
if (msg == null) {
continue;
}
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
mSeries1.appendData(new DataPoint(counter, getRandom()), true, 100);
counter++;
}
});
}
}
public void onEvent(ReadingsUpdateData data){
inputList.add(data);
}
}
Unfortunately, it isn't working neither.
First of All,
The runnable part in your followed example is just for the sake of animating realtime data updating, You can choose to call appendData()
without creating a new runnable. You need to call appendData()
from main thread though.
Secondly,
You can call the appendData()
function directly from your onEventMainThread
function, but as you pointed out that this approach sometimes hangs the UI, One possible reason for this behaviour is that you are probably posting events too frequently, Updating UI too frequently would ultimately hang the UI at some point. You can do the following to avoid this:
Updating UI too frequently might also hang the UI, Here's a Solution:
Put some logic in ProcessThread
to save last sent event time and compare it before sending a new one and if the difference is less than 1 second than save it for sending later and when the next computation is done, compare the time again, if it is greater than 1 second now, than send the events in array or may be send just the latest event, as the latest computation can represent the latest state of graph right?
Hope that helps!
Edit: (in response to Comment 1 & 2)
I am not sure what you tried may be posting your updated code would give a better idea. but I think you tried to implement time check functionality in onEventMainThread
or in PlotsRun
runnable, is that correct? If yes than I am afraid that wouldn't be of much of help for you. What you need to do instead is to implement this time checking check inside ProcessThread and only post new event if threshold time is reached. For following reasons:
1- EventBus on the backend automatically creates a new runnable and calls onEventMainThread
in it. So, handling time check inside ProcessThread
would spawn less unwanted runnables into the memory which would result in less memory consumption.
2- Also no need to maintain queue and spawn new runnables, just update the data in onEventMainThread
.
Below is a bare minimum code to provide proof of concept only, You would need to update it according to your needs:
ProcessThread
class:
private class ProcessThread extends Thread{
private static final long TIME_THRESHOLD = 100; //100 MS but can change as desired
private long lastSentTime = 0;
private float value = 0f;
private ReadingsUpdateData updater = new ReadingsUpdateData(values);
public void run() {
while(true) {
if (System.currentTimeMillis() - lastSentTime < TIME_THRESHOLD) {
try {
Thread.sleep(TIME_THRESHOLD - (System.currentTimeMillis() - lastSentTime));
} catch (InterruptedException e) {}
}
value = getRandom();
updater.setData(value);
EventBus.getDefault().post(updater);
lastSentTime = System.currentTimeMillis();
}
}
}
onEventMainThread
method:
public void onEventMainThread(ReadingsUpdateData data){
mSeries1.appendData(new DataPoint(counter, data), true, 100);
counter++;
}