관리 메뉴

HAMA 블로그

iOS 와 안드로이드에서의 병렬 쓰레드 개발 (AsyncTask 와 GCD) 본문

아이폰 (IOS)

iOS 와 안드로이드에서의 병렬 쓰레드 개발 (AsyncTask 와 GCD)

[하마] 이승현 (wowlsh93@gmail.com) 2016. 11. 21. 19:23


현재 많은것을 하고 있기에 앱 개발을 해야한다는 부담감에 조금은 피곤함이 몰려온다. 웹디자인,웹프론트엔드,웹벡엔드,기획,데브옵스,클라우드에 마이크로서비스식 서버개발, 데이터 가시화, 데이터 분석등을 병렬적으로 한다는건 사실 그만큼 완성도가 떨어진다는걸 뜻한다. 제품의 품질은 그것을 요구하는 상황에 따라서 다르며 그것에 의존되어 개인이 다루어야할 기술 범위 또한  달라지는데 현재는 뭐 한사람이 다 해도 상관없는 상태이긴 하지만.....ㅠㅠ 빠른 시일내에 사업이 본궤도에 올라서 분야별 전문가가 존재하길 희망해본다.

자!! 이러한 긴박한 상황하에 안드로이드와 iOS 개발을 하게 되었다. 기간은 한달. 

먼저 안드로이드는 개발을 2주동안 했다. 기능은 인증/푸쉬/전광판/사용히스토리/지도/음성인식/설정 정도의 복잡하지 않은 것들로 전광판에 보여줄 IoT 정보는 15분 스케쥴링으로 서버에 요청하여 JSON으로 가지고 오는 방식이었다. 서버는  Akka TCP 서버라 Java 기본 소켓통신 (JAVA IO) 을 이용하여 한번 콜하고 바로 커넥션을 끊는 방식을 택했다. 마찬가지로 푸쉬도 구글에서 제공하는 푸쉬를 사용하지 않고 서비스에서 always 커넥션되어 직접 받도록 하였다. 

이제 iOS 를 개발해야하는데 자료는 Object-C 가 많았지만 개인적으로 Swift 가 맘에 들었기 때문에 이것으로 구현 할 생각이다. UI를 개발하기전에 일단 가장 중요하다고 생각되는 네트워킹 및 쓰레드에 관련된 기능을 확인하기 위해 찾아보았고 그것에 해당하는 안드로이드의 AsyncTask 와 그와 비슷하다고 생각되는 iOS 의  GCD 에 대해 말할 예정이다. 이미 눈치 채셨듯이 두개(iOS 와 안드로이드) 에 대해서 잘 알지는 못하며 나 스스로를 위한 정리쯤으로 남겨놓는 글임을 밝힌다. 틀린 정보가 있을 수 있다는 야그..즉 추후에 업데이트되거나 삭제될수도 있을거 같다..


이만 잡담을 마무리하고 본론으로 들어가보자.


1. 안드로이드  AsyncTask

처음에는  AsycnTask를 사용하진 않고 그냥 자바쓰레드를 만들어서 쓰레드안에서 서버와 커뮤니케이션을 한 후에 받은 데이터는 Handler 를 통해서 UI 업데이트를 했다. 그걸 다시 AsyncTask로 바꾸진 않았지만 앞으로 개발할땐 AsyncTask 를 사용할것 같다. 왜? 조금이라도 더 편하니깐. 

무엇이 편하냐?  일단 안드로이드에서는 Activity 라는 하나의 View/controller 가 있고 (iOS에서는 UIViewController ) 그것을 다루기위한 단 하나의 쓰레드가 있다. 쓰레드가 단 하나이기 때문에 시간이 좀 걸리는 기능 (서버와의 커넥션을 해서 데이터를 가져오는) 을 그 쓰레드에서 그냥 해버리면 UI가 버벅댈 수 있으니 따로 쓰레드를 만들어서 사용해야한다. 그때 그냥 일반적인 자바 쓰레드를 하나 띄워서 거기서 데이터를 받은 후에 그 데이터를 UI 에 업데이트해야하는데 그냥 해버리면 에러가 나며 (두개의 쓰레드가 동시에 하나의 구조에 접근하게 되면 문제가 생길거라는건  유츄가능하죠.) 그때 연결고리 역할을 할 Handler 라는것을 활용한다.

* 참고로 멀티쓰레드 패턴에 대해 공부하고 싶은 분은 이 글을 한번 읽어보시길 -> 생산자 - 소비자 패턴 

서브쓰레드에서 데이터를 메인쓰레드에게 자연스럽게 넘겨주는 방식인데 다음과 같이 코딩되는데

