Python Multithreading Programlama | Thread ve Process Kullanımı
6 min read

Python Multithreading Programlama | Thread ve Process Kullanımı

Python Multithreading Programlama | Thread ve Process Kullanımı

Python thread ve process programlama kavramları sürekli birbirine karıştırılır. Bu dersimizde tüm kavramları tek tek işleyip multi thread ve process kullanımını göreceğiz.

Multithreading programlama birçok programlama dili tarafından desteklenir ve programların eş zamanlı olarak birden fazla işi yapabilmesine olanak sağlar. Tabi bu sonuç olarak çok daha hızlı süreçler anlamına gelse de aynı zamanda deadlock ve race-condition gibi sorunları da beraberinde getirebilir. Yine de Python ile thread programlama diğer dillere bakarak çok daha kolaydır.

İlk olarak kavramları tanımlamakla başlayalım:

Process: Bir işletim sistemi üzerinde herhangi bir dil ile kodlanmış ve bir compiler (derleyici) ile derlendikten sonra hafızaya yüklenerek işlemcide çalıştırılan programlara process denir. Kısacası bir programın çalışan hali processtir.

Thread: Threadler ise processlerin içerisinde yer alan eş zamanlı olarak çalışabilen iş parçacıklarıdır. Yani threadler sayesinde kodlarımızı ardaşıl olarak yürütmek yerine eş zamanlı olarak yürütebiliriz. Bir process içinde birden fazla thread olabilir.

731x445

Yani biz multithread bir programlamada aynı process içindeki thread sayısını artıracağız ama multiprocessing bir programlamada kaynak sayısını yani process sayısını artıracağız. Öyleyse her multithreading programlamayı görelim.

1. Multithreading Programlama

Python ile multithreading programlama geliştirmek istiyorsak iki farklı modül kullanabiliriz. Bunlar _thread ve threading modülleridir. Fakat daha üstün bir modül olduğu için bu derste threading kullanacağız.

İlk multi thread yapımızı oluşturalım ve küçük bir örnek yapalım.

import threading

def calistir(threadName): 
    for i in range(7):
        print(threadName ,"çalışıyor")

t1 = threading.Thread(target=calistir, args = ("thread-1", ))
t2 = threading.Thread(target=calistir, args = ("thread-2", ))

t1.start()
t2.start()

Burada kütüphanemizi dahil ettikten sonra Threadlerin çalıştıracağı fonksiyonu oluşturduk. Fonksiyonun tek yaptığı o esnada çalışan thread ismini ekrana yazdırmak. Bu ismi args parametreli tuple veri tipi ile gönderdik. Sizde fonksiyona dışarıdan bir değişken taşımak istediğinizde bu demet içerisine kendi değişkeninizi koyarak yapabilirsiniz. Fakat tek elemanlı demetlerin sonunda virgül olacağını unutmayın!

Fonksiyonumuzu yazdıktan sonra iki adet thread oluşturduk ve target ile çalıştıracakları fonksiyonun ismini belirttik. Son iş olarak ise çalışmaya hazır olan threadlerimizi start ile işleme başlattık. Ekran çıktımız ise şu şekilde oldu:

585x224

Çıktıda gördüğünüz gibi Thread-1 çalışırken araya Thread-2 girmiş. Çünkü her iki thread de artık eş zamanlı olarak çalışıyor. Kim işini bitirirse ekrana çıktıyı basıyor. İşte multithreading programlamanın temeli budur. Unutmadan söyleyelim, yukarıda ki programı her çalıştırdığımızda çıktımız değişebilir çünkü hangi threadin daha önce işini bitireceğini bilemiyoruz.

1.1 Thread Daemon Kavramı

Normal bir python programımız çalıştırıldığında tek bir thread yani main thread oluşturulur ve işlemler bittikten sonra main thread sonlanır. Tabi ardından geriye hiçbir thread kalmadığı için Python programı da sonlandırılır. Multithreading programlamada da yine kaç tane thread varsa her birinin işini bitirmesini beklenir ve ardından iş kalmayınca program sonlandırılır. Peki daemon nedir?

Daemon, oluşturduğumuz threadin işleri bitmese bile programın sonlandırılabileceğini belirtir. Yukarıda normal bir işleyişte program kapanmadan önce tüm threadlerin işlerini bitirmesi beklenir dedik ama daemon parametresi ile bazı threadleri işleri bitmese bile sonlandırılabilir ayarlayabiliriz. Yani özetle main thread sonlandığında daemon thread çalışıyor olsa bile sonlandırılır.

import threading
import time

def zaman(): 
    while True:
        time.sleep(1)
        print(time.ctime(time.time()))

def calistir(threadName):
    for i in range(5):
        time.sleep(1)
        print(threadName ,"çalışıyor")

t1 = threading.Thread(target=zaman, daemon=True)
t2 = threading.Thread(target=calistir, args = ("thread-2", ))

t1.start()
t2.start()

