여러 사이트를 동시에 크롤링 하는 등의 작업을 위해서는 동시에 웹페이지 내용을 가져오는 것이 거의 필수적입니다. 하지만, 가져올 페이지가 10000개라고 동시에 10000개를 모두 가져 올 수는 없습니다. 서버 사양이나 네트워크 대역폭을 감안하여, 동시에 수십개 정도 사이트를 수집하는 정도가 일반적입니다.

동시에 작업을 처리하는 방법은 많지만, 개인적으로 요즘은 사용법이 단순하고 쉬운 Parallel 패키지를 자주 사용하고 있습니다. Parallel은 PHP의 pcntl 함수를 사용하기 편하게 클래스로 만든 것인데, 한가지 부족한 점은 많은 양의 작업을 대상으로 그 중 일정한 갯수만 동시에 처리하는 기능입니다. 이 점을 보완하기 위해 작은 클래스를 하나 만들어 보았습니다.

우선 Parallel 패키지는 컴포져(Composer)를 이용해 설치 할 수 있습니다. 다만 pcntl 모듈을 꼭 필요합니다.

1
composer require kzykhys/parallel
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
<?php

namespace SangheonHan\Thread;

use KzykHys\Thread\Runnable;
use KzykHys\Thread\Thread;

class WorkerPool
{
    public function __construct(int $numWorker)
    {
        $this->workers = array_fill(0, $numWorker, null);
    }

    public function start(Runnable $job) : int
    {
        while (true) {
            $id = $this->find();
            if ($id >= 0) {
                break;
            }
            usleep(1000);
        }

        $this->workers[$id] = new Thread($job);
        $this->workers[$id]->start();

        return $id;
    }

    public function find() : int
    {
        foreach ($this->workers as $id => $worker) {
            if ($worker == null) {
                return $id;
            }

            $pid = $this->workers[$id]->getPid();
            $result = pcntl_waitpid($pid, $status, WNOHANG);
            if ($result == -1 || $result > 0) {
                $this->workers[$id] = null;
                return $id;
            }
        }

        return -1;
    }

    public function wait()
    {
        foreach ($this->workers as $worker) {
            if ($worker instanceof Thread) {
                $worker->wait();
            }
        }
    }
}

동시에 여러 작업을 처리하기 위해 WorkerPool 클래스를 생성하실 때 동시 처리 최대 작업 갯수를 파라미터로 전달하면 됩니다.

WorkerPool::start() 메소드를 통해 작업을 시작하기 위해서는 KzykHys\Thread\Runnable 인터페이스를 구현한 Job 클래스를 생성해서 파라미터로 전달합니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<?php

namespace SangheonHan\Thread;

use KzykHys\Thread\Runnable;

class Job implements Runnable
{
    public function __construct($id) {
        $this->id = $id;
    }

    public function run()
    {
        for ($i = 10; $i < 20; $i++) {
            echo "{$this->id} - {$i}".PHP_EOL;
            sleep(rand(1, 5));
        }
    }
}

WorkerPool 클래스와 작업을 실제 처리 할 Job 클래스를 이용해 많은 양의 작업을 정한 프로세스 갯수만큼 동시에 처리하는 방법은 다음 코드와 같습니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<?php

use \SangheonHan\Thread\WorkerPool;
use \SangheonHan\Thread\Job;

require 'vendor/autoload.php';

$workerPool = new WorkerPool(5);

for ($i = 0; $i < 10; $i++) {
    $jobList[$i] = new Job($i);
}

foreach ($jobList as $job) {
    $workerPool->start($job);
}

$workerPool->wait();

한가지 잊지 말아야 할 것은 모든 작업이 끝나고 프로그램이 종료될 수 있도록 WorkerPool::wait() 메소드를 마지막에 꼭 호출 해 주어야 합니다.