일반쓰레드 와 Handler 예)


public
class SearchActivity extends AppCompatActivity { UpdateHandler updatehandler; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_search); updatehandler = new UpdateHandler(); } public void OnClick(View view) { switch (view.getId()) { case R.id.button: try { ConnectThread cthread = new ConnectThread("192.168.1.x"); cthread.start(); } catch (MalformedURLException e) { e.printStackTrace(); } break; } } class ConnectThread extends Thread { String hostname; public ConnectThread(String addr) { this.hostname = addr; } public void run() { try { String result = SimpleSocketUtil.getGWLocation(....); Message msg = updatehandler.obtainMessage(); Bundle data = new Bundle(); data.putString(PKConst.connection, PKConst.connection_success); data.putString(PKConst.useStatus, result); msg.setData(data); updatehandler.sendMessage(msg); } catch(Exception ex) { ex.printStackTrace(); } } } public class UpdateHandler extends Handler { public void handleMessage(Message msg) { String is_connected = msg.getData().getString(PKConst.connection); String result = msg.getData().getString(PKConst.useStatus); if (is_connected.equals(PKConst.connection_success)){ onUpdateSuccess(result); } else { onConnectionFailed(); } } }

 SearchActivity 라는 메인 쓰레드와 cThread 서브쓰레드가 있으며 cThread 에서 서버측에서 데이터를 얻은후에 그 값을 SearchActivity 로 넘겨줄때 Handler 를 이용해서 넘겨주고 있다.

방식은

1. Message 객체를 만든다.
2. Bundle 객체를 만든다.
3. Bundle 객체에 넘겨줄 데이터 key, value 형식으로 담는다. 
4. Message 객체에 Bundle 를 담은 후에 
5. Handler 의 sendMessage 메소드를 통해 보내준다.
6. Handler 는 그 데이터를 받아서 UI 를 업데이트 해준다. (위 코드에서는 onUpdateSuccess 함수 내에서 업데이트) 

이다. 

근데 이 방식 말고도 다양하게 있는 듯 하고 . 더 자세히 알려면 다른 블로그를 참고하시라.

어쨋든 저렇게 하려면 쓰레드도 따로 만들어야하고 핸들러도 따로 만들어야하고 데이터를 메세지와 번들을 이용하여 따로 만들어서 보내줘야하는등 좀 할것들이 있어 보입니다. 이걸 더 간편하게 만들기 위한것이 AsyncTask  인데 

예를 봅시다.

AsyncTask 예 )


public class SearchActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

 
    public void OnClick(View view) {
        switch (view.getId()) {
            case R.id.button:
                try {
                    new ConnectServerTask().execute("192.168.1.x");
                } catch (Exception e) {
                    e.printStackTrace();
                }
                break;
        }
    }

    private class ConnectServerTask extends AsyncTask<String,String,String> {
        
        @Override
        protected void onPreExecute() {
            super.onPreExecute();
        }

        @Override
        protected String doInBackground(String... ip) {
            String result = SimpleSocketUtil.getGWLocation(ip[0]);
            return result;  // onPostExecute 인자로 넘어감
        }

        @Override
        protected void onProgressUpdate(String... progress) {
           // doInBackgournd 에서 publishProgress 호출하면 이리로옴
        }

        @Override
        protected void onPostExecute(String result) {   // UI 업데이트용 
          super.onPostExecute()
            String is_connected  = result.getData().getString(PKConst.connection);
            String status  = result.getData().getString(PKConst.useStatus);
            if( is_connected.equals(PKConst.connection_success)){
                onUpdateSuccess(status);
            }
            else {
                onConnectionFailed();
            }
        }
    }
} 

소스 설명   

1. AsyncTask 를 상속받은 클래스를 만든다.
2. AsyncTask 객체를 만들어서 execute 로 실행시킨다. (필요한 인자전달) 
3. doInBackgournd 에서 서버에 접속하여 데이터를 가져온 후에 리턴하면 onPostExecute 의 인자로 넘겨집니다.
4. onPostExecute 메소드에서 메인쓰레드의 UI 를 업데이트 시켜준다. 


위 코드에서 중요한것은 
onPreExecute  :   실행되기전에 처리해야할것들
doInBackground  : 서브쓰레드에서 실행되는 부분 (이외의 메소드는 모두 메인쓰레드에서 사용됨) 
onProgressUpdate :  서브쓰레드에서 실행되는 진행상태에 대해 처리하는 함수
onPostExecute  :  결과에 대해서 메인쓰레드에서 실행될 부분
이 밖에 onCancelled 등 이 있으나 생략함.


