Tag Archives: 쓰레드

iOS NSOperation과 NSOperationQueue 이해하기

iOS/Android가 처음나왔을때 접할 수 있었던 앱들의 수준이 기억납니다. 지금 생각하면 매우 조악하고 허접한 앱들이 많이 있었습니다. 특이 무언가 버튼 하나 누르면 결과가 나올때까지 화면의 전체 UI가 정지 된다던지 사용성이 떨어지는 앱들이 많이 있었는데 요즘은 이런 바보같은 앱들이 보기 힘들어진것 같습니다.

대부분의 앱들이 쓰레드를 적절히 활용하고 있고 부담이 될 수 있는 작업들은 백그라운드에서 실행되도록 할 수 있습니다. 일반적으로 iOS 개발자들이 이러한 쓰레드 활용을 위해서는 Grand Central Dispatch와 NSOperation 두가지를 활용할 수 있습니다. 이 글에서는 우선 NSOperation에 대해서 다루어 보겠습니다.

NSOperation은 어떤 하나의 작업을 나타냅니다. NSOperation은 모델링 상태, 우선순위, 의존성 그리고 관리를 지원하는 유용하고 Thread safe한 추상 클래스입니다.

NSOperation를 상속하여 커스텀 클래스를 만들어 활용하는것이 이해가 되지 않을 경우 NSBlockOperation 또는 NSInvocationOperation을 활용하시기 바랍니다.

예를 들어 네트워크 요청, 이미지 리사이즈, 텍스트 처리, 또는 기타 다양한 반복처리등 오래 걸리는 작업을 처리해야하는 NSOperation이 있다고 할 때, 이 특정 작업이 담겨있는 객체는 감독 없이 많은 일을 할 수 없습니다. 이러한 작업을 진행하는 감독을 NSOperationQueue가 담당합니다.

NSOperationQueue

NSOperationQueue는 작업의 동시 실행을 조절하는 일을 합니다. 이 큐는 우선순위 큐와 같이 동작하며 기본적으로 First-In-First-Out으로 동작을 하나 더 높은 우선 순위(NSOperation.queuePriority)의 작업이 들어오게 되면 더 낮은 우선 순위를 가진 작업들을 건너뛰고 실행되게 됩니다. NSOperationQueue는 maxConcurrentOperationCount 프로퍼티 설정을 이용해 주어진 특정 시간동안 동시에 실행 가능한 작업의 숫자를 제한하는것이 가능합니다.

NSOperation을 시작하기 위해서는 start 메소드를 호출하거나 NSOperationQueue에 추가하여 큐의 가장 처음에 도달하면 한번 실행되도록 할 수 있습니다. NSOperation을 사용하여 얻을 수 있는 많은 장점들이NSOperationQueue를 사용함에서 오기 때문에 NSOperation의 start를 직접적으로 호출하는것보다는 NSOperationQueue에 추가하여 사용하는것이 바람직합니다.

State

NSOperation 의 작업 실행 과정을 설명하기 위해 상태 머신을 통해 표현해 보자면 다음과 같습니다.

ready → executing → finished

명시적인 state 프로퍼티가 존재하는것 대신에 state는 암묵적으로 해당 키패스에 KVO(Key-Value Observing) 통지를 하는것으로 결정하게 됩니다. 작업이 실행될 준비가 되었다면 ready 키패스에 KVO 통지를 하면 이에 대응하는 프로퍼티가 true를 반환하게 됩니다.

  • ready: 이 값이 true를 반환한다면 작업이 실행될 준비가 되었음을 의미합니다. false라면 작업의 초기화가 아직 끝나지 않은것을 의미합니다.
  • executing: 이 값이 true를 반환한다면 해당 작업이 현재 작업중임을 의미합니다. false라면 그 외의 상황임을 의미합니다.
  • finished: 이 값이 true를 반환한다면 작업이 성공적으로 종료되었거나 취소가 되었음을 의미합니다. NSOperationQueue는 이 finished 값이 true로 바뀌지 않는한 큐에서 Dequeue 하지 않으므로 데드락을 야기할 소지가 있습니다. 그러므로 작업을 구현할 때 주의해 주시기 바랍니다.

