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.
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 Thread
lerin ç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:
Çı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.
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.
Ö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.
Kullandığımız komutları ise hızlıca özetlersek;
import queue
: Queue kütüphanesini projeye ekliyoruzq = queue.Queue()
: Kuyruk değişkenimizi oluşturuyoruzq.put(i)
: Kuyruğa yeni bir eleman eklerken put() fonksiyonu kullanılıritem = 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.