satur9nine
satur9nine

Reputation: 15042

Binder thread interrupt unexpected behavior

Android ContentProviders use a pool of "binder threads" to process RPCs such as query, insert and call. In my implementation of a ContentProvider I am performing a long running task that is interruptible using Thread#interrupt(). I wrote a watchdog thread that interrupts the currently executing binder thread under certain circumstances. The interrupt is correctly taking effect but now I noticed that binder thread still has its interrupt flag set when it is used again by android to handle a new RPC. This causes the new RPC to behave as if it was interrupted. I did not expect this. I catch InterruptedException but this can happen if the watchdog thread sets the interrupted flag towards the end of the RPC when there is nothing looking at or clearing the flag.

Some ad-hoc testing of ExecutorService indicates that when it is reusing threads to handle tasks it resets the interrupt flag sometime before running each task. I found this comment in the ThreadPoolExecutor class:

 * 2. Before running any task, the lock is acquired to prevent
 * other pool interrupts while the task is executing, and
 * clearInterruptsForTaskRun called to ensure that unless pool is
 * stopping, this thread does not have its interrupt set.

To correct android's behavior I am thinking that at the beginning of each ContentProvider RPC I clear the interrupt flag with Thread.interrupted() but that doesn't seem like the most elegant solution.

Can anyone confirm if android binder threads are in fact handling thread interrupts differently from an ExecutorService, and what might be the best work-around so the interrupt flag isn't propagated between different RPCs?

Here is some code that can be placed in a ContentProvider to reproduce the issue:

@Override
public Bundle call(String method, String arg, Bundle extras) {
  Log.w(TAG, "Thread "+ Thread.currentThread().getName() + " before isInterrupted()=" + Thread.currentThread().isInterrupted());

  final Thread callThread = Thread.currentThread();

  if (method.equals("interrupt")) {
    new Thread("interrupter") {
      @Override
      public void run() {
        callThread.interrupt();
      }
    }.start();
  }

  try {
    Thread.sleep(500);
  } catch (InterruptedException e) {
    Log.w(TAG, e);
    Thread.currentThread().interrupt();
  }

  Log.w(TAG, "Thread "+ Thread.currentThread().getName() + " after isInterrupted()=" + Thread.currentThread().isInterrupted());

  return null;
}

Then in an Activity (make sure it is running in a different process from the ContentProvider!) hook up two buttons, one that does a regular call and one that does a call which will trigger an interrupt:

findViewById(R.id.call).setOnClickListener(new View.OnClickListener() {
  @Override
  public void onClick(View view) {
    getContentResolver().call(CallContentProvider.CONTENT_URI, "", "", null);
  }
});

findViewById(R.id.callint).setOnClickListener(new View.OnClickListener() {
  @Override
  public void onClick(View view) {
    getContentResolver().call(CallContentProvider.CONTENT_URI, "interrupt", "", null);
  }
});

Upvotes: 2

Views: 715

Answers (1)

satur9nine
satur9nine

Reputation: 15042

So here is my best stab so far at a work-around to this problem:

private volatile Thread mRunningCallThread;

@Override
public Bundle call(String method, String arg, Bundle extras) {
    Thread.interrupted(); // Clear interrupted flag

    mRunningCallThread = Thread.currentThread(); // Thread to interrupt

    // Do stuff, allowing interruption by another thread

    mRunningCallThread = null;

    Thread.interrupted(); // Clear interrupted flag
}

The field mRunningCallThread is a pointer to the currently running thread that I may interrupt.

Upvotes: 1

Related Questions