Cancellation

필요없는 작업이 실행되는것을 방지하거나 의존성 있는 작업이 실패하였을때, 혹은 유저에 의해 명시적으로 취소가 될때 작업을 취소하는것은 유용하게 사용됩니다.

state값이 변경되는 과정과 비슷하게 작업이 취소되면 NSOperation은 KVO를 통해 cancelled 키패스에 통지를 보내게 됩니다. 작업이 취소되었다면 내부의 사용되던 자원들을 정리하고 가능한 빠르게 적절한 최종상태로 변경하게 됩니다. cancelled와 finished 프로퍼티의 값은 true로 바뀌고 executing 프로퍼티의 값은 false로 변경됩니다.

Priority

모든 작업들은 같은 중요성을 가지지 않을것입니다. queuePriority 프로퍼티의 값을 설정함으로써 NSOperationQueue가 우선순위를 상승 또는 하강하게 됩니다.

// Objective-C
typedef enum : NSInteger {
   NSOperationQueuePriorityVeryLow = -8,
   NSOperationQueuePriorityLow = -4,
   NSOperationQueuePriorityNormal = 0,
   NSOperationQueuePriorityHigh = 4,
   NSOperationQueuePriorityVeryHigh = 8
} NSOperationQueuePriority;

// Swift
enum NSOperationQueuePriority : Int {
    case VeryLow
    case Low
    case Normal
    case High
    case VeryHigh
}

Asynchronous Operations

iOS8/OSX요세미티에서 concurrent 프로퍼티가 Deprecate되고 새로운 asynchronous 프로퍼티가 추가되었습니다. 원래 concurrent 프로퍼티는 하나의 메인 메소드에서 모든 작업을 수행할것인지 여부를 구별하는데 사용되고 작업이 비동기적으로 수행될 때 그 상태를 관리하는데 사용되었습니다. 이 프로퍼티는 NSOperationQueue가 다른 별도의 쓰레드에서 작업을 실행할지 여부를 결정하는데 사용되었습니다. NSOperationQueue가 기존의 직접적으로 쓰레드를 관리하는 방법에서 Internal Dispatch Queue 위에서 동작하도록 변경된 이후 이 프로퍼티는 무시하게 되었습니다. 새로운 asynchronous 프로퍼티는 기존의 복잡한 의미를 갖는 concurrent 프로퍼티 대신에 NSOperation이 동기로 실행될지 비동기로 실행될지를 결정하는 유일한 방법입니다.

Dependencies

어떤 복잡한 어플리케이션에서는 큰 작업을 다양한 작은 작업으로 분리하여 처리할 수 있습니다. 이러한 처리를 NSOperation의 의존성을 통해 할 수 있습니다.

예를들어 서버로부터 이미지를 다운로드 받고 리사이즈하는 작업이 있다고 할 때 네트워크 통신을 하는 작업 한개, 이미지를 리사이즈하는 작업 한개로 분리할 수 있습니다. 그러나 이미지 다운로드가 완료될 때 까지 리사이즈 작업은 수행할 수 없습니다. 네트워크 작업은 리사이즈작업에게 의존성을 가지게 됩니다. 네트워크 작업이 완료되어야 리사이즈 작업이 시작될 수 있습니다.

// Objective-C
NSOperation *networkingOperation = ...
NSOperation *resizingOperation = ...
[resizingOperation addDependency:networkingOperation];

NSOperationQueue *operationQueue = [NSOperationQueue mainQueue];
[operationQueue addOperation:networkingOperation];
[operationQueue addOperation:resizingOperation];

// Swift
let networkingOperation : NSOperation = ...
let resizingOperation : NSOperation = ...
resizingOperation.addDependency(networkingOperation)

let operationQueue = NSOperationQueue.mainQueue()
operationQueue.addOperations([networkingOperation, resizingOperation], waitUntilFinished: false)

