Python网络爬虫案例实战:并发与 Web:线程
在Python 中使用线程有两种方式:函数或者用类来包装线程对象。
第一种:函数式,调用 thread 模块中的 start_new_thread()函数来产生新线程。语法如下:
import _thread
import time
# 为线程定义一个函数
def print_time(threadName, delay):
count = 0
while count < 5:
time.sleep(delay)
count += 1
print ("%s: %s" % (threadName, time.ctime(time.time())))
# 创建两个线程
try:
_thread.start_new_thread(print_time, ("Thread-1", 2))
_thread.start_new_thread(print_time, ("Thread-2", 4))
except Exception as e:
print ("Error: unable to start thread", e)
# 主线程等待子线程完成
while True:
time.sleep(1)
输出:
Thread-1: Tue Aug 13 17:20:05 2024
Thread-1: Tue Aug 13 17:20:07 2024
Thread-2: Tue Aug 13 17:20:07 2024
Thread-1: Tue Aug 13 17:20:09 2024
Thread-1: Tue Aug 13 17:20:11 2024
Thread-2: Tue Aug 13 17:20:11 2024
Thread-1: Tue Aug 13 17:20:13 2024
Thread-2: Tue Aug 13 17:20:15 2024
Thread-2: Tue Aug 13 17:20:19 2024
线程的结束一般依靠线程函数的自然结束;也可以在线程函数中调用thread.exit(),它抛出 SystemExit exception,达到退出线程的目的。
6.2.1线程模块
Python通过两个标准库thread 和 threading提供对线程的支持。thread 提供了低级别的、原始的线程以及一个简单的锁。
threading 模块提供的其他方法主要有:
threading.currentThread()----返回当前的线程变量。
threading.enumerate()----返回一个包含正在运行的线程的lis,正在运行指线程启动后、结束前,不包括启动前和终止后的线程。
threading,activeCount()----返回正在运行的线程数量,与len(threading,enumerate())
有相同的结果。
除了使用方法外,线程模块同样提供了Thread类来处理线程,Thread类提供了以下方法:
run()用以表示线程活动的方法。
start()启动线程活动。
join([time])等待直到线程终止,阻塞调用线程的join()方法调用终止正常退出或者抛出未处理的异常或者是可选的超时发生。
isAlive()返回线程是否是活动的。
getName()返回线程名。
setName()设置线程名。
6.2.2使用Threading 模块创建线程
使用 Threading 模块创建线程,直接从threading.Thread继承,然后重写_init_方法和run方法。
[例 6-7]使用 Threading创建线程实例。
import threading
import time
exitFlag = 0
class myThread (threading.Thread): #继承父类threading.Thread
def __init__(self, threadID, name, counter):
threading.Thread.__init__(self)
self.threadID = threadID
self.name = name
self.counter = counter
def run(self): #把要执行的代码写到run函数里面 线程在创建后会直接运行run函数
print("Starting " + self.name)
print_time(self.name, self.counter, 5)
print("Exiting " + self.name)
def print_time(threadName, delay, counter):
while counter:
if exitFlag:
(threading.Thread).exit()
time.sleep(delay)
print("%s: %s" % (threadName, time.ctime(time.time())))
counter -= 1
# 创建新线程
thread1 = myThread(1, "Thread-1", 1)
thread2 = myThread(2, "Thread-2", 2)
# 开启线程
thread1.start()
thread2.start()
print("Exiting Main Thread")
输出:
Starting Thread-1
Starting Thread-2
Exiting Main Thread
Thread-1: Tue Aug 13 17:25:02 2024
Thread-2: Tue Aug 13 17:25:03 2024
Thread-1: Tue Aug 13 17:25:03 2024
Thread-1: Tue Aug 13 17:25:04 2024
Thread-2: Tue Aug 13 17:25:05 2024
Thread-1: Tue Aug 13 17:25:05 2024
Thread-1: Tue Aug 13 17:25:06 2024
Exiting Thread-1
Thread-2: Tue Aug 13 17:25:07 2024
Thread-2: Tue Aug 13 17:25:09 2024
Thread-2: Tue Aug 13 17:25:11 2024
Exiting Thread-2
6.2.3线程同步
如果多个线程共同对某个数据进行修改,则可能出现不可预料的结果。为了保证数据的正确性,需要对多个线程进行同步。
使用 Thread对象的Lock 和 Rlock可以实现简单的线程同步,这两个对象都有acquire方法和 release方法,对于那些每次只允许一个线程操作的数据,可以将其操作放到 acquire和release方法之间。
多线程的优势在于可以同时运行多个任务(至少感觉起来是这样)。但是当线程需要共享数据时,可能存在数据不同步的问题。
考虑这样一种情况:一个列表中所有元素都是0,线程set从后向前把所有元素改成1,而线程print负责从前向后读取列表并打印。
那么,可能线程set开始改的时候,线程print便来打印列表了,输出就成了一半0一半1,这就是数据的不同步。为了避免这种情况,引入了锁的概念。
锁有两种状态----锁定和未锁定。每当一个线程比如 set要访问共享数据时,必须先获得锁定;如果已经有其他线程比如print获得锁定了,那么就让线程set暂停,也就是同步阻塞;等到线程print访问完毕,释放锁以后,再让线程set继续。
经过这样的处理,打印列表时要么全部输出0,要么全部输出1,不会再出现一半0一半1的情况。
[例 6-8]线性同步演示实例。
import threading
import time
class myThread (threading.Thread):
def __init__(self, threadID, name, counter):
threading.Thread.__init__(self)
self.threadID = threadID
self.name = name
self.counter = counter
def run(self):
print ("Starting " + self.name)
# 获得锁,成功获得锁定后返回True
# 可选的timeout参数不填时将一直阻塞直到获得锁定
# 否则超时后将返回False
threadLock.acquire()
print_time(self.name, self.counter, 3)
# 释放锁
threadLock.release()
def print_time(threadName, delay, counter):
while counter:
time.sleep(delay)
print("%s: %s" % (threadName, time.ctime(time.time())))
counter -= 1
threadLock = threading.Lock()
threads = []
# 创建新线程
thread1 = myThread(1, "Thread-1", 1)
thread2 = myThread(2, "Thread-2", 2)
# 开启新线程
thread1.start()
thread2.start()
# 添加线程到线程列表
threads.append(thread1)
threads.append(thread2)
# 等待所有线程完成
for t in threads:
t.join()
print("Exiting Main Thread")
输出:
Starting Thread-1
Starting Thread-2
Thread-1: Tue Aug 13 17:27:16 2024
Thread-1: Tue Aug 13 17:27:17 2024
Thread-1: Tue Aug 13 17:27:18 2024
Thread-2: Tue Aug 13 17:27:20 2024
Thread-2: Tue Aug 13 17:27:22 2024
Thread-2: Tue Aug 13 17:27:24 2024
Exiting Main Thread
6.2.4线程池在Web编程的应用
Python有个库叫作 cherrypy,其内核使用的是Python线程池技术。cherrypy通过Python线程安全的队列来维护线程池,具体实现为:
import queue
import time
import threading
SHUTDOWN_REQUEST = object()
class WorkerThread(threading.Thread):
def __init__(self, server):
super().__init__()
self.server = server
self.ready = False
def run(self):
self.ready = True
while True:
request = self.server.thread_pool.queue.get()
if request is SHUTDOWN_REQUEST:
break
self.server.handle_request(request) # 假设服务器有一个处理请求的方法
self.ready = False
class ThreadPool(object):
"""A Request Queue for an HTTPServer which pools threads.
ThreadPool objects must provide min, get(), put(obj), start() and stop(timeout) attributes.
"""
def __init__(self, server, min=10, max=-1, accepted_queue_size=-1, accepted_queue_timeout=10):
self.server = server
self.min = min
self.max = max
self.queue = queue.Queue(maxsize=accepted_queue_size)
self.queue_put_timeout = accepted_queue_timeout
self.threads = []
def start(self):
"""Start the pool of threads."""
for _ in range(self.min):
worker = WorkerThread(self.server)
self.threads.append(worker)
worker.setName(f'CP Server Worker-{worker.getName()}')
worker.start()
for worker in self.threads:
while not worker.ready:
time.sleep(0.1)
def put(self, obj):
"""Put a new request into the queue."""
self.queue.put(obj, block=True, timeout=self.queue_put_timeout)
if obj is SHUTDOWN_REQUEST:
return
def grow(self, amount):
"""Spawn new worker threads (not above self.max)."""
if self.max > 0:
budget = max(self.max - len(self.threads), 0)
else:
# self.max <= 0 indicates no maximum
budget = float('inf')
n_new = min(amount, budget)
workers = [self.spawn_worker() for _ in range(n_new)]
while not all(worker.ready for worker in workers):
time.sleep(0.1)
self.threads.extend(workers)
def shrink(self, amount):
"""Kill off worker threads (not below self.min)."""
if len(self.threads) <= self.min:
return
for _ in range(min(amount, len(self.threads) - self.min)):
self.queue.put(SHUTDOWN_REQUEST)
self.threads.pop().join()
def stop(self, timeout=5):
"""Stop the pool of threads."""
self.shrink(len(self.threads))
for thread in self.threads:
thread.join(timeout)
可以看出,cherrypy的线程池将大小初始化为10,每当有一个httpconnect进来就将其放入任务队列中,然后WorkerThread 会不断从任务队列中取出任务执行,可以看到,这是一个非常标准的线程池模型。