또한 AsyncTask의 <> 에 들어갈 3가지 파라미터 타입은 

첫번째인자 =>  doInBackground()의 인자타입이며 execute() 에서 넘겨줄 타입이다.
두번째인자 =>  onProgressUpdate() 의 인자타입
세번째인자=>  onPostExcute() 의 인자타입. 즉 doInBackgournd 의 리턴타입이자 onPostExecute 의 인자타입이다.



2. iOS GCD (Grand Central Dispatch) 

참고 : https://www.appcoda.com/ios-concurrency/ 


GCD 는 가장 일반적으로 사용되는 쓰레딩(NSThread)을 직접구현하지 않기 위한 비동기 매니져이며 C 로 구성되었고 iOS4 부터 지원한다고 한다. GCD 는 2가지 방식으로 비동기 업무에 대한 관리를 해주며 , 내부의 쓰레드풀에서 일감을 큐에서 가져와서 처리하는데 구체적으로 살펴보자.


- serial 큐 

 이름처럼 하나의 시간에 하나의 업무만 실행되는 큐이다. 여러개의 시리얼 큐를 만들면, 동시에 일을 처리하게 만들 수도 있다. 공유리소스에 차례대로 접근하기때문에 race condition 문제가 발생하지 않는다. 또한 a 다음에 b 를 처리 하기때문에 순서가 분명한 일에 적합하다.  GCD 에서 dispatch_get_main_queue 가 순차적큐이며 UI 작업은 메인큐에서만 해야한다. 

- concurrent 큐

하나의 큐에 넣은 업무들이 동시적으로 실행된다. 따라서 순서를 보장 받을 수 없다. a 와 b 를 시작시켰는데 
무엇이 먼저 끝날지 모른다. 


serial  및 concurrent 큐에 대해 설명했는데  이제는 어떻게 사용할 수 있는지 알아보자. 기본적으로 시스템은 각 응용 프로그램에 단일 직렬 대기열과 네 개의 동시 대기열을 제공한다. 주 디스패치 큐는 응용 프로그램의 주 스레드에서 작업을 실행하는 전역적으로 사용 가능한 serial 큐입니다. 앱 UI를 업데이트하고 UIView 업데이트와 관련된 모든 작업을 수행하는 데 사용된다. 한 번에 하나의 작업 만 실행되므로 메인 큐에서 무거운 작업을 실행할 때 UI의 움직임이 멈출 수 있다. 

메인 큐 외에 시스템은 4 개의 concurrent 큐를 제공하는데 그것들을 Global Dispatch 큐 라고 한다. 이 큐는 응용 프로그램에 대해 전역이며 우선 순위 수준에 의해서만 구분됩니다. 전역 concurrent 큐 중 하나를 사용하려면 첫 번째 매개 변수 인 다음 값 중 하나를 취하는 dispatch_get_global_queue 함수를 사용하여 원하는 대기열에 대한 참조를 가져와야합니다.

  • DISPATCH_QUEUE_PRIORITY_HIGH
  • DISPATCH_QUEUE_PRIORITY_DEFAULT
  • DISPATCH_QUEUE_PRIORITY_LOW
  • DISPATCH_QUEUE_PRIORITY_BACKGROUND

이러한 유형은 실행 우선 순위를 나타낸다. HIGH로 설정된  큐가  가장 높은 우선 순위를 가지며 BACKGROUND가 가장 우선 순위가 낮다.. 따라서 작업의 우선 순위에 따라 사용하는 큐를 결정할 수 있게된다. 마지막으로, 임의의 수의 직렬 또는 concurrent 큐를  작성할 수 있다. concurrent 큐의 경우 네 개의 글로벌 대기열 중 하나를 사용하는 것이 좋지만 직접 만들 수도 있다..

즉 대략 코드의 모형은 이렇다. 

