Up

スレッドとプロセス

 

スレッド(thread)とプロセス(process)の簡単な例を作ってみた。スレッドでは、スレッド間の通信は容易であるが、複数のスレッドは、各スレッドの処理が他のスレッドの処理と部分的に順番に交代で実行されるので、スレッドの数に応じて処理時間が長くなる。これに対して、プロセスは複数のCPUコアで同時に実行されるので、プロセスの管理処理の時間の増加はあるが、プロセス数に比例して処理時間が増えるということはない。これらを確認するためのスレッドのプログラム例プロセスのプログラム例を作成してみた。

スレッドでは、スレッドとの通信が容易であるので、入出力機器の監視を行いながら処理を行うときに便利である。入出力機器の監視に処理の待ち時間がある場合は、スレッドとして処理を行えば、他の処理を並行して行うことができるので、待ち時間の影響を抑えることができる。

プロセスの場合は、複数のプロセスが複数のCPU(コア)によって処理が行われるので、長時間掛かる計算を複数のプロセスに分割して行うことにより、計算時間の短縮が可能になる。

 

スレッドの例

マルチスレッド(multi-thread)の処理時間を確認するスクリプト例と、スレッド間のデータのやり取りを行うスクリプト例の2つを用意した。まず、処理時間の比較例を示し、次にスレッド間の通信例を示す。なお、Windows 10の場合、実行はPython IDLEでも可能であるが、端末画面においてコマンドpythonによって実行する方が、出力が見やすい。

 

まず、処理時間の比較の基準とするため、以下のスクリプトを用意した。

 

def calc_sum(d, nm):

        sum_v = 0.0

        i = 0

        for v in d:

                sum_v += v

                i += 1

                if i % 1_000_000 == 0:

                        print(nm, i)

        return sum_v

 

n = 10_000_000

data = ss.norm.rvs(size = n)

 

start_t = time.time()

sum_v = calc_sum(data, '0:')

end_t = time.time()

print('time = ', end_t - start_t)

print('result: ', sum_v, '\n')

 

このスクリプトの実行で、以下の出力を得る。

 

0: 1000000

0: 2000000

0: 3000000

0: 4000000

0: 5000000

0: 6000000

0: 7000000

0: 8000000

0: 9000000

0: 10000000

time =  2.648660898208618

result:  -3262.383797592117

 

処理時間は、約2.6秒である。

 

上のスクリプトにおける関数calc_sumの実行をスレッドで行うため、以下のスクリプトを用意した。

 

class Calc_Sum(Thread):

        def __init__(self, d, td_name):

                super().__init__()

                self.d = d

                self.my_name = td_name

               

        def run(self):

                self.sum_v = calc_sum(self.d, self.my_name)

               

#  1 thread

threads = [Calc_Sum(data, 'id-0')] 

start_t = time.time()

for thread in threads:

        thread.start()

for thread in threads:

        thread.join()

end_t = time.time()

print('time =', end_t - start_t)

for t in threads:

        print('result:', t.sum_v)

print('\n')

 

スレッドは、クラス型Threadを継承したクラスとして用意し、実行処理内容はメソッドrunに記述する。このメソッドrunは、メソッドstartの呼び出しで実行される。スレッドの実行終了はメソッドjoinの呼び出しでチェックできる。メソッドjoinは、メソッドの終了を待つものである。

スレッドは1本である。実行時の出力は以下のようになった。

 

id-0 1000000

id-0 2000000

id-0 3000000

id-0 4000000

id-0 5000000

id-0 6000000

id-0 7000000

id-0 8000000

id-0 9000000

id-0 10000000

time = 2.708895683288574

result: -3262.383797592117

 

処理時間は約2.7秒である。先のスレッドでない場合は約2.6秒であったことに注意。スレッドとして実行するための処理時間の増加が見られる。

次に、スレッドを増やして4本のスレッドを実行するスクリプトを以下のように用意した。

 

#   4 threads  

threads = [Calc_Sum(data, f'ID-{i+1}') for i in range(4)]

start_t = time.time()

for thread in threads:

        thread.start()

for thread in threads:

        thread.join()

end_t = time.time()

print('time =', end_t - start_t)

for t in threads:

        print(f'{t.my_name}: results = {t.sum_v}')

 

このスクリプトを実行すると、以下のような出力になる。

 

ID-3 1000000

ID-2 1000000

ID-1 1000000

