Python Thread


Threadとは?


PCにはWindows、MacOS、Linuxなどのオペレーティングシステムがインストールされています.オペレーティングシステムはコンピュータを全面的に管理するマネージャの役割を果たしています.プログラムと呼ばれるものはオペレーティングシステム上で動作します.プログラムがメモリで実行されることをプロセスと呼びます.これらのプロセスの実行単位をスレッドと呼びます.プロセスには少なくとも1つ以上のスレッドがあり、複数のスレッドがある場合があります.
複数のスレッドが交差して動作する方法を同期と呼びます.
同期は一般的なマルチタスク処理です.これは、必要な仕事がA、B、Cである場合、仕事を分割し、少しずつ順番に処理することを意味する.一瞬にして一つのことをしているのですが、すぐに交代でやれば、まるで同時処理のようです.

2. GIL


Pythonでは,グローバル予測ロック(GIL)という概念が存在する.
GILって何ですか?これは、Python Infraterがスレッドのバイトコードを1つだけ実行できるようにするロックです.
1つのスレッドがすべてのリソースを使用し、ロックを解除して他のスレッドが実行できないようにします.
次の図は、Pythonで3つのスレッドで実行される例です.各スレッドはGILを使用して動作し、他のスレッドは動作を停止します.

また、マルチスレッドは、thread context switch(スレッド切替と理解される)に従ってメモリ割り当てを行う必要があり、これは逆に単一スレッドよりも長い時間を要する.
なぜPythonはGILを使うのですか?
Pythonは基本的にGarbage CollectionとReferenceカウントによって割り当てられたメモリを管理します.GCは過去に手動でメモリを管理して自動的にメモリを管理していた良い奴です.
まず準備の仕事を理解してください.
CPTyhonでのGCは参照カウント方式であり,Pythonでオブジェクトを作成するたびに基本CオブジェクトはPythonタイプ(list,dict,関数)とreferencecountを生成する.
オブジェクトを参照するたびに[参照数](Reference Count)が増加し、オブジェクトの参照が解除されると[参照数](Reference Count)が減少します.つまり、オブジェクトの参照数が0の場合、オブジェクトのメモリ割り当てが解除されます.
デフォルトでは、上記参照回数が0のオブジェクトをメモリから解放する転送カウント方式を使用しますが、参照回数が0ではなく到達できませんが、参照ループ(ループ参照)が発生した場合はGCを使用して解決します.(翻訳体が難しいので、次に例を見てみましょう)
ループ参照の簡単な例は、自分のオブジェクトを参照することです.次のコードを見てみましょう.
L = [] # Reference count가 1이 되었다.
L.append(L) # Reference count가 2가 되었다.
del L # Reference count가 1이 되었다.
Lの参照回数は1ですが、オブジェクトはアクセスできなくなり、転送カウントでメモリから解放できません.
相互参照のオブジェクトもループ参照の例です.
a = Foo() # 빈 함수 대입 메모리 주소값 0x60 / Reference count가 1이 되었다.
b = Foo() # 빈 함수 대입 메모리 주소값 0xa8 / Reference count가 1이 되었다.
a.x = b # 0x60의 x는 0xa8을 가리키고 있다.
		# 0x60의 Reference count 는 a와 b.x로 2다
b.x = a # 0xa8의 x는 0x60을 가리키고 있다.
		# 0xa8의 Reference count 는 b와 a.x로 2다
del a # 0x60의 Reference count는 1로 감소했다.
del b # 0xa8의 Reference count는 1로 감소했다.
この状態では、0x60.x0xa8.xが互いに参照し合うため、転送カウンタは両方とも1であるが0には至らない.
この時、ゴミ収集が発生し、バタンと音を立てて、問題の解決を助けます.
GILを理解するためにここまで知っておきましょう後でよく勉強します.

3.GILを使う理由