let priority = DISPATCH_QUEUE_PRIORITY_DEFAULT dispatch_async(dispatch_get_global_queue(priority, 0)) { // 서버로 부터 데이터를 가져오는 코드 여기다 넣음 dispatch_async(dispatch_get_main_queue()) { // UI 를 업데이트함. } }


좀 더 구체적인 예를 가지고 말해보자.

Concurrent 큐 사용하기

이제 Xcode 프로젝트의 ViewController.swift 파일의  didClick 메서드에 이미지 다운로드를 처리한다고 하자. 

@IBAction func didClickOnStart(sender: AnyObject) {
    let img1 = Downloader.downloadImageWithURL(imageURLs[0])
    self.imageView1.image = img1
    
    let img2 = Downloader.downloadImageWithURL(imageURLs[1])
    self.imageView2.image = img2
    
    let img3 = Downloader.downloadImageWithURL(imageURLs[2])
    self.imageView3.image = img3
    
    let img4 = Downloader.downloadImageWithURL(imageURLs[3])
    self.imageView4.image = img4
    
}

각 다운로더는 하나의 작업으로 간주되며 모든 작업은 이제 메인 큐에서 수행된다. 메인큐는 알다시피 시리얼 큐이기때문에 망했다고 볼 수 있다. ;;

이제 기본 우선 순위 큐인 글로벌concurrent 큐중 하나에 대한 참조를 얻자.

let queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
        dispatch_async(queue) { () -> Void in
            
            let img1 = Downloader.downloadImageWithURL(imageURLs[0])
            dispatch_async(dispatch_get_main_queue(), {
                
                self.imageView1.image = img1
            })
            
        }

먼저 dispatch_get_global_queue를 사용하여 기본 concurrent 큐에 대한 참조를 가져온 다음 블록 내부에 첫 번째 이미지를 다운로드하는 작업을 제출한다. 이미지 다운로드가 완료되면 메인큐에 작업을 제출하여 다운로드 한 이미지로 업데이트 한다. 즉, 이미지 다운로드 작업을 백그라운드 스레드에 하고  메인 대기열에서 UI 관련 작업을 실행 한다.

이미지의 나머지 부분도 똑같이 하면 요렇게 되겠지.

@IBAction func didClickOnStart(sender: AnyObject) {
    
    let queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
    dispatch_async(queue) { () -> Void in
        
        let img1 = Downloader.downloadImageWithURL(imageURLs[0])
        dispatch_async(dispatch_get_main_queue(), {
            
            self.imageView1.image = img1
        })
        
    }
    dispatch_async(queue) { () -> Void in
        
        let img2 = Downloader.downloadImageWithURL(imageURLs[1])
        
        dispatch_async(dispatch_get_main_queue(), {
            
            self.imageView2.image = img2
        })
        
    }
    dispatch_async(queue) { () -> Void in
        
        let img3 = Downloader.downloadImageWithURL(imageURLs[2])
        
        dispatch_async(dispatch_get_main_queue(), {
            
            self.imageView3.image = img3
        })
        
    }
    dispatch_async(queue) { () -> Void in
        
        let img4 = Downloader.downloadImageWithURL(imageURLs[3])
        
        dispatch_async(dispatch_get_main_queue(), {
            
            self.imageView4.image = img4
        })
    }
    
}

네 개의 이미지 다운로드를  기본 제공되는 concurrent  에 제출한다.  이렇게 하면 이제 이미지를 다운로드하는 동안 버벅거림 없이 슬라이더를 드래그 할 수 있게 된다.

Serial 큐 사용하기 

지연 문제를 해결하기위한 또 다른 방법은 serial 큐를 사용하는 것 인데 이제 ViewController.swift 파일에서 동일한 didClickOnStart () 메서드로 돌아가서 이미지를 다운로드하기 위한 직렬 큐를 사용해보자. 직렬 큐ㄹ 사용할 때는 참조하는 직렬 큐에 세심한 주의를 기울여야 한다. 각 응용 프로그램에는 실제로 UI의 기본 대기열인 하나의 기본 직렬 큐가 있기때문에 직렬 큐를사용할 때 새로운 큐를  만들어야하며, 그렇지 않으면 앱이 UI 업데이트 작업을 실행하는 동안 작업을 같이 실행하게 된다. 이로 인해 다양한 오류 및 지연이 발생할 수 있다.
dispatch_queue_create 함수를 사용하여 새 큐를 만든 다음 이전과 같은 방식으로 모든 작업을 제출할 수 있다. 변경 후 코드를 살펴보자
@IBAction func didClickOnStart(sender: AnyObject) {
    
    let serialQueue = dispatch_queue_create("com.appcoda.imagesQueue", DISPATCH_QUEUE_SERIAL)
    
    
    dispatch_async(serialQueue) { () -> Void in
        
        let img1 = Downloader .downloadImageWithURL(imageURLs[0])
        dispatch_async(dispatch_get_main_queue(), {
            
            self.imageView1.image = img1
        })
        
    }
    dispatch_async(serialQueue) { () -> Void in
        
        let img2 = Downloader.downloadImageWithURL(imageURLs[1])
        
        dispatch_async(dispatch_get_main_queue(), {
            
            self.imageView2.image = img2
        })
        
    }
    dispatch_async(serialQueue) { () -> Void in
        
        let img3 = Downloader.downloadImageWithURL(imageURLs[2])
        
        dispatch_async(dispatch_get_main_queue(), {
            
            self.imageView3.image = img3
        })
        
    }
    dispatch_async(serialQueue) { () -> Void in
        
        let img4 = Downloader.downloadImageWithURL(imageURLs[3])
        
        dispatch_async(dispatch_get_main_queue(), {
            
            self.imageView4.image = img4
        })
    }
    
}
concurrent  큐와 다른 유일한 점은 직렬 큐 생성이다. 앱을 다시 빌드하고 실행하면 UI에 계속해서 상호 작용할 수 있도록 이미지가 백그라운드에서 다시 다운로드됨을 알 수 있다.

그러나 두 가지 사실을 알게 될 것인데 concurrent 큐의 경우와 비교하여 이미지를 다운로드하는 데 약간 시간이 걸립니다. 그 이유는 한 번에 하나의 이미지 만 로드하기 때문. 각 작업은 이전 작업이 완료되기 전에 대기한다.

이미지는 image1, image2, image3 및 image4 순서로 로드되며  이는 대기열이 한 번에 하나의 작업을 실행하는 시리얼로 작업되는 큐이기 때문이다.


스위프트 초보코너 

코드중에 ()-> Void in 으로 시작하는건 말이지. 스위프트에서 클로저가 저 모냥으로 생겼다. (여기서 클로저는 함수형프로그래밍에서 말하는 그 클로저를 말하는것은 아니다. 람다나 익명함수를 스위프트에서는 클로저로 지칭한다) 

즉  

func something() -> String {
   ... 뭔가 함..

}

이런 함수에서 함수명하고 앞에 func 를 뺀것이다. 그리고 in 을 붙히고 외곽에 {} 로 감싸면 클로저!

{ ()-> String in 

   .... 뭔가 함 ..

}


그리고 어떤 언어에서나 개발자가 사용 하기 편하게 만들어 놓은 희안하게 생긴것들이 있는데 그걸 Syntataic sugar 라고 한다. 위에서는 아래와 같은 코드가 그러한 것이다. 그냥 외워야한다. ;;;

dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
    dispatch_async(queue) {


3. Swift 3.0 에서의 GCD 

swift 2 에서 dispatch_get_global_queue 이렇게 길게 써야 했던게 
swift 3 에서는 DispatchQueue.global 식으로 간단히 바뀌었다.

예를 들면 )

swift 2.0 

let queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
dispatch_async(queue) { () -> Void in
        
    let img1 = Downloader.downloadImageWithURL(imageURLs[0])
    dispatch_async(dispatch_get_main_queue(), {
            
        self.imageView1.image = img1
    })
        
}
swift 3.0

 let queue = DispatchQueue.global()
       
 queue.async { () -> Void in
     let img1 = Downloader.downloadImageWithURL(imageURLs[0])
     DispatchQueue.main.async {
          self.imageView1.image = img1
     }
 }


4. GCD 를 이용한 서비스  

iOS 에는 안드로이드의 서비스 개념이 없는듯 하다. 먼가 계속 돌아가는것에 대한 부정적 입장 때문인듯 싶은데 ,이때 GCD내부에서 while 을 돌면서 먼가를 계속 수행하면 될 듯 싶다.다만 app 이 백그라운드로 가면 멈추더라~ (iOS 는 정해져있는듯)

 let queue = DispatchQueue.global()
        
        queue.async { () -> Void in  
            while true {
                sleep(1)
                // 먼가를 한다. 
                // 결과  a 가 생성 
                DispatchQueue.main.async {
                   // a 를 가지고 먼가를 한다. 
                    
                }
                
            }
            
        }
서버로부터 받은 a에 담겨진 값을 localnotification 으로 날리면 그게 바로 실시간 푸쉬서비스~ 아닐까?


5. SwiftTask  https://github.com/ReactKit/SwiftTask

이것에 대한 설명은 일단 사용해보고 난 후에 하기로 한다. 예만 저 링크 페이지에서 가져왔다.
뭔가 안드로이드의 AsyncTask 같다. -.-a


let task = Task<Float, String, NSError> { progress, fulfill, reject, configure in player.doSomethingWithProgress({ (progressValue: Float) in progress(progressValue) }, completion: { (value: NSData?, error: NSError?) in if error == nil { fulfill("OK") } else { reject(error) } }) // pause/resume/cancel configuration (optional) configure.pause = { [weak player] in player?.pause() } configure.resume = { [weak player] in player?.resume() } configure.cancel = { [weak player] in player?.cancel() } } task.success { (value: String) -> Void in // do something with fulfilled value }.failure { (error: NSError?, isCancelled: Bool) -> Void in // do something with rejected error } task.pause() task.resume() task.cancel()





Comments