ID-4 1000000

ID-3 2000000

ID-1 2000000

ID-2 2000000

  ・

  ・

  ・

ID-4 8000000

ID-1 9000000

ID-2 10000000

ID-4 9000000

ID-1 10000000

ID-4 10000000

time = 11.90782356262207

ID-1: results = -3262.383797592117

ID-2: results = -3262.383797592117

ID-3: results = -3262.383797592117

ID-4: results = -3262.383797592117

 

計算の途中経過の出力から4本のスレッドが並行して実行されていることが分かるが、各スレッドの実行は部分的に進み、各瞬間には1つのスレッドのみが実行されている。このことは、処理時間が約11.9秒とスレッドが1本の場合の約4倍になっていることで確認できる。

上のスクリプト例の完全なスクリプトリストをリスト1に示す。

 

リスト1

 

import scipy.stats as ss

from threading import Thread

import time

 

def calc_sum(d, nm):

        sum_v = 0.0

        i = 0

        for v in d:

                sum_v += v

                i += 1

                if i % 1_000_000 == 0:

                        print(nm, i)

        return sum_v

 

n = 10_000_000

data = ss.norm.rvs(size = n)

 

start_t = time.time()

sum_v = calc_sum(data, '0:')

end_t = time.time()

print('time = ', end_t - start_t)

print('result: ', sum_v, '\n')

 

class Calc_Sum(Thread):

        def __init__(self, d, td_name):

                super().__init__()

                self.d = d

                self.my_name = td_name

               

        def run(self):

                self.sum_v = calc_sum(self.d, self.my_name)

               

#  1 thread

threads = [Calc_Sum(data, 'id-0')] 

start_t = time.time()

for thread in threads:

        thread.start()

for thread in threads:

        thread.join()

end_t = time.time()

print('time =', end_t - start_t)

for t in threads:

        print('result:', t.sum_v)

print('\n')

       

#   4 threads  

threads = [Calc_Sum(data, f'ID-{i+1}') for i in range(4)]

start_t = time.time()

for thread in threads:

        thread.start()

for thread in threads:

        thread.join()

end_t = time.time()

print('time =', end_t - start_t)

for t in threads:

        print(f'{t.my_name}: results = {t.sum_v}')

 

実行は、リスト1のファイル名がcheck_thread.pyのときは、pythonコマンドを

 

PS D:\ThreadPool\Prgs> python check_thread.py

 

と実行すればよい。

 

 

スレッド間の通信

 

計数のスレッドを2つ走らせ、それらのスレッドを監視するスレッドを1つ用意する。

スレッドは、クラス型Threadを継承したクラスとして用意し、実行処理内容はメソッドrunに記述する。このメソッドrunは、メソッドstartの呼び出しで実行される。スレッドの実行終了はメソッドjoinの呼び出しでチェックできる。メソッドjoinは、メソッドの終了を待つものである。

計数用スレッドは以下のように用意した。

 

class Counting(Thread):

        def __init__(self, ck):

                super().__init__()

                self.ck = ck       #    Threadの状態

                self.id = ck       #    ThreadID

                self.a = None      #    他のThread

                self.ps = 0        #    printの使用状態

                self.counter = 0   #    カウンター

 

        def run(self):

                while self.ck != 0:

                        self.counter += self.ck

                        while self.a.ps == 1:   #   他のThreadprint使用の空きを待つ

                                pass

                        self.ps = 1

                        if self.ck < 0:

                                print('{0:>10s}'.format(' '),end = '')

                        print(f'{self.id}:  {self.counter}')

                        self.ps = 0    

                        s = time.time()

                        while time.time() - s < 1:

                                pass       

 

スレッドの状態を表すアトリビュートckは、係数アトリビュートcounterに加えられる値であるが、このck0になるとスレッドの実行は修了する。

他のスレッドをアトリビュートaで表し、そのスレッドの状態のチェックができるようにする。

アトリビュートpsは、画面出力printの使用状態を表す。1ならば、使用中である。

計数用スレッドの管理用スレッドを以下のように用意した。

 

class Man(Thread):

        def __init__(self, c1, c2):

                super().__init__()

                self.c1 = c1

                self.c2 = c2

                c1.a = c2

                c2.a = c1

 

        def run(self):

                s = time.time()

                while time.time() - s < 10.1:

                        pass

                #   ckの値を0に設定してThreadの終了

                self.c1.ck = 0

                self.c2.ck = 0

 

