농농씨 2023. 10. 30. 19:14

KeyWord: 프로그램, 프로세스, 스레드, 멀티프로세스, 멀티스레드

 

프로그램이란?

컴퓨터에서 실행 가능한 명령어들의 집합으로, 특정 작업을 수행하기 위한 코드와 데이터의 모음

쉽게말해, 작성한 코드가 디스크에 저장된 형태로 존재함.

물리적인 공간인 디스크에 저장된 프로그램이, 논리적인 공간인 메모리에 올라오고, 

리소스 할당과 관리를 운영체제가 담당한다.

그리고 이렇게 메모리에 올라와서(적재되어) 실행중인 프로그램프로세스라 한다.

 

프로그램: 작성한 코드

프로세스: 운영체제에 의해 메모리에 적재되어 실행중인 프로그램

 

프로세서(cpu 라고도 함): 프로세스나 스레드같은 작업을 처리하는 하드웨어

 

프로세스는 자신의 코드 시작점부터 종료지점까지 순차적인 실행 흐름을 가짐

ex. Main 함수부터 실행.

그런데 하나의 흐름만으로는 동작을 처리하기 힘들 때가 있다. 

ex. 네트워크에서 데이터 받아오면서 화면의 내용 변경

스레드란? 하나의 프로세스 내에서 실행되는 각각의 독립적인 실행흐름

멀티스레드 프로그래밍이란? 하나의 프로세스 내에서 두 개 이상의 스레드가 동작하도록 하는 것

멀티 프로세서

: 두 개 이상의 프로세서(cpu)들이 하나의 작업을 병렬적으로 처리하는 것

(작업은... 프로세스?)

멀티스레드는 작업이 여러개라는 뜻

멀티프로세서는 한 작업을 여러 프로세서(cpu)가 처리한다는 것

 

안드로이드 플랫폼은 여러 프로세스를 가질 수 있는 멀티 프로세서 플랫폼이다.

하나의 앱 안에서 내부적으로 여러 가지 일을 동시에 하는 경우가 생기기 때문에

그것을 처리하기 위해 멀티 프로세스나 멀티 스레드를 다룸.

 

멀티프로세스 환경 예시

각각의 프로세스마다 Stack, Heap, Data, Text 영역을 독립적으로 가지고 있음.

이런 멀티프로세스 환경에서는 독립된 구조를 통해 프로그램 전체의 안전성을 확보할 수 있음.

하지만 단점으로는 Context의 Switch 에 드는 비용이 높다는 점이 있다.

Context Switching이란?

프로세스나 스레드를 동시에 실행하는 것처럼 보이게 하는 멀티태스킹의 한 방법.

cpu는 한 번에 하나의 Task만 처리할 수밖에 없음.

만약 Context Switching이 없다면 사용자는 하나의 Task가 끝날 때까지 기다려야 함.

따라서 사용자 입장에서 다양한 기능을 동시에 처리하는 것처럼 보이게 하기 위해

cpu는 빠른 속도로 task를 바꿔가며 실행함. 이것을 Context Switching이라 함.

 

멀티 프로세스 환경에서는 Context Switching에 드는 비용이 높다. 

위의 사진처럼 '독립적인 영역'을 갖고 있기 때문에 설정할 것들이 많기 때문이다.

 

그렇다면 멀티 스레드 환경에서는 어떨까?

스레드끼리는 운영체제에서 할당받은 프로세스의 메모리를 공유한다.

스레드끼리 Code, Data, Heap 영역은 공유하고 Stack 영역은 다르게 가진다.

Stack 영역만 다르게 가지는 이유는 Stack의 LIFO(Last Input, First Out: 후입선출, 나중에 넣은게 제일 먼저 나옴) 특성 때문이다.

따라서 스레드끼리 Stack을 공유하게 되면 실행 흐름이 섞이고 복잡해질 가능성이 있기 때문이다.

즉, 멀티스레드 환경은 멀티프로세스 환경에 비해 Context Switching에 드는 비용이 적기 때문에 유용하게 사용할 수 있다.

 

그러면 멀티 스레드는 무조건 좋은 것인가?

답은 No.

1. 안전성이나 동기화 관련 문제가 있고, 

2. 스레드를 무한생성할 수 있는 것도 아니기 때문에 

멀티프로세스와 멀티스레드는 각각의 장단점이 있다.

 

 

 

안드로이드 환경에서 앱이 실행되면 어떤 일이 일어나는지 확인해보자.

일반적인 다른 프로그래밍 함수들의 경우 Main 함수에서 시작한다.

하지만 안드로이드의 경우 개발자가 직접 Main 함수를 구현하지 않아도 된다.

