여덟째 날: 돌리고~ 돌리고~ 작업 병렬 실행

저자

@aer0 - Seoul.pm, #perl-kr의 정신적 지주, Perl에 대한 근원적이면서 깊은 부분까지 놓치지 않고 다루는 홈페이지 및 블로그를 운영하고 있다. aero라는 닉을 사용하기도 한다.

시작하며

요즘은 물리적 CPU가 하나라도 내부 코어(core) 갯수가 한 개인 CPU는 찾아보기 어렵고 심지어 모바일 스마트폰의 CPU조차 여러 개의 코어를 지원하는 시대에 살고 있습니다. 따라서 어떤 작업을 할 때 멀티 코어의 장점을 살리려면 병렬 실행이 필수적입니다. 작업을 병렬로 실행하는 방법은 크게 스레드(thread)를 이용한 방법과 멀티 프로세스(multi-process)를 이용한 방법이 있습니다. Perl은 두 가지 방법 모두 지원합니다. Python, Ruby의 스레드가 GIL(Global Interpreter Lock)이란 구조 때문에 하나의 코어만 사용할 수 있는 반면, Perl 스레드는 여러 개의 코어를 동시에 활용 가능합니다. 하지만 네이티브 스레드와는 구조가 좀 다른 인터프리티드 스레드(interpreted thread)라는 구조를 택하고 있어서 요즘은 될 수 있으면 멀티 프로세스 구조를 사용하도록 추천합니다.

단순히 독립적인 작업을 단순히 처리하는 것이 목적인 병렬 작업은 그냥 동시에 돌려놓고 작업들이 끝나기를 기다리면 그만이기 때문에 스레드를 이용하든 포크(fork)를 이용한 멀티 프로세스 구조든 간단합니다. 하지만 어떤 공통되는 성공 실패 카운트를 세는 변수가 있거나, 각 작업의 결과를 모아서 처리를 해야 하고 자신의 CPU 코어 수에 맞게 시스템이 응답지연 및 hang상태에 빠지지 않도록 과도한 부하가 가지 않게 동시에 실행되는 작업의 갯수를 적절하게 조절해야 되는 요구가 생기면 슬슬 신경 써야 할 부분이 많아지죠. 스레드 같으면 여러 스레드가 변수값을 동시에 변경하지 못하도록 mutex같은 lock을 써서 동기화시켜 주어야 하고 멀티 프로세스에서는 부모, 자식 프로세스간 메모리 영역이 공유되지 않기 때문에 자식 프로세스가 넘기는 결과나 값을 받으려면 파이프, 소켓, 임시파일 등을 통한 부모, 자식 프로세스 간에 데이터 통신을 하기 위한 IPC(inter-process communication)장치도 만들어야 합니다. 그리고 현재 동시에 작업하고 있는 스레드나 프로세스의 갯수를 세고, 지정한 개수를 초과하지 않도록 조절해야 합니다. 이걸 다 직접 구현하려면 머리가 지끈지끈하겠죠? 하지만 걱정마세요! Perl CPAN에는 이런 요구사항을 한방에 만족시켜주는 Parallel::ForkManager라는 모듈이 있으니까요.

준비물

필요한 모듈은 다음과 같습니다.

직접 CPAN을 이용해서 설치한다면 다음 명령을 이용해서 모듈을 설치합니다.

$ sudo cpan Parallel::ForkManager

사용자 계정으로 모듈을 설치하는 방법을 정확하게 알고 있거나 perlbrew를 이용해서 자신만의 Perl을 사용하고 있다면 다음 명령을 이용해서 모듈을 설치합니다.

$ cpan Parallel::ForkManager

Parallel::ForkManager

Parallel::ForkManager 모듈은 CPAN의 각종 병렬처리 모듈 중 가장 인기가 좋은 모듈이며, 그러다보니 이 모듈에 의존성을 가지는 모듈들도 아주 많습니다. Parallel::ForkManager는 세부적으로 다양한 컨트롤 옵션과 함수를 가지고 있지만 지금은 군더더기를 걷어내고 가장 단순한 예제 코드로 어떻게 동작하는지 살펴보겠습니다.

#!/usr/bin/env perl

use strict;
use warnings;

use Parallel::ForkManager;

my @num = 1 .. 50;