所定の時間(10.1秒)が経つと、係数スレッドのアトリビュートckを0に設定して終了させる。

上の2種類のスレッドを実行するスクリプト例をリスト1aのように用意した。

 

リスト1a

 

import scipy.stats as ss

from threading import Thread

import time

 

class Counting(Thread):

        def __init__(self, ck):

                super().__init__()

                self.ck = ck       #    Threadの状態

                self.id = ck       #    ThreadID

                self.a = None      #    他のThread

                self.ps = 0        #    printの使用状態

                self.counter = 0   #    カウンター

 

        def run(self):

            while self.ck != 0:

                self.counter += self.ck

                while self.a.ps == 1:   #   他のThreadprint使用の空きを待つ

                    pass

                self.ps = 1

                if self.ck < 0:

                    print('{0:>10s}'.format(' '),end = '')

                print(f'{self.id}:  {self.counter}')

                self.ps = 0    

                s = time.time()

                while time.time() - s < 1:

                    pass          

 

#   Threadの監視用Thread

class Man(Thread):

    def __init__(self, c1, c2):

        super().__init__()

        self.c1 = c1

        self.c2 = c2

        c1.a = c2

        c2.a = c1

 

    def run(self):

        s = time.time()

        while time.time() - s < 10:

            pass

        #   ckの値を0に設定してThreadの終了

        self.c1.ck = 0

        self.c2.ck = 0

 

thread1 = Counting(1)

thread2 = Counting(-1)

thread1.a = thread2

thread2.a = thread1

threadM = Man(thread1, thread2)

thread1.start()

thread2.start()

threadM.start()

thread1.join()

thread2.join()

threadM.join()

print('Threadの終了.')

 

リスト1aのファイル名がcheck_1_thread.pyであるときの実行例を以下に示す。

 

PS D:\ThreadPool\Prgs> python .\check_1_thread.py

1:  1

          -1:  -1

1:  2

          -1:  -2

1:  3

          -1:  -3

1:  4

          -1:  -4

1:  5

          -1:  -5

1:  6

          -1:  -6

1:  7

          -1:  -7

1:  8

          -1:  -8

1:  9

          -1:  -9

1:  10

          -1:  -10

Threadの終了.

 

 

プロセスの例

マルチプロセス(multi-process)の処理時間を、以下の手順で確認した。なお、マルチプロセスのプログラムは、Windows 10では、Python IDLEでの実行ではトラブルが生じたが、端末画面においてコマンドpythonによる実行では問題なかった。

マルチプロセスの実行は、クラス型Poolを用いて行う方法を用いるが、このときマルチプロセスの実行は次のif

 

if __name__ == '__main__':

 

のブロック(スイート:suite)内で行う必要がある。ここで、変数__name__はこのスクリプトを実行しているプログラムの名前であり、メインの実行プログラムのみ名前が’__main__’である。マルチプロセシングでメイン以外の実行プロセスから読み込まれているときは__main__以外の名前が与えられる。従って、上記のif文のブロックは、最初のメインのプログラムとして実行されている場合のみ処理の対象となる。

また、input関数の実行も、このif文のブロック内で行うが、Poolinputについてはこのウェブサイトで説明している。

マルチプロセシングの実行時間測定の基準を得るために、まず、次のスクリプトを用意する。

 

def calc_sum(i_d):

    d = i_d[1]

    sum_v = 0.0

    print(f'{i_d[0]} started.!')

    for v in d:

        sum_v += v

    print(f'{i_d[0]} ended.')

    return i_d[0], sum_v

 

if __name__ == '__main__':

    n = 30_000_000

    data = ss.norm.rvs(size = n)

 

    start_t = time.time()

    _, sum_v = calc_sum((0, data))

    end_t = time.time()

    print('time =', end_t - start_t)

    print('result =', sum_v)

 

関数calc_sumの宣言が、if

 

if __name__ == '__main__':

 

の前(スイート外)で行われていることに注意。このことにより、メイン以外のプロセスからこの関数宣言を読むことができる。

また、スクリプトによる処理、例えば、

 

data = ss.norm.rvs(size = n)

 

は、if文のスイート内に書かれているが、これにより上記の実行はメインのプログラムでのみ行われることになる。

上のスクリプトの実行時の出力は、以下のようになる。

 

0 started.!

0 ended.

time = 4.180828332901001

