Share
Embed
Multi-Threading, Concurrency and Async on Android - DroidCon NYC 2016
Multi-threading and concurrency is one of the most difficult challenges in software development. When writing apps for Android, this has proven to be even more of a challenge because the way the framework and APIs are designed. In this session we will look at why this is so difficult, take a deep dive into how threading is implemented on Android and finally how developers can make their work easier by gaining a better understanding of threads work on Android and the tools and APIs available. This session will not require any existing knowledge about threads, RxJava, java.util.concurrent or any other concurrency or asynchronous API.
Erik Hellman
September 30, 2016
Transcript
73 slides
-
fork() #include <unistd.h> void start_new_app() { // return process
id in parent // and 0 in new child pid_t new_pid = fork(); if(new_pid == 0) { // Child process init here } else { // Parent registers new child here } } -
JAVA THREADS public void startNewThread() { Thread myThread = new
Thread(new Runnable() { @Override public void run() { // Do my thing! } }); myThread.start(); } -
POSIX THREADS #include <pthread.h> void *call_from_thread(void *) { //do
stuff return NULL; } JNIEXPORT void JNICALL Java_se_hellsoft_async_Native_start(JNIEnv * env, jobject obj) { pthread_t t; //Launch a thread pthread_create(&t, NULL, call_from_thread, NULL); } Pointer to function -
LINUX TASKS // Start new thread clone(CLONE_VM | CLONE_FS |
CLONE_FILES | CLONE_SIGHAND, 0); // Start new process clone(SIGCHLD, 0); -
MULTI-THREADING, CONCURRENCY AND ASYNC OPERATIONS App 1 Main Thread App
1 Main Thread App 1 Main Thread App 1 Main Thread -
MULTI-THREADING, CONCURRENCY AND ASYNC OPERATIONS App 1 Main Thread App
1 Main Thread App 1 Main Thread App 1 Main Thread Only multi-threading but no concurrency challenges for the developer! -
MULTI-THREADING Activity IntentService ContentProvider SQLite Database Worker Thread Main Thread
Concurrency handled by SQLite -
CONCURRENCY public class SharedData { private long id; private String
title; private String description; public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } public long getId() { return id; } public void setId(long id) { this.id = id; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } } Main Thread Worker Thread SharedData Write Read -
CONCURRENCY public class SharedData { private long id; private String
title; private String description; public synchronized String getDescription() { return description; } public synchronized void setDescription(String description) { this.description = description; } public synchronized long getId() { return id; } public synchronized void setId(long id) { this.id = id; } public synchronized String getTitle() { return title; } public synchronized void setTitle(String title) { this.title = title; } } Main Thread Worker Thread SharedData Write Read -
IMMUTABLE OBJECTS public class ImmutableSharedData { private long id; private
String title; private String description; private ImmutableSharedData(String description, long id, String title) { this.description = description; this.id = id; this.title = title; } public String getDescription() { return description; } public long getId() { return id; } public String getTitle() { return title; } public Builder newBuilder() { return new Builder().description(description) .id(id).title(title); } } public static class Builder { private String description = null; private long id = 0; private String title = null; public Builder description(String description) { this.description = description; return this; } public Builder id(long id) { this.id = id; return this; } public Builder title(String title) { this.title = title; return this; } public ImmutableSharedData build() { return new SharedData(description, id, title); } } -
CONCURRENCY public class DataManager { private ImmutableSharedData sharedData; private Set<Listener>
listeners; public DataManager() { listeners = Collections.synchronizedSet(new HashSet<>()); } public void updateData(ImmutableSharedData data) { sharedData = data; for (Listener listener : listeners) { listener.onDataChanged(sharedData); } } public void registerListener(Listener listener) { listener.onDataChanged(sharedData); listeners.add(listener); } public void unregisterListener(Listener listener) { listeners.remove(listener); } public interface Listener { void onDataChanged(ImmutableSharedData data); } } -
CONCURRENCY public class DataManager { private Handler notifier; private ImmutableSharedData
sharedData; private Set<Listener> listeners; public DataManager() { listeners = Collections.synchronizedSet(new HashSet<>()); notifier = new Handler(Looper.getMainLooper()); } public void updateData(ImmutableSharedData data) { sharedData = data; notifier.post(() -> { for (Listener listener : listeners) { listener.onDataChanged(sharedData); } }); } public void registerListener(Listener listener) { listener.onDataChanged(sharedData); listeners.add(listener); } public void unregisterListener(Listener listener) { listeners.remove(listener); } public interface Listener { void onDataChanged(ImmutableSharedData data); } } -
CONCURRENCY public class DataManager { private Handler notifier; private ImmutableSharedData
sharedData; private Set<Listener> listeners; public DataManager() { listeners = Collections.synchronizedSet(new HashSet<>()); notifier = new Handler(Looper.getMainLooper()); } public void updateData(ImmutableSharedData data) { sharedData = data; notifier.post(() -> { for (Listener listener : listeners) { listener.onDataChanged(sharedData); } }); } public void registerListener(Listener listener) { listener.onDataChanged(sharedData); listeners.add(listener); } public void unregisterListener(Listener listener) { listeners.remove(listener); } public interface Listener { void onDataChanged(ImmutableSharedData data); } } Getting complicated… -
THE ASYNC PROBLEM public static class NetworkCallFactory { private static
ExecutorService executor = Executors.newCachedThreadPool(); public static <T> void fetchData(String url, NetworkCallback<T> callback) { executor.submit(() -> { try { T result = syncNetworkCall(url); callback.onResult(result); } catch (Exception e) { callback.onError(e); } }); } } public interface NetworkCallback<T> { void onResult(T result); void onError(Throwable t); } } -
THE ASYNC PROBLEM protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); RecyclerView
productsList = (RecyclerView) findViewById(R.id.products); productsList.setLayoutManager(new LinearLayoutManager(this)); productsAdapter = new ProductsAdapter(); NetworkCallFactory.fetchData(DATA_URL, new NetworkCallFactory.NetworkCallback<List<Product>>() { @Override public void onResult(List<Product> result) { productsAdapter.setProducts(result); } @Override public void onError(Throwable t) { // TODO Error handling } }); } Ouch!! -
THE ASYNC PROBLEM protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); RecyclerView
productsList = (RecyclerView) findViewById(R.id.products); productsList.setLayoutManager(new LinearLayoutManager(this)); productsAdapter = new ProductsAdapter(); NetworkCallFactory.fetchData(DATA_URL, new NetworkCallFactory.NetworkCallback<List<Product>>() { @Override public void onResult(List<Product> result) { runOnUiThread(() -> productsAdapter.setProducts(result)); } @Override public void onError(Throwable t) { // TODO Error handling } }); } Might be too late! -
runOnUiThread() public final void runOnUiThread(Runnable action) { if (Thread.currentThread() !=
mUiThread) { mHandler.post(action); } else { action.run(); } } -
THE ASYNC PROBLEM NetworkCallFactory.fetchData(DATA_URL, new NetworkCallFactory.NetworkCallback<List<Product>>() { @Override public void
onResult(final List<Product> result) { runOnUiThread(() -> { if (!isDestroyed()) { productsAdapter.setProducts(result); } }); } @Override public void onError(Throwable t) { // TODO Error handling } }) -
THE ASYNC PROBLEM NetworkCallFactory.fetchData(DATA_URL, new CustomAsyncSample.NetworkCallback<List<Product>>() { @Override public void
onResult(final List<Product> result) { runOnUiThread(() -> { if (!isDestroyed()) { productsAdapter.setProducts(result); } }); } @Override public void onError(Throwable t) { // TODO Error handling } }) Getting complicated… -
THE ASYNC PROBLEM • Loading should be multi-thread aware (worker
+ main thread) • Callbacks go on the main thread • Loading should be possible to cancel -
SINGLE THREADED ASYNC PROBLEM private void showStartPage() { FragmentManager fragmentManager
= getSupportFragmentManager(); fragmentManager .beginTransaction() .replace(R.id.content, StartPageFragment.newInstance()) .commit(); } public static class StartPageFragment extends Fragment { public static StartPageFragment newInstance() { return new StartPageFragment(); } public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { return inflater.inflate(R.layout.start_page, container, false); } } -
SINGLE THREADED ASYNC PROBLEM public void showSplashAndContinue(ImageView imageView) { imageView.setImageResource(R.drawable.splash);
Handler handler = new Handler(); handler.postDelayed(() -> showStartPage(), 5000); } -
SINGLE THREADED ASYNC PROBLEM Fatal Exception: java.lang.IllegalStateException: Can not perform
this action after onSaveInstanceState at android.support.v4.app.FragmentManagerImpl.checkStateLoss(SourceFile:1493) at android.support.v4.app.FragmentManagerImpl.enqueueAction(SourceFile:1511) at android.support.v4.app.BackStackRecord.commitInternal(SourceFile:638) at android.support.v4.app.BackStackRecord.commit(SourceFile:617) -
4 SIMPLE RULES • Start job • Receive result •
Cancel job • Cancel result delivery -
Cancelling a Thread Thread myThread = new Thread() { @Override
public void run() { try(InputStream stream = new FileInputStream("/dev/urandom")) { byte[] buffer = new byte[32]; while(stream.read(buffer) != -1) { Log.d(TAG, "Read some data!"); } } catch (Throwable t) { Log.e(TAG, "Error when reading!", t); } } }; myThread.start(); SystemClock.sleep(1000); myThread.interrupt(); -
–The Java™ Tutorials “An interrupt is an indication to a
thread that it should stop what it is doing and do something else. It's up to the programmer to decide exactly how a thread responds to an interrupt, but it is very common for the thread to terminate.” https://docs.oracle.com/javase/tutorial/essential/concurrency/interrupt.html -
–The Java™ Tutorials “An interrupt is an indication to a
thread that it should stop what it is doing and do something else. It's up to the programmer to decide exactly how a thread responds to an interrupt, but it is very common for the thread to terminate.” https://docs.oracle.com/javase/tutorial/essential/concurrency/interrupt.html -
Cancelling a Thread Thread myThread = new Thread() { @Override
public void run() { try(InputStream stream = new FileInputStream("/dev/urandom")) { byte[] buffer = new byte[32]; while(stream.read(buffer) != -1) { Log.d(TAG, "Read some data!"); if(interrupted()) { Log.d(TAG, "Got interrupted - cancel reading!"); return; } } } catch (IOException e) { Log.e(TAG, "Error when reading!", e); } } }; myThread.start(); SystemClock.sleep(1000); myThread.interrupt(); -
java.lang.Thread • Can be started only once • Limited support
for cancelling • No built-in support for callbacks on other threads • Avoid! -
MessageQueue (simplified) public final class MessageQueue { Message mMessages; Message
next() { synchronized (this) { Message prevMsg = null; Message msg = mMessages; do { prevMsg = msg; msg = msg.next; } while (msg != null && !msg.isAsynchronous()); if (msg != null) { if (now < msg.when) { nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE); } else { mBlocked = false; if (prevMsg != null) { prevMsg.next = msg.next; } else { mMessages = msg.next; } msg.next = null; return msg; } } else { nextPollTimeoutMillis = -1; } } } -
Looper (simplified) public final class Looper { final MessageQueue mQueue;
final Thread mThread; private Looper(boolean quitAllowed) { mQueue = new MessageQueue(quitAllowed); mThread = Thread.currentThread(); } public static void loop() { final Looper me = myLooper(); final MessageQueue queue = me.mQueue; for (;;) { Message msg = queue.next(); // might block msg.target.dispatchMessage(msg); } } -
HandlerThread private Handler createBackgroundHandler(Handler.Callback callback) { HandlerThread handlerThread = new
HandlerThread("bg-thread"); handlerThread.start(); return new Handler(handlerThread.getLooper(), callback); } -
Timeouts with Handler @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState);
RecyclerView productsList = (RecyclerView) findViewById(R.id.products); productsList.setLayoutManager(new LinearLayoutManager(this)); productsAdapter = new ProductsAdapter(); uiHandler = new Handler(Looper.getMainLooper(), this); swipeRefreshLayout = (SwipeRefreshLayout) findViewById(R.id.swipe_refresh_layout); swipeRefreshLayout.setOnRefreshListener(this::performNetworkSync); } -
Timeouts with Handler private void performNetworkSync() { swipeRefreshLayout.setRefreshing(true); uiHandler.sendEmptyMessageDelayed(MSG_TIMEOUT, REFRESH_TIMEOUT_MS)
syncNetworkData(new NetworkCallback() { public void onData(List<Product> data) { uiHandler.obtainMessage(MSG_PRODUCTS_UPDATED, data) .sendToTarget(); uiHandler.removeMessages(MSG_TIMEOUT); } }); } -
Timeouts with Handler @Override public boolean handleMessage(Message msg) { switch
(msg.what) { case MSG_TIMEOUT: swipeRefreshLayout.setRefreshing(false); Snackbar.make(findViewById(R.id.content), "Timeout when loading data", Snackbar.LENGTH_LONG) .setAction("Retry", v -> performNetworkSync()) .show(); break; case MSG_PRODUCTS_UPDATED: swipeRefreshLayout.setRefreshing(false); productsAdapter.setProducts((List<Product>) msg.obj); break; } return true; } -
Cancelling with Handler // Remove all messages with what ==
123 handler.removeMessages(123); // Remove messages with what 123 and carrying someObject handler.removeMessages(123, someObject); // Remove message containing this runnable handler.removeCallbacks(runnable); // Remove message containing this runnable and carrying someObject handler.removeCallbacks(runnable, someObject); // Remove all messages handler.removeCallbacksAndMessages(null); -
android.os.Handler • Message-based async utility • Thread safe • Reusable
• Cancel scheduled messages • Still needs manual cancel flag • Useful for repeated processing tasks -
android.os.AsyncTask private Result postResult(Result result) { Message message = getHandler().obtainMessage(MESSAGE_POST_RESULT,
new AsyncTaskResult<Result>(this, result)); message.sendToTarget(); return result; } private static Handler getHandler() { synchronized (AsyncTask.class) { if (sHandler == null) { sHandler = new InternalHandler(); } return sHandler; } } -
android.os.AsyncTask private static class InternalHandler extends Handler { public InternalHandler()
{ super(Looper.getMainLooper()); } public void handleMessage(Message msg) { AsyncTaskResult<?> result = (AsyncTaskResult<?>) msg.obj; switch (msg.what) { case MESSAGE_POST_RESULT: // There is only one result result.mTask.finish(result.mData[0]); break; case MESSAGE_POST_PROGRESS: result.mTask.onProgressUpdate(result.mData); break; } } } -
android.os.AsyncTask private void finish(Result result) { if (isCancelled()) { onCancelled(result);
} else { onPostExecute(result); } mStatus = Status.FINISHED; } public final boolean cancel(boolean mayInterruptIfRunning) { mCancelled.set(true); return mFuture.cancel(mayInterruptIfRunning); } -
android.os.AsyncTask protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_async_task_sample); myTask
= new AsyncTask<Void, Void, String>() { protected String doInBackground(Void... params) { SystemClock.sleep(5000); return "Result!"; } protected void onPostExecute(String result) { super.onPostExecute(result); ((TextView) findViewById(R.id.result)).setText(result); } protected void onCancelled() { super.onCancelled(); ((TextView) findViewById(R.id.result)).setText("Cancelled!"); } }; } -
android.os.AsyncTask protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_async_task_sample); myTask =
new AsyncTask<Void, Void, String>() { protected String doInBackground(Void... params) { SystemClock.sleep(5000); return "Result!"; } protected void onPostExecute(String result) { super.onPostExecute(result); ((TextView) findViewById(R.id.result)).setText(result); } protected void onCancelled() { super.onCancelled(); ((TextView) findViewById(R.id.result)).setText("Cancelled!"); } }; } Don’t try to modify the UI in onCancel()! -
AsyncTask with Progress public class AsyncTaskWithProgress extends AsyncTask<String, AsyncTaskWithProgress.State,
String> { public enum State { Started, Loading, Updating, Completed, Failed } protected String doInBackground(String... params) { try { publishProgress(State.Started); SystemClock.sleep(1000); publishProgress(State.Loading); SystemClock.sleep(1000); publishProgress(State.Updating); SystemClock.sleep(1000); publishProgress(State.Completed); return "Done!"; } catch (Exception e) { publishProgress(State.Failed); return "Failed!"; } } } -
android.os.AsyncTask • Can only be started once • Callbacks to
the main-thread • Avoid touching UI in onCancelled()! • Be vary of Context/Activity references - use cancel()! • Use only for short-lived calls (seconds)! -
Loaders public static class DataListLoader extends AsyncTaskLoader<List<Data>> { private List<Data>
loadedData; public DataListLoader(Context context) { super(context); } protected void onStartLoading() { super.onStartLoading(); if(loadedData == null) { forceLoad(); } else { deliverResult(loadedData); } } -
Loaders public List<Data> loadInBackground() { List<Data> newData = new ArrayList<>();
SystemClock.sleep(2000); return newData; } public void deliverResult(List<Data> data) { loadedData = data; super.deliverResult(data); } } -
Loaders • Not multi-threaded by default • Strange start behaviour
• Main advantage: Possible to maintain data between config changes • Not suitable for network calls! -
IntentServiceSample public class IntentServiceSample extends IntentService { public IntentServiceSample() {
super(“name"); } protected void onHandleIntent(Intent intent) { try { ProductResource productResource = RetrofitHelper.getProductResource(); // Synchronous call since we're on a background thread Response<List<Product>> response = productResource.getProducts().execute(); List<Product> products = response.body(); writeListToDataBase(products); } catch (IOException e) { // Error handling } } private void writeListToDataBase(List<Product> products) { // Insert into database, synchronously... } } -
IntentService (simplified) public abstract class IntentService extends Service { private
final class ServiceHandler extends Handler { public ServiceHandler(Looper looper) { super(looper); } public void handleMessage(Message msg) { onHandleIntent((Intent)msg.obj); stopSelf(msg.arg1); } } public void onCreate() { super.onCreate(); HandlerThread thread = new HandlerThread("IntentService[" + mName + "]"); thread.start(); mServiceLooper = thread.getLooper(); mServiceHandler = new ServiceHandler(mServiceLooper); } public void onStart(@Nullable Intent intent, int startId) { Message msg = mServiceHandler.obtainMessage(); msg.arg1 = startId; msg.obj = intent; mServiceHandler.sendMessage(msg); } } -
IntentService • Single queue of background tasks • For long-running
background calls (network synchronization etc.) • Callback is complicated - avoid! • Useful for fire-and-forget tasks • Not possible to cancel -
SUPPORT ANNOTATIONS • @MainThread - Method must only be called
from main thread • @WorkerThread - Method must not be called from main thread • @AnyThread - Method can be called from any thread -
4 SIMPLE RULES + 1 • Start job • Receive
result • Cancel job • Cancel result delivery • Don’t re-invent the wheel!