Python中的多线程
在Python中,你可以启动一个线程,但却无法停止它。
内容:threading模块,和queue模块的结合使用及实践。
###一、简介:
多线程对于具有以下特点的编程任务是非常理想的:本质上是异步的;需要多个并发活动;每个活动的处理顺序可能是不确定的,或者说是随机的、不可预测的。这种编程任务可以被组织或划分成多个执行流,其中每个执行流都有一个指定要完成的任务。
###二、线程和进程
“进程——资源分配的最小单位,线程——程序执行的最小单位”
1、进程:进程是表示资源分配的基本单位,又是调度运行的基本单位。是一个执行中的程序。每个进程都拥有自己的地址空间、内存、数据栈。进程可以通过派生新的对象来执行其他任务,不过新进程也拥有自己的内存和数据栈,只能能用进程间通信(IPC)的方式共享信息。
2、线程:线程是进程的一个执行流,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。一个进程由几个线程组成(拥有很多相对独立的执行流的用户程序共享应用程序的大部分数据结构),线程与同属一个进程的其他的线程共享进程所拥有的全部资源。
总的来说就是:进程有独立的地址空间,线程没有单独的地址空间(同一进程内的线程共享进程的地址空间)
线程的好处:
- 开销少,创建线程比创建进程要快,所以开销很少。
- 线程间方便的通信机制。
- 易于调度,提高并发性,通过线程可方便有效地实现并发性。进程可创建多个线程来执行同一程序的不同部分。
- 充分发挥多处理器的功能,通过创建多线程进程(即一个进程可具有两个或更多个线程),每个线程在一个处理器上运行,从而实现应用程序的并发性,使每个处理器都得到充分运行。
线程和进程的关系
- 一个线程只属于一个进程,一个进程可以有多个线程。线程是操作系统中可识别的最小执行和调度单位。
- 资源分配给进程,同一进程的所有线程共享该进程的所有资源。
- 处理机分给线程,即真正在处理机上运行的是线程。
- 线程在执行过程中,需要协作同步。不同进程的线程间要利用消息通信的办法实现同步。
三、线程和Python
Python代码的执行由Python虚拟机(解释器主循环)进行控制的。Python的设计是,在主循环中同时只能由一个控制线程在执行,Python解释器中可以运行多个线程,但是在任意给定时刻只有一个线程会被解释器执行,是由全局解释器锁(GIL)控制的。
I/O 密集型的 Python 程序要比计算密集型的代码能够更好地利用多线程 环境。
###四、Python的Threading模块
多线程模块Thread、Threading、queue模块等。threading模块来创建与管理线程。queue用于线程间数据共享。
**不推荐使用thread模块:**它对于进程何时退出没有控制。当主线程结束 时,所有其他线程也都强制结束,不会发出警告或者进行适当的清理。
守护线程
threading模块支持守护线程:守护线程一般是一个等待客户端请求服务的服务器。如果没有客户端请求,守护线程就是空闲的。如果把一个线程设置为守护线程,就表示这个线程是不重要的,进程退出时不需要等待这个线程执行完成。
####Thread类
属性 | 描述 |
---|---|
Thread对象的属性 | |
name | 线程名 |
ident | 线程标示符 |
daemon | 布尔值,表示线程是否是守护线程 |
Thread对象方法 | |
init(target=None, name=None,args=(),daemon=None) | 必须参数target、args。还可以传递name,daemon的值。 |
start() | 开始执行线程 |
run() | 定义线程功能的方法 |
jion(timeout=None) | 等待启动的线程终止之前一直挂起,除非给了timeout(秒),否则一直会阻塞。 |
getName() | 返回线程名字 |
setName() | 设定线程名 |
is_alive() | 表示线程的存货 |
isDaemon() | 判断是否守护线程 |
创建线程的方法:
- 创建Thread的实例,传给他一个函数。
- 派生Thread的子类,并创建子类的实例。
1、创建Thread实例,并传递给它一个函数
import threading
from time import sleep, ctime
loops = [4, 2] # 睡眠的秒
def loop(nloop, sec):
print('start loop', nloop, 'at:', ctime())
sleep(sec)
print('loop', nloop, 'done at:', ctime())
def main():
threads = []
nloops = range(len(loops)) # 总共有两个线程
for i in nloops:
t = threading.Thread(target=loop, args=(i, loops[i]))
threads.append(t)
for i in nloops:
threads[i].start() # 线程开始执行
for i in nloops:
threads[i].join() # 等待线程结束,或者达到超时时间。
if __name__ == '__main__':
main()
结果为:
start loop 0 at: Sat Jul 7 22:45:21 2018
start loop 1 at: Sat Jul 7 22:45:21 2018
loop 1 done at: Sat Jul 7 22:45:23 2018
loop 0 done at: Sat Jul 7 22:45:25 2018
####2、派生Thread的子类,并创建子类的实例。
import threading
from time import sleep, ctime
class MyThread(threading.Thread):
def __init__(self, func, args, name=''):
threading.Thread.__init__(self)
self.name = name
self.func = func
self.args = args
def run(self):
self.func(*self.args)
loops = [4, 2]
def loop(nloop, nsec):
print('start loop', nloop, 'at:', ctime())
sleep(nsec)
print('loop', nloop, 'done at:', ctime())
def main():
threads = []
nloops = range(len(loops))
for i in nloops:
t = MyThread(loop, (i, loops[i]))
threads.append(t)
for i in nloops:
threads[i].start()
for i in nloops:
threads[i].join()
if __name__ == '__main__':
main()
结果为:
start loop 0 at: Sat Jul 7 22:50:54 2018
start loop 1 at: Sat Jul 7 22:50:54 2018
loop 1 done at: Sat Jul 7 22:50:56 2018
loop 0 done at: Sat Jul 7 22:50:58 2018
区别在于:MyThread()继承threading.Thread,继承中调用的方法为run()。
####threading 模块的其他函数
函数 | 描述 |
---|---|
active_count() | 当前活动Thread对象的个数 |
current_thread() | 返回当前Thread对象 |
enumerate() | 返回当前会懂的Thread对象列表 |
由于 Python 虚拟机是单线程(GIL)的原因,只有线程在执行 I/O 密集 型的应用时才能更好地发挥 Python 的并发性(对比计算密集型应用,它只需要做轮询)
from threading import Thread
def fun(n):
while n:
n -= 1
N = 100000000
threads = []
threads.append(Thread(target=fun, args=(N,)).start())
threads.append(Thread(target=fun, args=(N,)).start())
所以上面的计算密集的方法,使用进程执行两次和直接执行两次并没有减少时间。
多线程爬取当当网图书,并分析排名
import requests
import atexit
import threading
import json
import re
import time
# 爬取当当网图书分析
URL = 'http://search.dangdang.com' # 爬取的地址
RANK_URL = 'http://product.dangdang.com/index.php?r=callback/get-bang-rank&productId={0}'
params = { # 爬取的内容
'key': 'python',
'act': 'input'
}
def get_book():
response = requests.get(url=URL, params=params).text
return re.finditer(r'<a title=" (.*)?" ddclick.*?dangdang\.com/(\d{8,10}?).html', response) # 返回一个迭代器对象
def show_book(book):
res = requests.get(url=RANK_URL.format(book[1])).text # 获取排名的链接
if res == '': # 改书没有排名
book_rank = None
else:
res = json.loads(res)
book_rank = res['data']['rank'] # 获取书的排名
print(f'name:{book[0]} rank:{book_rank}')
def get_rank():
for book in get_book():
threading.Thread(target = show_book, args=(book.groups(),)).start()
@atexit.register # 退出函数,脚本退出之前调用。因为不是守护线程不需要调用jion
def end():
print(time.ctime())
if __name__ == '__main__':
print(time.ctime())
get_rank()
在不使用多线程的情况下要8秒左右,使用多线程速度在1秒以内。
####锁的实例
from random import randrange
from threading import Thread, currentThread, Lock
from time import ctime, sleep
class CleanOutputSet(set): # 继承集合
def __str__(self): # 重写输出方法
return ','.join(x for x in self)
loops = (randrange(2, 5) for x in range(4)) # 睡眠的时间
remaining = CleanOutputSet()
look = Lock()
def loop(nsec):
myname = currentThread().name
look.acquire() # 获取锁
remaining.add(myname)
print(f'{ctime()} Started {myname}')
look.release() # 释放锁
sleep(nsec)
look.acquire() # 获取锁
remaining.remove(myname)
print(f'{ctime()} Completed {myname}')
print('(remaining: %s)' % (remaining or 'NONE'))
look.release() # 释放锁
def main():
for pause in loops:
Thread(target=loop, args=(pause,)).start()
main()
结果:
Sat Jul 7 23:13:52 2018 Started Thread-1
Sat Jul 7 23:13:52 2018 Started Thread-2
Sat Jul 7 23:13:52 2018 Started Thread-3
Sat Jul 7 23:13:52 2018 Started Thread-4
Sat Jul 7 23:13:55 2018 Completed Thread-4
(remaining: Thread-3,Thread-1,Thread-2)
Sat Jul 7 23:13:55 2018 Completed Thread-3
(remaining: Thread-1,Thread-2)
Sat Jul 7 23:13:56 2018 Completed Thread-1
(remaining: Thread-2)
Sat Jul 7 23:13:56 2018 Completed Thread-2
(remaining: NONE)
####使用上下文来管理锁
from random import randrange
from threading import Thread, currentThread, Lock
from time import ctime, sleep
class CleanOutputSet(set): # 继承集合
def __str__(self): # 重写输出方法
return ','.join(x for x in self)
loops = (randrange(2, 5) for x in range(4)) # 睡眠的时间
remaining = CleanOutputSet()
look = Lock()
def loop(nsec):
myname = currentThread().name
with look:
remaining.add(myname)
print(f'{ctime()} Started {myname}')
sleep(nsec)
with look:
remaining.remove(myname)
print(f'{ctime()} Completed {myname}')
print('(remaining: %s)' % (remaining or 'NONE'))
def main():
for pause in loops:
Thread(target=loop, args=(pause,)).start()
main()
和上面锁的程序一样,只是使用了with上下文管理器。
###五、生产者-消费者问题和queue模块
queue模块的介绍:链接
from random import randint
from time import sleep
from queue import Queue
import threading
class MyThread(threading.Thread):
def __init__(self, func, args, name=''):
threading.Thread.__init__(self)
self.name = name
self.func = func
self.args = args
def getResult(self):
return self.res
def run(self):
self.res = self.func(*self.args)
def writeQ(queue): # 写入队列内容
queue.put('xxx', 1)
print('writer Q... size now', queue.qsize())
def readQ(queue): # 读出队列内容
val = queue.get(1)
print('reader Q... size now', queue.qsize())
def writer(queue, loops): # 向队列中放入对象,并等待几秒
for i in range(loops):
writeQ(queue)
sleep(randint(1, 2)) # 写入的睡眠比读取的睡眠要短,防止消费对象为空
def reader(queue, loops): # 从队列中读出对象,并等待几秒
for i in range(loops):
readQ(queue)
sleep(randint(3, 5))
funcs = [writer, reader]
nfuncs = range(len(funcs))
def main():
nloops = randint(2, 5) # 随机设置的执行次数
q = Queue(32)
threads = []
for i in nfuncs:
t = MyThread(funcs[i], (q, nloops), funcs[i].__name__)
threads.append(t)
for i in nfuncs:
threads[i].start()
for i in nfuncs:
threads[i].join()
print('ALL DONE')
main()
输出结果:
writer Q... size now 1
reader Q... size now 0
writer Q... size now 1
writer Q... size now 2
reader Q... size now 1
writer Q... size now 2
reader Q... size now 1
reader Q... size now 0
ALL DONE