Bu örneğimizde ise t1 isimli thread her saniye ekrana zamanı basarken t2 ise her saniye kendi adını basıyor. Normal bir işleyişte programımız her iki threadin de bitmesini beklemeli ama birinci threadin çalıştırdığı fonksiyonda sonsuz bir while döngüsü olduğu için maalesef ki bu kadar bekleyemez. Bu yüzden ilk threadimizi daemon olarak ayarlıyoruz ve ikinci thread bittiğinde ana programımız bu threadi acımadan sonlandırabiliyor. Ekran çıktımız ise şu şekilde olacaktır.

630x163

1.2 Thread ve Queue Kullanımı

Queue, yani kuyruk yapısı, FIFO (First In First Out) ve LIFO (Last In First Out) mimarilerini kullanan kurtarıcı bir kütüphanedir. Temelde bakıldığında bir diziye eleman ekleme ve eklendiği sıraya göre çıkarma işlemini başarılı bir şekilde yapabilmektedir. Peki threadler ile nasıl kullanabiliriz?

Örneğin bir liste üzerinde çalıştığımızı düşünün. Başlattığımız threadler bu listedeki elemanları alıp işleyecek ve ardından listeden çıkaracaktır. Burada ki sorun bir threadin listedeki bir elemanla çalıştığı anda başka bir threadinde o elemanla çalışabilir olmasıdır. Halbuki biz listedeki bir elemanın bir kere işlenip listeden çıkarılmasını istemiştik. Bu sorunu hemen bir ekran çıktısıyla gösterelim:

import threading, queue

liste = ['Ankara','İstanbul','Kayseri']

def islem(q):
    global liste
    print(f"{q} Çıkarılacak eleman: {liste[len(liste)-1]}")
    liste.pop()


for i in liste:
    for i in range(2):
        worker = threading.Thread(target=islem, args=(f"Thread-{i}",), daemon=True)
        worker.start()

Bu örnekte bir listedeki elemanları 2 farklı thread kullanarak listeden çıkarıyoruz. Fakat bu kodları çalıştırdığımızda birinci thread bir eleman çıkarsa bile diğer thread bu elemanın çıkmamış halindeki listede çalışıyor olabilir. Tıpkı aşağıdaki ekran çıktısında olduğu gibi.

677x64

Öyleyse bu yapıda liste yerine Queue kullanmamız daha doğru olacaktır. Şimdi aynı örneği queue ile yapalım.

import threading, queue

q = queue.Queue()

liste = ['Ankara','İstanbul','Kayseri']

def islem(threadName,q):
    global liste
    while not q.empty():
        item = q.get()
        print(f"{threadName} Çıkarılacak eleman: {item}")        
        q.task_done()

for i in liste:
    q.put(i)

for i in range(2):
    worker = threading.Thread(target=islem, args=(f"Thread-{i}",q), daemon=True)
    worker.start()

q.join()

Burada listemizi FIFO yapısı ile kuyruğa ekledik ve threadlerimize bu kuyruk üzerinde çalışmalarını söyledik. Böylece bir thread gelip kuyruktan eleman aldığında bu elemanı kuyruktan çıkmış oluyor ve diğer thread kuyrukta bu elamanı bulamıyor. Ekran çıktımız ise program kaç defa çalışırsa çalışsın hep aynı sırayla elemanları verecek şekilde olacaktır.

703x52

Kullandığımız komutları ise hızlıca özetlersek;

  • import queue: Queue kütüphanesini projeye ekliyoruz
  • q = queue.Queue(): Kuyruk değişkenimizi oluşturuyoruz
  • q.put(i): Kuyruğa yeni bir eleman eklerken put() fonksiyonu kullanılır
  • item = q.get(): Kuyrukta sıradaki elemanı almak için get() fonksiyonu kullanılır.
  • q.task_done(): En son alınan eleman ile işlemin bittiğini, sıradaki elemanın verilebileceğini belirtir. Kısaca kilit mekanizmasıdır.
  • q.join(): Liste boşalana kadar programın kapanmasına izin vermez. Daemon thread kullanırsanız zorunludur.

Sonuç olarak eğer işlem sırası önemliyse kuyruk yapısını kullanmanız gerekir.

1.3 Threading Modülü Methodları

Threading kütüphanesi birçok kullanışlı yönteme sahiptir. Bu fonksiyonlardan bazıları şunlardır:

  • threading.activeCount() : Aktif olarak çalışan thread sayısını geri döndürür.
  • threading.enumerate() : Aktif olarak çalışan threadlerin listesini geri döndürür.
  • threading.main_thread() : Main threadi geri döndürür.
  • threading.get_ident() : Threadin tanımlayıcısını (identifier) geri döndürür.

Küçük bir örnek üzerinde gösterelim.

import threading,time

def islem(threadName):
    for i in range(5):
        time.sleep(1)
        pass        

threading.Thread(target=islem, args=("Thread-1",)).start()

print(threading.activeCount())  #çalışan thread sayısı
print(threading.enumerate())    #çalışan thread listesi
print(threading.main_thread())  #main thread
print(threading.get_ident())    #thread tanımlayıcısı

Ekran çıktımız şu şekilde olacaktır.

833x69