Основи роботи з потоками в Python (исходники), Різне, Програмування, статті

Введення


Потоки дозволяють додаткам виконувати в один і той же час безліч завдань. Нить (multi-threading) важлива в безлічі додатків, від примітивних серверів до сучасних складних і ресурсномістких ігор, так що, природно, багато мов програмування підтримують позможность роботи з потоками. Python теж входить в їх число.

Однак, підтримка багатопоточності в Python не обходиться без обмежень і наслідків, як писав Гвідо ван Россум:


«На жаль, для більшості смертних програмування потоків просто Занадто Складне, щоб робити його правильно … Навіть в Python – всякий раз, як хтось серйозно береться за програмування потоків, на мене обрушуються тонни повідомлень про помилки, причому якщо причиною половини з них дійсно є помилки в інтерпретаторі Python, то причина другої половини криється в недостатньому розумінні особливостей багатопоточності … »

Перш ніж ми приступимо до розбору коду, що працює з потоками, нам потрібно розглянути найбільш важливу річ – глобальну блокування інтерпретатора (global interpreter lock, GIL) Python. Якщо два або більше потоку спробують маніпулювати одним і тим же об’єктом в один і той же час, то неминуче виникнуть проблеми. Глобальна блокування інтерпретатора виправляє це. В будь-який момент часу дії може виконувати тільки один потік. Python автоматично перемикається між потоками, коли в цьому виникає необхідність.


Використання модуля Threading


Модуль threading надає нам простий спосіб роботи з потоками. Його клас Thread може бути успадкований (subclassed) для створення потоку або декількох потоків. Метод run повинен містити код, який ви бажаєте виконати при виконанні потоку. Звучить просто, чи не так? Ось, подивіться:
import threading
class MyThread(threading.Thread):
def run(self):
print “Insert some thread stuff here.”
print “It”ll be executed…yeah….”
print “There”s not much to it.”

Виконати потік також просто. Все, що нам потрібно зробити, це створити екземпляр нашого класу потоку, після чого викликати його метод start:


import threading
class MyThread(threading.thread):
def run(self):
print “You called my start method, yeah.”
print “Were you expecting something amazing?”
MyThread().start()

Звичайно, всього один потік це не бозна що. Як і люди, потоки через деякий час залишаються на самоті. Давайте створимо групу потоків:


import threading
theVar = 1
class MyThread(threading.Thread):
def run ( self ):
global theVar
print “This is thread ” + str(theVar) + ” speaking.”
print “Hello and good bye.”
theVar = theVar + 1
for x in xrange ( 20 ):
MyThread().start()

Давайте тепер зробимо за допомогою модуля threading щось умовно-корисне. Сервера часто використовують потоки для роботи в один і той же час з декількома клієнтами. Давайте створимо простий, але розширюваний сервер. Коли клієнт підключиться до нього, сервер створить новий потік для обслуговування цього клієнта. Щоб відправляти потоку дані клієнта нам знадобиться перекрити метод __init__ класу Thread, Щоб він приймав параметри. Відтепер сервер буде відправляти потік своєю дорогою і чекати нових клієнтів. Кожен потік буде посилати упакований (pickled) об’єкт відповідному клієнту, після чого друкувати не більше десяти рядків, отриманих від клієнта. (Упакований об’єкт в загальному випадку є об’єктом, зменшеним до декількох символів. Це корисно при збереженні об’єктів для подальшого використання або для передачі об’єктів по мережі).


 import pickle
import socket
import threading
# We”ll pickle a list of numbers:
someList = [1, 2, 7, 9, 0]
pickledList = pickle.dumps(someList)
# Our thread class:
class ClientThread(threading.Thread):
# Override Thread”s __init__ method to accept the parameters needed:
def __init__(self, channel, details):
self.channel = channel
self.details = details
threading.Thread.__init__(self)
def run(self):
print “Received connection:”, self.details[0]
self.channel.send(pickledList)
for x in xrange(10):
print self.channel.recv(1024)
self.channel.close()
print “Closed connection:”, self.details[0]
# Set up the server:
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind((“”, 2727))
server.listen(5)
# Have the server serve “forever”:
while True:
channel, details = server.accept()
ClientThread(channel, details).start()

