Difficulty understanding complex multithreading in an Android app
I have a big problem understanding multithreading in my application and find the error because of this. I have checked, I think, all possibilities, and yet I am getting various (sometimes unexpected) errors.
Perhaps someone here can advise me on what I should do.
In my project I am using two external libraries:
- GraphView - Provides views for graphical drawing
- EventBus - Provides an interface for easy communication between application components.
As for the application, it has the following structure:
MainActivity
/ \
/ \
Thread Fragment
(ProcessThread) (GraphFragment)
The idea is that it ProcessThread
computes the data and provides a constant stream of values until GraphFragment
throught EventBus
. As GraphFragment
I have one Series
required GraphView
.
To update the live charts according to the example , I need to create a new Runnable
one so that I can do this:
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 with a fragment method onResume()
everything works like a charm.
Unfortunately, as I mentioned, I am using external data from another thread. To get it in GraphFragment
, I use (as per the documentation ) onEventMainThread()
.
And here, no matter what I do, I cannot pass data to update my graph in the object PlotsRun
. So far I have tried:
- using
Queue
- add value toonEventMainThread
and enterPlotsRun
. It turned out that the runnable reads faster than the method is able to update the queue. - creating different buffers - the result is exactly the same as with
Queue
. - call
mSeries1.appendData(new DataPoint(counter, getRandom()), true, 100);
directly fromonEventMainThread
- at some point it gets freez. - creating a
onEvent()
method inside my runnable and calling from theremHandler.post()
- blocks the UI and updates look like snapshots. - using all mentioned with
synchronized()
or without block .
What is hard for me to understand is this is working correctly (at some point).
As the official Android blog said , you cannot update the UI from a non-UI thread. This is why I cannot use another thread internally GraphFragment
. But when I checked my runnable it works on the main thread (UI). That's why I can't create an infinite in while loop
there it needs to be called instead mHandler.post(this)
.
And yet it behaves like a different thread because it is faster (more often) and then onEventMainThread
.
What can I do to update my graphs (or where should I be looking) using the data from ProcessThread
?
EDIT1:
Answering @Matt Wolfe's request, I am including what I think is the most important piece of code for this problem, with all the required variable shown as they are declared. This is a 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);
}
}
If the runnable is added for protection because of this for a quick read problem. But it doesn't have to be, because there must always be something (at least that's what I expect).
Another thing to add is when I put a simple one Log.d
and counting the variable inside onEventMainThread
, it updated and displayed its value correctly, but unfortunately logcat is not the main user interface.
EDIT2:
This is basically the answer for @MattWolfe's comment
mHandler is just a variable declared and created in the GrapgFragment:
private final Handler mHandler = new Handler();
private Runnable mTimer2;
Yes, that's right, I use it mHandler.post()
without any delay. I'll try to use some delay to see if there is a difference.
What I didn't mention earlier is that it ProcessThread
provides data to other fragments as well - don't worry, they don't interfere with each other or share any resources. This is why I am using EventBus
.
EDIT3:
This code I used as my other idea with another thread in the method GraphFragment
and runOnMainThread
:
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 doesn't work either.
source to share
First of all,
The executable part in the following example is only for animating real-time data refresh, you can call appendData()
without creating a new runnable. You have to call appendData()
from the main thread though.
Secondly,
You can call the function appendData()
directly from your function onEventMainThread
, but as you pointed out that this approach sometimes hangs in the UI, one possible reason for this behavior is that you are probably dispatching events too often... Updating the UI too often will eventually damage the UI. You can do the following to avoid this:
Updating the UI too often can also hang the UI, Here's the Solution:
Put some logic in ProcessThread
to store the last dispatched event time and compare it before sending a new one, and if the difference is less than 1 second than saving it to send later and when the next computation is done, compare the time again if it is more than 1 second now than dispatching events in an array or can only the last event be dispatched since the last computation can display the last chart state correctly?
Hope it helps!
Edit: (in response to comments 1 and 2)
I'm not sure if you tried to post the updated code to give a better idea. but i think you tried to implement time check function in onEventMainThread
or in PlotsRun
runnable, is that correct? If so, then I am afraid it will not do you much good. Instead, you need to do a timing check inside the ProcessThread and only post a new event if the threshold time is reached. The following reasons:
1- EventBus on the backend automatically creates a new runnable and calls in it onEventMainThread
. So checking the processing time internally ProcessThread
will result in fewer unwanted launches in memory, resulting in less memory consumption.
2- Also no need to maintain the queue and start new runnables, just update the data in onEventMainThread
.
Below is the minimal code to provide a proof of concept only. You 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
:
public void onEventMainThread(ReadingsUpdateData data){
mSeries1.appendData(new DataPoint(counter, data), true, 100);
counter++;
}
source to share
Your PlotsRun is actually too fast: as soon as it finishes its execution, it requests the main thread's loop to run by calling mHandler.post(processPlots);
.
First, you need to make your data buffer independent of the data collector and data visualizer: create an object that can receive (from the collector) and send (to the visualizer) data. Thus, each component can work completely independently. And your data object is independent of any thread. Your data collector can pass data to your data object when needed, and your main thread can request your data object based on a regular timer.
Then place a lock on that buffer so that none of the other two objects that need to access the data buffer can do so at the same time (which will crash). This locking can be simple synchronized
in the method declaration.
This should ensure that your application does not crash due to concurrency access (this should be your main problem, I guess).
You can then start optimizing your data object by creating additional buffers to store temporary data if the main data collection is already in use when new data arrives, or make a copy of the actual data for it always available to the main thread even when new data is being added when requested by the main thread flow for values.
source to share
I would install something like this:
public class MainActivity extends Activity {
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);
}
}
}
@Override
protected void onResume() {
super.onResume();
testThread.start();
}
}
public class GraphFragment extends Fragment {
private Handler mHandler;
private Queue<ReadingsUpdateData> queue;
@Override
public void onActivityCreated(Bundle state) {
super.onActivityCreated(state);
mHandler = new Handler(Looper.getMainLooper());
}
public void onEvent(ReadingsUpdateData data){
synchronized(queue){
queue.add(data);
}
if (mHandler != null) {
mHandler.post(processPlots);
}
}
//implement pause/resume to register/unregister from event bus
private Runnable processPlots = new Runnable {
@Override
public void run() {
synchronized(queue) {
if (queue.size()>0) {
mSeries1.appendData(new DataPoint(counter, queue.poll()), true, 100);
counter++;
}
}
}
}
}
source to share
Try using AsyncTask which can be executed from your Fragment or Activity. Here is a link to Android Docs for AsyncTask
public class SomeAsyncTask extends AsyncTask<Object,Void, Object>{
@Override
protected void onPreExecute(){
}
@Override
protected Object doInBackground(Object… params) {
//make your request for any data here
return getData();
}
@Override
protected void onPostExecute(Object object){
//update your UI elements here
mSeries1. appendData(object);
}
}
source to share