위의 작업은 의존성을 가지고 있는 작업들의 finished값이 true가 될때까지 시작되지 않습니다. 만약 A가 B의 의존성을 가지고 있고 반대로 B가 A의 의존성을 가지게 되면 의존성 사이클이 생기게 되는데 데드락을 발생시킬 수 있으니 주의하도록 합니다.

completionBlock

NSOperation이 종료되면 completionBlock이 한번 실행됩니다. 이는 모델이나 뷰컨트롤러에서 작업이 수행될때 부가적인 작업을 수행할 수 있도록 하는 방법을 제공합니다.

// Objective-C
NSOperation *operation = ...;
operation.completionBlock = ^{
    NSLog("Completed");
};

[[NSOperationQueue mainQueue] addOperation:operation];

// Swift
let operation = NSOperation()
operation.completionBlock = {
    println("Completed")
}

NSOperationQueue.mainQueue().addOperation(operation)

예를 들어 네트워크를 통해 서버로부터 어떤 데이터를 다운받은 다음에 응답값을 처리하기 위해서 적절하게 사용될 수 있습니다.

When to Use NSOperation

NSOperation은 각 작업들간에 의존성을 설정할 수 있고 우선순위 큐와 QOS를 통해 작업을 수행할 수 있습니다. GCD 큐와 다르게 NSOperation은 작업을 취소하거나 현재 상태를 질의할 수 있습니다. 또한 NSOperation을 상속하여 나중에 다른곳에서 참조가능하도록 결과들을 보관하는것이 가능합니다.

참고: http://nshipster.com/nsoperation/

[Java/Android] Thread를 이용한 특정 작업 수행하기

안드로이드에서 비동기로 특정 작업을 처리하기 위해 간편하게 사용할 수 있는 것으로 AsyncTask가 있습니다. 하지만 그것 이전에 어디서든지 사용할 수 있는 Thread를 이용한 방법에 대해 간단히 정리해 보도록 하겠습니다.

여기서 주의할 점은 처리가 끝나는 시점에서 UI를 변경한다거나 하는 것을 핸들러를 이용하여 처리하는 부분인데요 안드로이드에서는 메인 쓰레드 이외에서 UI에 영향을 끼치는 행위를 거부하고 있습니다. 그렇기 때문에 핸들러를 이용하여 처리를 합니다.

[code]public class ThreadDownloadActivity extends Activity implements OnClickListener
{
    private ProgressDialog mPdProgress;

    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
       
        // 상태 표시용 프로그래스바
        mPdProgress = new ProgressDialog(this);
        mPdProgress.setMessage(“작업을 수행중입니다.”);
        mPdProgress.setIndeterminate(true);
        mPdProgress.setProgressStyle(ProgressDialog.STYLE_SPINNER);
        mPdProgress.setCancelable(false);
       
        Button button = (Button) findViewById(R.id.button);
        button.setOnClickListener(this);
    }
   
  // 이미지 다운로드 결과 표시용 핸들러
    final Handler handler = new Handler()
    {
    public void handleMessage(Message msg)
    {
    mPdProgress.dismiss();
    String message = null;
    switch(msg.what)
    {
case 0:
message = “수행 완료”;
break;

default:
message = “수행 실패”;
break;
}
   
    // 메시지 출력
    Toast.makeText(ThreadDownloadActivity.this, message, Toast.LENGTH_SHORT).show();
    }
    };

@Override
public void onClick(View v)
{
mPdProgress.show();

// 이미지 저장용 쓰레드
   Thread thread = new Thread(new Runnable()
   {
            public void run()
            {
        try
        {
        // 특정 작업을 수행
        // 원래는 이미지 다운로드를 구현해 보려 하였지만 포기;;
        Thread.sleep(2000);
        handler.sendEmptyMessage(0);
        }
        catch(Exception e)
        {
        // 작업이 실패 시
        handler.sendEmptyMessage(1);
        }
            }
        });
        thread.start();
}
}[/code]
위와 같은 코드를 수행해 보면 다음과 같이 잘 동작하는 것을 확인할 수 있습니다.
사용자 삽입 이미지1359710808.zip