[Android] 핸들러, 스레드, 병렬처리, 동기화 왜 필요할까
안드로이드를 무작정 개발하다 보면, 여러 스레드들이 충돌이 일어나 원하지 않는 결과가 나오거나 모듈의 순서가 맞지 않는 경우가 종종 있는데요. 스레드는 프로세스 안에서 돌아가는 코드와 별개로 병렬로 실행되며 여러개의 스레드가 존재할 수도 있습니다.
"동기화"란, 여러 스레드가 같은 프로세스 안에서 서로 자원을 공유하기 때문에 영향을 주게 되는 것을 말하는데
만약 어떤 스레드가 처리하고 있던 내용을 중간에 다른 스레드가 처리하게 되면 충돌이 발생하거나 원치 않은 결과를 받을 수 있습니다. 이러한 현상을 스레드 간섭이라고 합니다.
그래서 오늘은
안드로이드 Handler, Thread, Looper, AsyncTask, Join, Syncronized
등 동기화에 관한 개념을 총 정리하도록 하겠습니다.
○ 안드로이드 Thread
- Thread
- Main Thread
안드로이드의 UI작업(화면 구성에 관한 기능)은 워커 스레드가 아닌 메인 스레드에서 하게 되는데요. UI작업을 메인 스레드에서 하게 된다면, 동기화 문제가 생기게 됩니다. 안드로이드는 이러한 문제 발생을 예방하기 위해 병렬로 동작하는 메인 스레드와 워커 스레드 사이에 핸들러를 두고 UI작업은 모두 메인 스레드로 전달하도록 한 것입니다.
따라서 작업이 오래걸리는 작업은 메인스레드가 아닌 워커스레드에서(@WorkerThread)에서 하도록 지정해주어야 합니다.
- Worker Thread
따라서 작업이 오래걸리는 작업은 메인스레드가 아닌 워커스레드에서(@WorkerThread)에서 하도록 지정해주어야 합니다.
@WorkerThread
public void load(){
loadModel();
loadDictionary();
loadLabels();
}
○ 안드로이드 Handler
- Handler
핸들러는 핸들러 객체를 만든 스레드와 해당 스레드의 메세지 큐(Message Queue)에 바인딩 됩니다. 여기서 메세지 큐는 핸들러가 전달하는 메세지를 보관하는 큐 입니다. 다른 스레드에 메세지를 전달하려면 수신 대상 스레드에서 핸들러의 post(handler.post())나 sendMessage등의 함수를 이용해야 합니다.
- Looper
메세지 큐에 저장된 message나 runnable은 Looper가 차례로 꺼내(First In First Out) 핸들러로 전달합니다. 핸들러가 메시지 큐에 넣은 것을 다시 꺼내 핸들러로 전달하는 이유는 message나 runnable을 처리하기 위해서입니다.
// 루퍼 생성
Thread t = new Thread(new Runnable(){
@Override
public void run() {
Looper.prepare();
handler = new Handler();
Looper.loop();
}
}); t.start();
이처럼, 루퍼를 생성해서 사용할 수도 있지만 안드로이드는 편리성 제공을 위해 핸들러를 기본 생성자를 통해 루퍼 없이 사용할 수 있게 해줍니다. 기본 생성자를 통해 핸들러를 생성하면 생성되는 핸들러는 해당 핸들러를 호출한 스레드의 메세지 큐와 루퍼에 자동 연결됩니다.
// 기본 생성자를 통한 루퍼 생성
public class MainActivity extends AppCompatActivity {
Handler mHandler = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mHandler = new Handler();
Thread t = new Thread(new Runnable(){
@Override public void run() {
// UI 작업 수행 X
mHandler.post(new Runnable(){
@Override
public void run() {
// UI 작업 수행 O
}
});
}
});
t.start();
}
}
"핸들러를 생성하는 스레드만이 다른 스레드가 전송하는 Message와 Runnable객체를 받을 수 있다."
- 주요 핸들러 함수
- Handler.sendMessage(Message msg)
: Message 객체를 메세지 큐에 전달하는 함수
- Handler.sendEmptyMessage(int what)
: 메세지의 what필드를 전달하는 함수
- Handler.post(new Runnable())
: Runnable 객체를 메세지 큐에 전달하는 함수, 이 Runnable객체는 해당 핸들러가 연결된 스레드에서 실행된다.
new Thread(new Runnable() {
@Override
public void run() {
Looper.prepare()
MyHandler workerHandler;
workerHandler = new MyHandler();
workerHandler.post(new Runnable() {
@Override
public void run() {
tv.setText("~~"); /// UI 작업 불가능
}
});
Looper.loop();
}
}).start();
다음 예시 코드에서 메인 스레드가 아닌 워커 스레드 내부에서 핸들러를 생성하였는데, 이 스레드를 통해 전달된 Runnable 객체는 워커스레드의 메세지 큐에 저장됩니다. 따라서 워커스레드의 루퍼가 run()을 실행한다. 하지만 run()내부에서 처리하려는 작업이 UI작업이기 때문에 에러가 발생하게 됩니다.
○ 안드로이드 Join
Thread.join() 은 스레드가 멈출 때까지 기다렸다가 진행할 수 있도록 해주는 함수입니다.
public class ThreadJoin {
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName() + "start");
Runnable r = new MyRunnable();
Thread thread = new Thread(r);
thread.start();
try {
thread.join();
} catch (Exception e) {
// TODO: handle exception
}
System.out.println(Thread.currentThread().getName() + "end");
}
}
class MyRunnable implements Runnable{
@Override
public void run() {
// TODO Auto-generated method stub
System.out.println("쓰레드1단계");
thread2();
}
public void thread2() {
System.out.println("쓰레드2단계");
thread3();
}
public void thread3() {
System.out.println("쓰레드3단계");
}
}
○ 안드로이드 Synchronized
Synchronized에는 method에 적용시키는 synchronized method와 객체에 적용시키는 synchronized block 가 있습니다. 메소드나 객체를 lock시킨 후 lock을 가진 스레드만 접근할 수 있도록 해주는 방법입니다.
public class SynchThread extends Thread {
int total = 0;
@Override
public void run() {
synchronized (this) { // 해당 객체(this)에 Lock 이 걸린다.
for (int i = 0; i < 5; i++) {
System.out.println(i + "를 더한다.");
total += i;
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
notify(); // 위 작업이 모두 끝나면 notify()를 호출하여 다른 쓰레드를 실행 대기 상태로 만든다.
}
}
}
public class SyncMainThread {
public static void main(String[] args) {
SynchThread syncThread = new SynchThread();
syncThread.start();
synchronized (syncThread) {
System.out.println("syncThread 가 완료될 때까지 기다린다.");
try {
syncThread.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Total Sum : " + syncThread.total);
}
}
}
○ 안드로이드 Atomic Objects
대부분 스레드 동기화는 synchronized를 사용하지만 너무 느리다는 단점이 있습니다. 그렇게 나온 Atomic변수 하지만 속도보다 더 큰 이유는 스레드 안정성 향상 연산에 있다고 합니다.
○ 안드로이드 AsyncTask
- AsyncTask
안드로이드에서 스레드나 메세지 루프 등의 원리를 이해하지 않아도 하나의 클래스에서 UI작업을 쉽게 할 수 있게 해주는 클래스입니다.
- execute( ) 명령어를 통해 AsyncTask을 실행합니다.
- AsyncTask로 백그라운드 작업을 실행하기 전에 onPreExcuted( )실행됩니다. 이 부분에는 이미지 로딩 작업이라면 로딩 중 이미지를 띄워 놓기 등, 스레드 작업 이전에 수행할 동작을 구현합니다.
- 새로 만든 스레드에서 백그라운드 작업을 수행합니다. execute( ) 메소드를 호출할 때 사용된 파라미터를 전달 받습니다.
- doInBackground( ) 에서 중간 중간 진행 상태를 UI에 업데이트 하도록 하려면 publishProgress( ) 메소드를 호출 합니다.
- onProgressUpdate( ) 메소드는 publishProgress( )가 호출 될 때 마다 자동으로 호출됩니다.
- doInBackground( ) 메소드에서 작업이 끝나면 onPostExcuted( ) 로 결과 파라미터를 리턴하면서 그 리턴값을 통해 스레드 작업이 끝났을 때의 동작을 구현합니다.
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
// 버튼을 클릭하면 파일 다운로드 경로를 파라미터로 AsyncTask 실행
public void OnClick(View view) {
switch (view.getId()) {
case R.id.button:
try {
new DownloadFilesTask().execute(new URL("파일 다운로드 경로1"));
} catch (MalformedURLException e) {
e.printStackTrace();
}
break;
}
}
private class DownloadFilesTask extends AsyncTask {
@Override
protected void onPreExecute() {
super.onPreExecute();
}
@Override
protected Long doInBackground(URL... urls) {
// 전달된 URL 사용 작업
return total;
}
@Override
protected void onProgressUpdate(Integer... progress) {
// 파일 다운로드 퍼센티지 표시 작업
}
@Override
protected void onPostExecute(Long result) {
// doInBackground 에서 받아온 total 값 사용 장소
}
}
}
References
핸들러 사용법 : https://itmining.tistory.com/16
https://itmining.tistory.com/5?category=640759
https://brunch.co.kr/@mystoryg/84