Тепер нам потрібно створити клієнта, який буде підключатися до сервера, отримувати від нього упакований об’єкт, розпаковувати (reconstructs) об’єкт і, нарешті, посилати десять повідомлень і закривати підключення:


 import pickle
import socket
# Connect to the server:
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect((“localhost”, 2727))
# Retrieve and unpickle the list object:
print pickle.loads(client.recv(1024))
# Send some messages:
for x in xrange(10):
client.send(“Hey. ” + str(x) + “n”)
# Close the connection
client.close()

Звичайно, наведений вище клієнт не в змозі скористатися всіма перевагами багатопоточності нашого сервера. Клієнт породжує тільки один потік, що робить багатопоточність безглуздою. Давайте додамо клієнту багатопоточності, щоб зробити все цікавішим. Кожен потік буде підключатися до сервера і виконувати наведений вище код:


import pickle
import socket
import threading
# Here”s our thread:
class ConnectionThread(threading.Thread):
def run(self):
# Connect to the server:
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect ((“localhost”, 2727))
# Retrieve and unpickle the list object:
print pickle.loads(client.recv(1024))
# Send some messages:
for x in xrange(10):
client.send(“Hey. ” + str(x) + “n”)
# Close the connection
client.close()
# Let”s spawn a few threads:
for x in xrange(5):
ConnectionThread().start()

Пули потоків (pooling threads)


Важливо пам’ятати, що потоки не з’являються миттєво. Створення великої їх числа може сповільнити ваш додаток. Щоб створити потік і, пізніше, знищити його, потрібен час. Потоки можуть також споживати багато цінних системних ресурсів у великих додатках. Ця проблема легко вирішується шляхом створення обмеженого числа потоків (set number of threads) (пулу потоків) і призначення ним нових завдань, загалом, повторного їх використання. З’єднання будуть прийматися і передаватися того потоку, який раніше за всіх закінчить роботу з попереднім клієнтом.

Якщо ви як і раніше не розумієте, порівняйте це з лікарнею. Скажімо, у нас є п’ятеро лікарів. Це наші потоки. Пацієнти (клієнти) приходять в лікарню і, якщо лікарі зайняті, сидять в приймальному покої.


Очевидно, нам потрібно щось, що зможе передавати дані клієнта в наші потоки, не викликаючи при цьому проблем (воно має бути «потокобезпечна»). Модуль Queue Python робить це для нас. Клієнтська інформація зберігається в об’єкті Queue, Звідки потоки витягають її в міру потреби.


Давайте переробимо наш сервер, щоб оцінити переваги пулу потоків:


 import pickle
import Queue
import socket
import threading
# We”ll pickle a list of numbers, yet again:
someList = [1, 2, 7, 9, 0]
pickledList = pickle.dumps(someList)
# A revised version of our thread class:
class ClientThread(threading.Thread):
# Note that we do not override Thread”s __init__ method.
# The Queue module makes this not necessary.
def run(self):
# Have our thread serve “forever”:
while True:
# Get a client out of the queue
client = clientPool.get()
# Check if we actually have an actual client in the client variable:
if client != None:
print “Received connection:”, client[1][0]
client[0].send(pickledList)
for x in xrange(10):
print client[0].recv(1024)
client[0].close()
print “Closed connection:”, client[1][0]
# Create our Queue:
clientPool = Queue.Queue(0)
# Start two threads:
for x in xrange(2):
ClientThread().start()
# Set up the server:
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind((“”, 2727))
server.listen(5)
# Have the server serve “forever”:
while True:
clientPool.put(server.accept())

