Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Multi-Threading, Concurrency and Async on Android - DroidCon NYC 2016

Erik Hellman
September 30, 2016

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
Tweet

More Decks by Erik Hellman

Other Decks in Programming

Transcript

  1. 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
 }
 }
  2. JAVA THREADS public void startNewThread() {
 Thread myThread = new

    Thread(new Runnable() {
 @Override
 public void run() {
 // Do my thing!
 }
 }); 
 myThread.start();
 }
  3. 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
  4. LINUX TASKS // Start new thread
 clone(CLONE_VM | CLONE_FS |

    CLONE_FILES | CLONE_SIGHAND, 0);
 
 // Start new process
 clone(SIGCHLD, 0);
  5. MULTI-THREADING, CONCURRENCY AND ASYNC OPERATIONS App 1 Main Thread App

    1 Main Thread App 1 Main Thread App 1 Main Thread
  6. 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!
  7. 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
  8. 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
  9. 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);
 }
 }
  10. 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);
 }
 }
  11. 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);
 }
 }
  12. 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…
  13. 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);
 }
 }
  14. 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!!
  15. 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!
  16. runOnUiThread() public final void runOnUiThread(Runnable action) {
 if (Thread.currentThread() !=

    mUiThread) {
 mHandler.post(action);
 } else {
 action.run();
 }
 }
  17. 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
 }
 })
  18. 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…
  19. THE ASYNC PROBLEM • Loading should be multi-thread aware (worker

    + main thread) • Callbacks go on the main thread • Loading should be possible to cancel
  20. 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);
 }
 }
  21. SINGLE THREADED ASYNC PROBLEM public void showSplashAndContinue(ImageView imageView) {
 imageView.setImageResource(R.drawable.splash);


    Handler handler = new Handler();
 handler.postDelayed(() -> showStartPage(), 5000);
 }
  22. 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)
  23. 4 SIMPLE RULES • Start job • Receive result •

    Cancel job • Cancel result delivery
  24. 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();
  25. –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
  26. –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
  27. 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();
  28. java.lang.Thread • Can be started only once • Limited support

    for cancelling • No built-in support for callbacks on other threads • Avoid!
  29. 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;
 }
 }
 }

  30. 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);
 } }
  31. HandlerThread private Handler createBackgroundHandler(Handler.Callback callback) {
 HandlerThread handlerThread = new

    HandlerThread("bg-thread");
 handlerThread.start();
 return new Handler(handlerThread.getLooper(), callback);
 }
  32. 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);
 }
  33. 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);
 }
 }); }
  34. 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;
 }
  35. 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);
  36. android.os.Handler • Message-based async utility • Thread safe • Reusable

    • Cancel scheduled messages • Still needs manual cancel flag • Useful for repeated processing tasks
  37. 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;
 }
 }
  38. 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;
 }
 }
 }
  39. 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);
 }
  40. 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!");
 }
 };
 }
  41. 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()!
  42. 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!";
 }
 }
 }
  43. 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)!
  44. 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);
 }
 }
  45. 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);
 }
 }
  46. Loaders • Not multi-threaded by default • Strange start behaviour

    • Main advantage: Possible to maintain data between config changes • Not suitable for network calls!
  47. 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...
 }
 }
  48. 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);
 } }
  49. 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
  50. 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
  51. 4 SIMPLE RULES + 1 • Start job • Receive

    result • Cancel job • Cancel result delivery • Don’t re-invent the wheel!