대신 앱에 포함된 Activity 중 하나를 Launcher로 지정함으로써 해당 액티비티를 앱의 시작점으로 삼을 수 있다.

Activity를 Launcher로 지정하는 방법은 위의 사진과 같이 AndroidManifest.xml에 정의되어 있다.

 

 

액티비티의 시작점으로 들어가게 되면 안드로이드는 Main UI Thread를 내부적으로 실행한다.

Main Thread 에서는 버튼 클릭을 감지하거나(Button Click), UI 상호작용(UI Interaction),

간단한 상호작용(Small Operation) 또는 수학적/논리적 연산(Mathematical Operation)과 같이 비교적 가벼운 연산들을 진행한다.

 

하지만 Application이 무거워지면 

ex. 서버에서 데이터를 받아와서 업데이트하는 작업이 필요한 경우

그런 작업들을 Main Thread에 올려서 작업하게 되면 

ex.  버튼 클릭 하나 했는데 엄청 많은(100만개 이상) 데이터를 받아와서 출력해야 한다고 가정해보자.

Main Thread에서는 UI 업데이트를 해야하는데..

(위 사진의 Worker Thread의) Network Operation 이나 DB 조회와 같이 무거운 작업을 계속 수행하면 작업 다될때까지 멈춰있는다.

 

결국, 좋은 사용자 경험을 위해 

Main Thread에서는 UI를 그리고, Worker Thread에서는 무거운 작업들을 수행한 다음에

Main Thread에 수행 결과를 넘겨서 다시 업데이트 하는 방식으로 구현해야 한다. 

 

 

Thread는 크게 Main Thread와 Worker Thread로 나뉘는데,

이 사이에는 가장 큰 규칙이 있다.

-Worker Thread는 직접적으로 UI작업을 할 수 없으며, Main Thread의 UI 요소에 접근할 수도 없다.

-UI 작업은 메인 스레드에서만 직접적으로 가능하다.

그 이유는 Main Thread가 있고, Worker Thread를 두세개 더 생성해서 작업하다보면, 

Main Thread는 열심히 UI를 업데이트하고 Worker Thread도 UI에 업데이트할 정보를 가진다.

위 사진의 왼쪽 그림처럼 두 Thread가 한 UI를 동시에 바꾸려는 일이 생긴다.

따라서 Main Thread만 UI를 업데이트할 수 있다.

 

그래서 Worker Thread가 UI를 변경할 때는 Handler와 Looper라는 것을 사용한다.

정리하자면

-Worker Thread에서 생성된 데이터 결과를 Main Thread에 넘기고,

-이를 토대로 Main Thread가 UI 업데이트를 할 수 있도록 매개체 역할을 하는 것이 Handler, Looper

 

 

 

각각의 Thread 하나는 Looper와 Message Queue를 하나씩 가진다.

Message Queue에서는 Thread 사이의 정보들을 주고받을 수 있다.

 

기본적인 동작 과정을 먼저 알아보자.

Looper는 하나의 Thread만을 담당하면서 Message Queue를 감시한다.

Queue가 비어있을 때는 가만히 있다가, Message Queue에 어떤 데이터나 정보가 들어오면

Handler로 그 정보를 전달한다.

그러면 그 Handler는 하나의 Looper에 대해서만 매칭이 되어있고, 

Looper가 이벤트를 전달해줄 때마다 그 이벤트에 알맞은 일을 수행하게 된다.

 

 

Thread #1: Main Thread

Thread #2: Worker Thread

라고 가정

Thread 2도 Message Queue, Looper, Handler를 하나식 가지고 있는데 생략됨.

 

Thread에서 어떤 일을 해서 결과가 나왔는데, 그 결과를 가지고 UI를 업데이트해야하는 상황.

그러면 그 결과를 Main Thread의 Handler로 전달.

How? Main Thread의 Handler에 구현된 함수를 호출해서 메시지 보내기.

그러면 이 메시지는 Main Thread의 Message Queue로 들어감.

Main Thread의 Looper가 이 메시지를 Handler로 다시 전달

(Message Queue도 기본적인 자료구조가 Queue라서 선입선출로 동작함)

Handler는 전달받은 메시지들을 이용해 작업을 함.

 

Message Queue에서

message 객체: 정수값과 오브젝트를 다른 Thread로 보낼 때 사용함(ex. Worker Thread에서 결과를 알림)

runnable 객체: 실행 가능한 코드블럭을 다른 Thread에 보낼 때 사용함. 즉, Thread는 runnable에 있는 코드블럭을 실행함.

따라서 Message Queue는 Thread가 어떤 작업을 해야하는지, 혹은 다른 thread로부터 어떤 결과가 나왔는지에 대한 정보를 담고 있다.

 

+

노션+추가자료 정리