result = -493.9780580970286

 

関数calc_sum((0, data))の実行時間が、約4.2秒であることが分かる。

 

まず、同じ処理をプロセスとして実行すると、処理時間がどうなるかを調べるため、以下のスクリプトを用意した。

 

    pool = Pool()

           

    #    1 process

    params = [(0, data)]

    print()

    start_t = time.time()

    results = pool.map(calc_sum, params)

    end_t = time.time()

    print('time =', end_t - start_t)

    print('result =',results)

 

プロセスはPool型のオブジェクトのmapメソッドにより実行することができる。上のスクリプトでは、mapメソッドが

 

results = pool.map(calc_sum, params)

 

と呼び出されている。メソッドmapの第1引数にプロセスとして実行する関数名を置き、第2引数に関数のパラメータのリストを置く。リスト内の各パラメータに対応して第1引数で指定された関数がプロセスとして実行される。プロセスとして実行された関数の戻り値は、メソッドmapの戻り値であるリストの要素として返される。

 

実行時の出力は以下のようである。

 

0 started.!

0 ended.

time = 6.0625505447387695

result = [(0, -493.9780580970286)]

 

実行時間が、約6.1秒となり、先の単に関数の実行の場合の時間約4.2秒より長くなっている。これは、プロセスとしての処理が加わったためである。

次に、プロセスの数を4本にして実行するスクリプトを以下のように用意した。

 

    #     4 processes

    params = [(i+1, data) for i in range(4)]

    print()

    start_t = time.time()

    results = pool.map(calc_sum, params)

    end_t =time.time()

    print('time =', end_t - start_t)

    print('results:')

    for r in results:

        print(r)

 

実行時の出力は以下のようである。

 

1 started.!

2 started.!

3 started.!

4 started.!

1 ended.

2 ended.

4 ended.

3 ended.

time = 7.732580661773682

results:

(1, -493.9780580970286)

(2, -493.9780580970286)

(3, -493.9780580970286)

(4, -493.9780580970286)

 

プロセス数が1本であるときの処理時間は約6.1秒であったが、4本にしたときの上の実行結果では約7.7秒になっている。1本のときの処理時間約6秒の4倍である24秒より短く、1本のときの処理時間約6.1秒に近い。4本のプロセスが複数のCPUCore)で同時に処理されていることが分かる。6.1秒より少し長いのは、プロセスの管理のための時間と思われる。

上のプロセスのスクリプト例を1つの完全なスクリプトとしてリスト2に示す。

 

リスト2

 

import scipy.stats as ss

from multiprocessing.pool import Pool

import time

import copy

 

def calc_sum(i_d):

    d = i_d[1]

    sum_v = 0.0

    print(f'{i_d[0]} started.!')

    for v in d:

        sum_v += v

    print(f'{i_d[0]} ended.')

    return i_d[0], sum_v

 

if __name__ == '__main__':

    n = 30_000_000

    data = ss.norm.rvs(size = n)

 

    start_t = time.time()

    _, sum_v = calc_sum((0, data))

    end_t = time.time()

    print('time =', end_t - start_t)

    print('result =', sum_v)

 

    pool = Pool()

           

    #    1 process

    params = [(0, data)]

    print()

    start_t = time.time()

    results = pool.map(calc_sum, params)

    end_t = time.time()

    print('time =', end_t - start_t)

    print('result =',results)

 

    #     4 processes

    params = [(i+1, data) for i in range(4)]

    print()

    start_t = time.time()

    results = pool.map(calc_sum, params)

    end_t =time.time()

    print('time =', end_t - start_t)

    print('results:')

    for r in results:

        print(r)

 

リスト2を実行すると以下のような出力を得る。

 

0 started.!

0 ended.

time = 4.180828332901001

result = -493.9780580970286

 

0 started.!

0 ended.

time = 6.0625505447387695

result = [(0, -493.9780580970286)]

 

1 started.!

2 started.!

3 started.!

4 started.!

1 ended.

2 ended.

4 ended.

3 ended.

time = 7.732580661773682

results:

(1, -493.9780580970286)

(2, -493.9780580970286)

(3, -493.9780580970286)

(4, -493.9780580970286)

 

リスト2のスクリプトのファイル名がcheck_pool.pyのときは、次のコマンドpythonで実行することができる。

 

PS D:\ThreadPool\Prgs> python check_pool.py

 

 

Up