前述したように、Python内のすべてのオブジェクトには、1つのレジストリ(変数参照の数)が格納されています.この場合、複数のスレッドで1つのオブジェクトを使用する場合は、レポートを管理するためにすべてのオブジェクトをロックする必要があります.このような非効率を防ぐために、PythonはGILを使用しています.
すべてのオブジェクトに対するレジストリ同期の問題は、ロックによって解決されます.

4.書き込みモジュールの利用


次のコードは、スレッドモジュールを使用して、0から5000万までのプロセスでshared numberがどれだけ増加したか(効率がどれだけ高いか)を決定するサンプルコードです.
import threading
import time

shared_number = 0

def thread_1(number):
    global shared_number
    print("number = ",end=""), print(number)
    for i in range(number):
        shared_number += 1

def thread_2(number):
    global shared_number
    print("number = ",end=""), print(number)
    for i in range(number):
        shared_number += 1


if __name__ == "__main__":

    threads = [ ]

    start_time = time.time()
    t1 = threading.Thread( target= thread_1, args=(50000000,) )
    t1.start()
    threads.append(t1)

    t2 = threading.Thread( target= thread_2, args=(50000000,) )
    t2.start()
    threads.append(t2)


    for t in threads:
        t.join()

    print("--- %s seconds ---" % (time.time() - start_time))

    print("shared_number=",end=""), print(shared_number)
    print("end of main")
>>>
number = 50000000
number = 50000000
--- 4.035499095916748 seconds ---
shared_number=63779654
end of main
shared numberが1億回未満の原因は何ですか?グローバル変数なので重ねて使うべきでしょうか?
いいえ.異なるスレッドがグローバル変数にアクセスするにつれて、計算は複雑になります.a+=1は通常3つのコマンドで実行され、cpuは1つのコマンドを実行する.
  • aの値をメモリからresisterにロードします.
  • レジスタで追加されます.
  • さらに
  • の値を実際のaのあるメモリに格納する.
  • thread 1はまず行われ、最後のメモリに保存する前にthread 2が実行されます.メモリ値は変更され続けます.0->1->0->1はこのようにしています.
    では、そのshared numberが1億回に設定されたらどうすればいいのでしょうか.
    Pythonは単一スレッドを使用しており,上記の方法はGILである.これは、スレッド間の同期がないため、同期を抑制するために使用されるためですか?同期が許可されている場合、カウントは正常に動作しますか?
    では、threadを同期する方法を使用しましょう.
    ロック()を使用して同期します.
    次のコードを見ると分かりやすいです.
    import threading	#threading 라이브러리를 이용
    import time			#time 라이브러리를 이용
    from threading import Lock	#이미 위에서 해줬지만, threading 의 Lock 을 가지고 왔다.
    
    shared_number = 0
    lock = Lock()	#Lock을 편하게 해주기 위해서 lock 이라는 변수에 객체를 만들어줬다.
    
    def thread_1(number):
        global shared_number
        print("number = ",end=""), print(number)
        
        lock.acquire() # for문 실행전에 lock을 걸어서 다른 스레드가 전역변수 접근을 못하게 했다.
        for i in range(number):
            shared_number += 1
        lock.release() # lock 해제
    
    def thread_2(number):
        global shared_number
        print("number = ",end=""), print(number)
    
        lock.acquire() # thread_2 lock 걸기
        for i in range(number):
            shared_number += 1
        lock.release() # thread_2 lock 해제
    
    if __name__ == "__main__":
    
        threads = [ ]
    
        start_time = time.time()
        t1 = threading.Thread( target= thread_1, args=(50000000,) )
        t1.start()
        threads.append(t1)
    
        t2 = threading.Thread( target= thread_2, args=(50000000,) )
        t2.start()
        threads.append(t2)
    
        for t in threads:
            t.join()	# join을 사용해서 sub스레드가 끝날 때 까지 main 스레드가 기다려주자
    
        print("--- %s seconds ---" % (time.time() - start_time))
    
        print("shared_number=",end=""), print(shared_number)
        print("end of main")
    >>>
    number = 50000000
    number = 50000000
    --- 4.0316081047058105 seconds ---
    shared_number=100000000
    end of main