0. 동기(sync)와 비동기(async) 설명
동기적으로 태스크를 실행한 후, 다른 태스크를 실행하려면 먼저 실행된 태스크가 종료되기를 기다려야 합니다.
비동기적으로 태스크를 실행하면 먼저 실행된 태스크가 종료되기 전에 다른 태스크를 실행할 수 있습니다.
예를 들어 메인 스레드가 실행되는 중에, 다른 스레드를 백그라운드로 실행시켜 두고 계속 메인스레드는 자신의 작업을 하다가, 이 후 백그라운드에서 돌던 스레드가 종료시 결과값을 받을 수 있습니다.
0-1. AsyncTask 개념
앱이 실행되면 안드로이드 시스템은 메인 스레드를 생성합니다. 이 스레드는 안드로이드 UI 툴키트에 접근합니다. 사용자의 입력을 기다리거나 디바이스 화면에 그리는 작업등을 다룹니다. 그래서 UI 스레드라고도 부릅니다.
앱의 모든 컴포넌트(Activity, Service, Content Provider, BroadcastReceiver 등)들은 같은 스레드내에서 실행됩니다. 필요에 따라 추가 스레드를 생성할 수 있습니다. UI 스레드가 thread-safe하지 않기 때문에 스레드 사용시 다음 2가지를 지켜야 합니다.
-
UI 스레드가 블록(대기)되지 않도록 해야합니다.
-
UI 스레드 외에 다른 스레드에서 UI 컴포넌트 접근를 하면 안됩니다.
위에서 UI 스레드는 블록되면 안된다 했는데 문제는 안드로이드는 single thread model모델을 따르기 때문에 문제가 생깁니다. 만약 오랜 시간이 걸리는 작업을 UI 스레드에서 수행한다면 작업이 완료 될때 까지 UI 스레드가 대기해야 하므로 UI는 먹통이 됩니다. 예를 들어 다수의 파일들을 다운로드 받는 작업을 UI 스레드에서 수행하면 모든 파일의 다운로드가 완료될 때까지 UI는 반응이 없게 됩니다.
UI 반응성 향상과 처리시간이 오래 걸리는 작업 처리를 같이 해결하기 위해선 별도의 스레드가 필요합니다. 예를 들어 네트워크 관련 처리는 메인 스레드에서 수행하는게 금지되어 있습니다.
이 문제를 해결하기 위해 안드로이드에서는 Handler, Runnable, AsyncTask등을 제공합니다.
AsyncTask는 메인스레드에서 생성 후 실행되며, 메인 스레드에서 처리시간이 오래 걸리는 작업을 백그라운드 스레드로 넘기고
계속 메인 스레드 작업을 진행하기 위해 사용됩니다.
AsyncTask는 위에서 설명한 비동기 태스크로써 백그라운드 스레드라는 별도의 스레드에서 작업을 수행하기 때문에 AsyncTask를 실행시켜 놓고 메인 스레드는 다음 작업을 바로 할 수 있습니다.
백그라운드 스레드는 작업 처리 중 메인 스레드에서 처리하는 UI 작업에 영향을 주지 않기 때문에 UI가 늦게 뜨거나 터치에 늦게 반응하는 등의 일이 발생하지 않습니다.
AsyncTask를 사용하면 백그라운드 스레드와 메인 스레드간에 커뮤티케니션이 간단해집니다. 백그라운드 스레드에서 작업 종료 후, 결과를 메인 스레드에서 통보해 줄 수 있고(onPostExecute) , 또한 백그라운드 스레드에서 작업 중에도 메인 스레드에게 UI 처리 요청을 쉽게 할 수 있습니다.(onProgressUpdate )
0-2. 시간이 걸리는 작업은
수 초정도의 짧은 시간걸리는 작업에 대해서만 AsyncTask를 사용하도록 권장하고 있으며 그 이상 시간이 걸리는 작업에 대해서는
java.util.concurrent에 포함되어 있는 Executor, ThreadPoolExecutor, FutureTask등을 사용하라고 합니다.
그 이유는 몇가지가 있습니다.
문제 1. 메모리 릭
AsyncTask를 생성했던 Activity가 먼저 destroy되는 경우 메모리 릭이 발생할 수 있습니다. 왜냐면 Activity가 destory된다고 해서 AsyncTask도 같이 destory되지 않기 때문입니다. doInBackground() 메소드가 완료될때까지 AsyncTask는 실행 중 상태를 유지하게 됩니다. 완료 후, cancel 메소드 호출된 적이 있다면 onCancelled메소드가 실행되고 그렇지 않으면 onPostExecute 메소드가 호출됩니다.
이미 자신을 실행시켰던 Activity가 존재하지 않는데 AsyncTask가 UI처리같은 걸을 요구하면 메모리상에 존재하지 않는 것을 참조하게 되어 메모리릭이 발생합니다. 따라서 반드시 Activity 종료 전 AsyncTask를 cancel해주어 백그라운드 스레드에서 처리하던 작업이 종료되도록 해야 합니다.
문제 2. 디바이스의 화면 회전(orientation)
Activity에서 AsyncTask를 실행하고나서 사용자가 디바이스의 화면을 회전시키면 Activity를 destroy를 시키고 (onDestroy) 새로운 Activity 인스턴스가 생성됩니다.(onCreate). 이때 Activity의 모든 멤버변수의 인스턴스가 새로 생성됩니다.
Activity의 상태가 변할 때, AsyncTask에 대한 처리를 자동으로 해주지 않기 때문에, AsyncTask는 백그라운드 스레드에서 작업을 하고 있는 상태로 남아있게 됩니다. 작업이 완료될 때까지 AsyncTask는 종료되지 않습니다.
문제는 AsyncTask가 작업을 완료하고 Activity의 UI를 업데이트 하려 할때, 새로 생성된 Activity 인스턴스가 아닌 이미 destroy된 Activity 인스턴스에 접근하기 때문입니다. 따라서 디바이스 화면의 UI를 업데이트 할 수 없으며, 예외(IllegalArgumentException)가 발생합니다. 또한 기존 destroy된 Activity를 계속 참조하고 있기 때문에 garbage collector가 destory된 Activity를 수집할 수 없어 메모리릭이 발생할 수 있습니다.
1.
2.
3.
4.
- 결과 -
- 코드 -
public class MainActivity extends AppCompatActivity {
TextView textView;
Button executeButton, cancelButton;
ProgressBar progress;
BackgroundTask task;
int value;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
textView = (TextView) findViewById(R.id.textView);
progress = (ProgressBar) findViewById(R.id.progress);
executeButton = (Button) findViewById(R.id.executeButton);
cancelButton = (Button) findViewById(R.id.cancelButton);
executeButton.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
task = new BackgroundTask();
task.execute(100);
}
});
cancelButton.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
task.cancel(true);
}
});
}
class BackgroundTask extends AsyncTask<Integer , Integer , Integer> {
protected void onPreExecute() {
value = 0;
progress.setProgress(value);
}
protected Integer doInBackground(Integer ... values) {
while (isCancelled() == false) {
value++;
if (value >= 100) {
break;
} else {
publishProgress(value);
}
try {
Thread.sleep(100);
} catch (InterruptedException ex) {}
}
return value;
}
protected void onProgressUpdate(Integer ... values) {
progress.setProgress(values[0].intValue());
textView.setText("현재 진행 값 : " + values[0].toString());
}
protected void onPostExecute(Integer result) {
progress.setProgress(0);
textView.setText("완료되었습니다");
}
protected void onCancelled() {
progress.setProgress(0);
textView.setText("취소되었습니다");
}
}
}
5.
- 코드 설명1 -
- 코드 설명2 -
파라메터(매개변수) 타입 설명
6.
-참고-
doInBackground 메소드만 백그라운드 스레드에서 실행되며, 나머지는 메인 스레드에서 실행됩니다.
7. 작업 중 취소 처리
doInBackground 메소드에서 백그라운드 작업을 처리하는 중에 태스크는 cancel 메소드 호출에 의해서 취소될 수 있습니다. 그 결과 isCancelled 메소드가 true를 반환하게 되며 doInBackground 메소드 리턴 후, onPostExecute 메소드 대신 onCancelled메소드가 호출됩니다. 태스크 작업 취소 요청에 바로 반응하기 위해서는 doInBackground 메소드에서 주기적으로 isCancelled 메소드의 리턴값을 체크하고 있어야 합니다.
-참고-
- AsyncTask 클래스는 메인 스레드에서 생성 및 호출되어야 합니다. execute 메소드는 메인 스레드에서 실행되어야 합니다.
- AsyncTask를 생성 후, 실행은 한번만 가능합니다. 또 다시 호출하면 예외가 발생합니다.
- 직접 onPreExecute, onPostExecute, doInBackground, onProgressUpdate를 호출할 수 없습니다.
8-1. 참고
1) 참고로 한번 종료된 AsyncTask는 재사용이 불가능합니다.
다시 사용하고싶으면 해당 AsyncTask를 재생성 해서 사용하시면 됩니다.
2) 다수의 AsyncTask를 실행하면 doInBackground 메소드가 블록되는 상황이 발생합니다.
왜냐면 단일 스레드 상에서 실행되는 모든 AsyncTask는 순차적으로 실행되므로
이전 태스크가 종료되기를 기다려야 합니다.
AsyncTask는 기본적으로 하나씩 순차적으로 진행된다.(.execute)
따라서 대기하지 않고 바로 실행시켜주려면 별도의 스레드에서 실행해야합니다.
그리고 AsyncTask는 현재기준 병렬처리가 기본값이 아니어서
여러개의 AsyncTask를 동시에 실행할 수 가 없습니다.
(동시에 실행한 경우 먼저 실행한걸 끝내고 다음께 실행됨)
그래서,
이 문제를 해결하기 위해선 다음 메소드를 사용해야 합니다.
AsyncTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR)
이렇게 실행할 경우,
3~4개 정도만 동시 실행이 가능하다고 한다.
태스크들이 독립적이 스레드에서 병렬적으로 실행됩니다.
아래 링크를 보면 위 두가지 AsyncTsask 실행방법의 결과 차이를 보여주고 있습니다.
http://www.programing-style.com/android/android-api/android-asynctask-order-execute/
간단히 설명하자면 병렬실행을 위해서는
spell12AsyncTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR,"100");
이런식으로 excute대신 사용해주면 된다. 두번째 매개변수는 넘기는 값으로 excute 괄호안의 역활과 똑같다.
3) 그러나 이것도 최대 3~4개 정도밖에 병렬실행이 안된다.
만약 더 많은 스레드를 사용하고싶다면, (8개인 경우)
8-2. 8-1 예
AsyncTask는 기본적으로 하나씩 순차적으로 진행된다.(.execute)
이것을 동시(병렬)에 실행시키기 위해서는
AsyncTask.THREAD_POOL_EXECUTOR : AsyncTask들을 스레드 풀에서 동시(병렬) 처리
AsyncTask.SERIAL_EXECUTOR : 순차적 AsyncTask 처리
'■ Android > Tip' 카테고리의 다른 글
[JAVA][Android] 람다식 (0) | 2020.04.01 |
---|---|
[Android] Background 처리(1000) - runOnUiThread 사용법, 설명 (0) | 2020.03.23 |
[Android] Background 처리(103) - 루퍼(Looper), 핸들러, 스레드로 메시지 전송하기 (실습 코드) (0) | 2020.03.21 |
[Android] Background 처리(102) - Handler와 Looper의 필요성 (0) | 2020.03.20 |
[Android] Background 처리(101) - Runnable 객체 설명, 사용법 (0) | 2020.03.19 |