Як ви можете побачити, він трохи складніше нашого попереднього сервера, але не ускладнений до повної незрозумілості. Для перевірки цього сервера, так само, як і попереднього, можна скористатися клієнтом з попереднього розділу.


Додаткові хитрощі


Робота з потоками не полягає лише в їх створенні і знищенні. Модуль (може, не модуль а клас?) Thread з модуля threading містить ще кілька методів, які можуть вам стати в нагоді. Перші два призначені для іменування потоків. Метод setName присвоює потоку ім’я, а метод getName повертає ім’я потоку:
 import threading
class TestThread(threading.Thread):
def run(self):
print “Hello, my name is”, self.getName()
cazaril = TestThread()
cazaril.setName(“Cazaril”)
cazaril.start()
ista = TestThread()
ista.setName(“Ista”)
ista.start()
TestThread().start()

Нічого дивного. Також, як ви можете бачити, у потоків є імена, навіть якщо ви їх не задавали.


Ми також можемо перевірити, чи є потік «живим», скориставшись методом isAlive. Якщо потік ще не закінчив виконуватися, незалежно від того, що відбувається в його методі run, То він класифікується як «живий»:


 import threading
import time
class TestThread(threading.Thread):
def run(self):
print “Patient: Doctor, am I going to die?”
class AnotherThread(TestThread):
def run (self):
TestThread.run(self)
time.sleep(10)
dying = TestThread()
dying.start()
if dying.isAlive():
print “Doctor: No.”
else:
print “Doctor: Next!”
living = AnotherThread()
living.start()
if living.isAlive():
print “Doctor: No.”
else:
print “Doctor: Next!”

Другий потік залишається в живих, оскільки ми змусили його чекати, скориставшись методом sleep модуля time.


Якщо нам потрібно, щоб потік дочекався завершення іншого потоку, можна скористатися методом join:


 import threading
import time
class ThreadOne(threading.Thread):
def run(self):
print “Thread”, self.getName(), “started.”
time.sleep ( 5 )
print “Thread”, self.getName(), “ended.”
class ThreadTwo(threading.Thread):
def run(self):
print “Thread”, self.getName(), “started.”
thingOne.join()
print “Thread”, self.getName(), “ended.”
thingOne = ThreadOne()
thingOne.start()
thingTwo = ThreadTwo()
thingTwo.start()

Ми також можемо використовувати метод setDaemon. Якщо при виклику в нього передається значення True та інші потоки завершили своє виконання, то з основної програми буде зроблений вихід, а потік продовжить роботу:


 import threading
import time
class DaemonThread(threading.Thread):
def run(self):
self.setDaemon(True)
time.sleep(10)
DaemonThread().start()
print “Leaving.”

Python також містить модуль thread, Що працює на більш низькому рівні, ніж threading. Хочу звернути вашу увагу на одну особливість: це міститься в ньому функція start_new_thread. Використовуючи її ми можемо перетворити звичайну функцію в потік:


 import thread
def thread(stuff):
print “I”m a real boy!”
print stuff
thread.start_new_thread(thread, (“Argument”))

Висновок


Про багатопоточності можна розповісти значно більше, ніж я зробив у цій статті, але я не буду намагатися осягнути неосяжне. Крім того, як згадав Гвідо ван Россум, переваги, які дає складна багатопоточність в Python можуть бути зведені нанівець наслідками. Проте, невелика доза здорового глузду може усунути більшість проблем в простій багатопоточності.

Нить дуже важлива, коли справа стосується комп’ютерних програм і, як я згадував раніше, Python її підтримує. За умови правильного використання, ефект від застосування потоків може бути дуже благотворним і часто навіть критичним, як я наголошував у цій статті.


Схожі статті:


Сподобалася стаття? Ви можете залишити відгук або підписатися на RSS , щоб автоматично отримувати інформацію про нові статтях.

Коментарів поки що немає.

Ваш отзыв

Поділ на параграфи відбувається автоматично, адреса електронної пошти ніколи не буде опублікований, допустимий HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*

*