# 5개의 최대 동시 프로세스
my $pm = Parallel::ForkManager->new(5);

# 결과값을 저장할 해시변수
my %out;

# 각 프로세스 시작시 실행하는 콜백함수 등록
$pm->run_on_start(
    sub {
        my ( $pid, $ident ) = @_; # $pid   - 프로세스 번호
                                  # $ident - start시 넘겨준 값
        print "### START $ident started, pid: $pid\n";
    }
);

# 각 프로세스 종료시 실행하는 콜백함수 등록
$pm->run_on_finish(
    sub {    # must be declared before first 'start'
        my (
            $pid,
            $exit_code,
            $ident,
            $exit_signal,
            $core_dump,
            $data,
        ) = @_; # $data는 프로세스 종료시 반환값

        $out{ $data->[0] } = $data->[1];
    }
);

# 실제 작업을 실행시키는 루프
for my $num (@num) {
    # Parent nexts, run_on_start에서 고유값으로 사용할 값을 넘겨줄 줄 수 있음.
    $pm->start($num) and next;

    # 자식 프로세스 영역 시작 ----------

    my $sq = $num ** 2;

    # 자식 프로세스 영역 끝 ----------

    # Child exits, 값을 리턴
    $pm->finish( 0, [ $num, $sq ] );
}

# 모든 프로세스가 끝나기를 기다림
$pm->wait_all_children;

for my $num ( sort { $a <=> $b } keys %out ) {
    print "$num  $out{$num}\n";
}

예제 코드는 최대 5개의 프로세스를 유지하며 1에서 50까지 수의 제곱을 계산하고 마지막에 결과를 출력합니다. 코드에 주석으로 설명을 달아놨지만 핵심적인 부분은 for 반복문과 반복문 내의 각 자식 프로세스가 실행되는 코드영역이며, 지금은 간단한 한 줄짜리 계산이지만 외부 함수 호출등 복잡한 작업이 필요하다면 대체하면 됩니다.

run_on_start 함수는 프로세스가 시작할 때 어떤 메시지를 찍거나, 전처리 작업을 하고 싶을 때 사용할 콜백함수를 등록하기 위해 사용합니다. 어떤 대상에 대한 작업인지 고유의 값으로 판별하고 싶으면 start 함수에 옵션으로 어떤 값을 넘겨(필요없으면 안써도 됨) 등록한 콜백함수 내에서 $ident 값으로 받아 사용할 수 있습니다.

그리고 프로세스가 끝났을때 finish( 0, [ $num, $sq ] ) 함수로 프로세스 종료시 기본 종료 코드 0을 지정하고 부모 프로세스에게 선택적으로 넘겨줄 수 있는 변수값을 뒤에 넘겨주고 있습니다. 변수는 스칼라 형태여야 하며 여기서는 두 값을 포함하고 있는 익명 배열 참조(레퍼런스는 실제 데이터가 위치한 주소값을 가지고 있는 스칼라변수)를 넘겨주고 있습니다. 이 값은 run_on_finish 함수에서 등록한 콜백함수에서 $data로 받아서 처리합니다.

### START 1 started, pid: 26015
### START 2 started, pid: 26016
### START 3 started, pid: 26017
.
.
### START 48 started, pid: 26062
### START 49 started, pid: 26063
### START 50 started, pid: 26064
1  1
2  4
3  9
.
.
48  2304
49  2401
50  2500

정리하며

사실 위 코드는 어떻게 동작하는지를 보여주기 위한 목적이지 실제는 간단한 계산이기 때문에 계산에 걸리는 부하보다 새로 프로세스를 fork하는데 걸리는 부하가 더 크기 때문에 그냥 순차적으로 계산하는 것 보다 빠르지 않을 것 입니다. 하지만 프로세스당 아주 복잡하고 긴 시간이 걸리는 계산을 하거나 여러 URL을 넘겨서 웹페이지를 긁어온다거나, 아주 많은 서버 IP 주소를 대상으로 ping을 해보거나, SSH로 원격으로 붙어서 어떤 명령을 날린다거나 하는 작업을 한다면 순차적으로 하는 것과는 비교가 되지 않는 작업 효율을 낼것입니다.

이제 이 코드를 기반으로 다양한 마법을 부리는 것은 여러분에게 달려있습니다. 돌리고~ 돌리고~!!

blog comments powered by Disqus