python多线程与多进程
1.python中的GIL
python中的一个线程对应于c语言的一个线程(基于cpython)
python前期为了简单,在解释过程中加了一把所,使得一次只能有一个线程运行在1个cpu上
GIL锁使得同一时刻只有一个线程在一个cpu上执行字节码
无法将多个线程映射到多个cpu上
import dis
def foo():
pass
print(dis.dis(foo))
dis模块可以查看函数的字节码
当把多个函数挂在线程上,函数的执行不是完整而独立的
比如函数有1000行字节码,则有可能先执行100行,再切换到别的线程的函数上再执行100行
但每个线程中的函数的字节码的执行是按照顺序的
依此达到某种程度上的同时进行
其实还是在一条主线程上
当遇到io操作,GIL会将执行权给别的线程(io操作慢)
2.多线程编程
一开始只有进程,进程对系统资源消耗大,所以有了线程
线程依赖于进程,线程间切换开销小于进程间切换
class Thread:
"""A class that represents a thread of control.
This class can be safely subclassed in a limited fashion. There are two ways
to specify the activity: by passing a callable object to the constructor, or
by overriding the run() method in a subclass.
"""
'''1.通过Thread类实例化'''
import time
import threading
def get_detail_html(url):
print("get detail html started")
time.sleep(2)
print("get detail html end")
def get_detail_url(url):
print("get detail url started")
time.sleep(4)
print("get detail url end")
if __name__ == '__main__':
thread1 = threading.Thread(target=get_detail_html, args=('', ))
thread2 = threading.Thread(target=get_detail_url, args=('', ))
start_time = time.time()
thread1.start()
thread2.start()
print ("last time: {}".format(time.time()-start_time))
'''2. 通过继承Thread来实现多线程'''
import time
import threading
class GetDetailHtml(threading.Thread):
def __init__(self, name):
super().__init__(name=name)
def run(self):
print("get detail html started")
time.sleep(2)
print("get detail html end")
class GetDetailUrl(threading.Thread):
def __init__(self, name):
super().__init__(name=name)
def run(self):
print("get detail url started")
time.sleep(4)
print("get detail url end")
if __name__ == "__main__":
thread1 = GetDetailHtml("get_detail_html")
thread2 = GetDetailUrl("get_detail_url")
start_time = time.time()
thread1.start()
thread2.start()
'''阻塞线程'''
thread1.join()
thread2.join()
#当主线程退出的时候, 子线程kill掉
print ("last time: {}".format(time.time()-start_time))
3.线程间通信
每个函数的执行涉及到一些外部变量
如果各个函数字节码胡乱执行,可能导致外部变量的值混乱出错
函数的参数之间要实现某种通信
python的参数传递是引用
Queue是线程安全的queue.get()
可以阻塞线程
实际上用了deque实现,deque是字节码级别上的线程安全
#通过queue的方式进行线程间同步
from queue import Queue
import time
import threading
def get_detail_html(queue):
#爬取文章详情页
while True:
url = queue.get()
# for url in detail_url_list:
print("get detail html started")
time.sleep(2)
print("get detail html end")
def get_detail_url(queue):
# 爬取文章列表页
while True:
print("get detail url started")
time.sleep(4)
for i in range(20):
queue.put("http://projectsedu.com/{id}".format(id=i))
print("get detail url end")
#1. 线程通信方式- 共享变量
if __name__ == "__main__":
detail_url_queue = Queue(maxsize=1000)
thread_detail_url = threading.Thread(target=get_detail_url, args=(detail_url_queue,))
for i in range(10):
html_thread = threading.Thread(target=get_detail_html, args=(detail_url_queue,))
html_thread.start()
# # thread2 = GetDetailUrl("get_detail_url")
start_time = time.time()
# thread_detail_url.start()
# thread_detail_url1.start()
#
# thread1.join()
# thread2.join()
detail_url_queue.task_done()'''发出这个信号才会退出queue的join'''
detail_url_queue.join()'''从queue的角度阻塞主线程'''
#当主线程退出的时候, 子线程kill掉
print ("last time: {}".format(time.time()-start_time))
4.线程同步lock、Rlock
a = 0
def add(a):
a += 1
def desc(a):
a -= 1
import dis
print(dis.dis(add))
print(dis.dis(desc))
add(a)
desc(a)
字节码
可见a+=1和a-=1
大致分别有4步操作
5 0 LOAD_FAST 0 (a) 加载a
2 LOAD_CONST 1 (1) 加载1
4 INPLACE_ADD 做加法
6 STORE_FAST 0 (a) 赋给a
8 LOAD_CONST 0 (None)
10 RETURN_VALUE
None
7 0 LOAD_FAST 0 (a)
2 LOAD_CONST 1 (1)
4 INPLACE_SUBTRACT
6 STORE_FAST 0 (a)
8 LOAD_CONST 0 (None)
10 RETURN_VALUE
None
如果线程在函数间随意切换,a极有可能出现问题,比如两个函数通过切换线程先分别先加载了值为0的a,之后又把两个a一个加1一个减1,然后从1和-1中选一个赋给最终的a
在函数中加锁可以只执行在函数被锁住的代码片段的字节码,其间不切换线程
但是加锁和开锁都需要时间,会降低性能
且可能有死锁
1.lock不能嵌套,嵌套会有死锁
2.果两个函数分别需要正在对方函数里的数据,都停住了,结果互相等待,资源竞争,出现死锁
Rlock可以嵌套
from threading import Lock, RLock, Condition #可重入的锁
#在同一个线程里面,可以连续调用多次acquire, 一定要注意acquire的次数要和release的次数相等
total = 0
lock = RLock()
def add():
#1. dosomething1
#2. io操作
# 1. dosomething3
global lock
global total
for i in range(1000000):
lock.acquire()
lock.acquire()
total += 1
lock.release()
lock.release()
def desc():
global total
global lock
for i in range(1000000):
lock.acquire()
total -= 1
lock.release()
import threading
thread1 = threading.Thread(target=add)
thread2 = threading.Thread(target=desc)
thread1.start()
thread2.start()
5.线程同步,condition使用
condition条件变量
用于复杂的线程间同步
如果两个函数的代码要你一段我一段地按顺序执行
锁可以锁住第一段,但不知道下一个获得锁的是哪个函数的代码段
通过condition完成协同读诗
class XiaoAi(threading.Thread):
def __init__(self, cond):
super().__init__(name="小爱")
self.cond = cond
def run(self):
with self.cond:
self.cond.wait()
print("{} : 在 ".format(self.name))
self.cond.notify()
self.cond.wait()
print("{} : 好啊 ".format(self.name))
self.cond.notify()
self.cond.wait()
print("{} : 君住长江尾 ".format(self.name))
self.cond.notify()
self.cond.wait()
print("{} : 共饮长江水 ".format(self.name))
self.cond.notify()
self.cond.wait()
print("{} : 此恨何时已 ".format(self.name))
self.cond.notify()
self.cond.wait()
print("{} : 定不负相思意 ".format(self.name))
self.cond.notify()
class TianMao(threading.Thread):
def __init__(self, cond):
super().__init__(name="天猫精灵")
self.cond = cond
def run(self):
with self.cond:
print("{} : 小爱同学 ".format(self.name))
self.cond.notify()
self.cond.wait()
print("{} : 我们来对古诗吧 ".format(self.name))
self.cond.notify()
self.cond.wait()
print("{} : 我住长江头 ".format(self.name))
self.cond.notify()
self.cond.wait()
print("{} : 日日思君不见君 ".format(self.name))
self.cond.notify()
self.cond.wait()
print("{} : 此水几时休 ".format(self.name))
self.cond.notify()
self.cond.wait()
print("{} : 只愿君心似我心 ".format(self.name))
self.cond.notify()
self.cond.wait()
if __name__ == "__main__":
from concurrent import futures
cond = threading.Condition()
xiaoai = XiaoAi(cond)
tianmao = TianMao(cond)
启动顺序很重要
在调用with cond之后才能调用wait或者notify方法
用了with上下文管理器就不需要acquire和release这个cond了
condition有两层锁, notify之后会找当前唤醒wait的下一个wait,
并把下一个wait加入等候队列、加锁(下一层),然后把当前的wait上的锁(上一层)打开
xiaoai.start()
tianmao.start()
condition内部还是调用了lock、Rlock实现的
notify会开启正在wait的线程
6.Semaphore使用
semaphore是用于控制进入数量的锁
比如爬虫控制并发线程的数量,防止被ban
import threading
import time
class HtmlSpider(threading.Thread):
def __init__(self, url, sem):
super().__init__()
self.url = url
self.sem = sem
def run(self):
time.sleep(2)
print("got html text success")
self.sem.release()
class UrlProducer(threading.Thread):
def __init__(self, sem):
super().__init__()
self.sem = sem
def run(self):
for i in range(20):
self.sem.acquire()
html_thread = HtmlSpider("https://baidu.com/{}".format(i), self.sem)
html_thread.start()
if __name__ == "__main__":
sem = threading.Semaphore(3)
url_producer = UrlProducer(sem)
url_producer.start()
实际上semaphore还是调用了condition来实现的
判断是否已经满足最大并发量,满足了就wait
Queue里边也调用了condition
7.线程池编程
调用from concurrent import futures
线程池并不止提供并发数量限制,还可以获取某一个线程的状态或某一个任务的状态,以及返回值
当一个线程完成时主线程能立即知道
futures可以让多线程和多进程编码接口一致
from concurrent.futures import ThreadPoolExecutor
import time
def get_html(times):
time.sleep(times)
print("get page {} success".format(times))
return times
executor = ThreadPoolExecutor(max_workers=2)
'''通过submit函数提交执行的函数到线程池中, submit 是立即返回'''
task1 = executor.submit(get_html, (3))
task2 = executor.submit(get_html, (2))
'''done方法用于判定某个任务是否完成 非阻塞'''
print(task1.done())
print(task2.cancel())'''取消任务,注意一定要在任务开始前取消 不然取消失败'''
time.sleep(3)
print(task1.done())
'''result方法可以获取task的执行结果 阻塞'''
print(task1.result())
如果要阻塞所有线程并了解已经成功的线程,可以
from concurrent.futures import ThreadPoolExecutor, as_completed
import time
def get_html(times):
time.sleep(times)
print("get page {} success".format(times))
return times
executor = ThreadPoolExecutor(max_workers=2)
'''通过submit函数提交执行的函数到线程池中, submit 是立即返回'''
task1 = executor.submit(get_html, (3))
task2 = executor.submit(get_html, (2))
'''1.用as_complete'''
urls = [3,2,4]
all_task = [executor.submit(get_html, (url)) for url in urls]
for future in as_completed(all_task):
data = future.result()
print("get {} page".format(data))
'''2.通过executor的map获取已经完成的task的值
注意这样输出的顺序是按照列表的url顺序来的'''
for data in executor.map(get_html, urls):
print("get {} page".format(data))
关于wait函数
可以让主线程阻塞,可以指定主线程必须在某一个线程执行完之后再继续
from concurrent.futures import wait, FIRST_COMPLETED
wait(all_task, return_when=FIRST_COMPLETED)
print("main")
8.多进程编程
对于某一些耗cpu的操作,用多进程好
对io操作,用多线程好
进程操作代价大于线程间切换
耗cpu的操作:数学运算、机器学习、挖矿
from concurrent.futures import ProcessPoolExecutor
def fib(n):
if n<=2:
return 1
return fib(n-1)+fib(n-2)
'''在windows下要写入口'''
if __name__ == "__main__":
with ProcessPoolExecutor(3) as executor:
all_task = [executor.submit(fib, (num)) for num in range(25,40)]
start_time = time.time()
for future in as_completed(all_task):
data = future.result()
print("exe result: {}".format(data))
print("last time is: {}".format(time.time()-start_time))
进程会把代码全部拷贝一遍,再从开始进程的地方执行
更加底层的方式:
import multiprocessing
#多进程编程
import time
def get_html(n):
time.sleep(n)
print("sub_progress success")
return n
if __name__ == "__main__":
progress = multiprocessing.Process(target=get_html, args=(2,))
print(progress.pid)
progress.start()
print(progress.pid)
progress.join()
print("main progress end")
使用池:
if __name__ == "__main__":
#使用进程池
pool = multiprocessing.Pool(multiprocessing.cpu_count())
result = pool.apply_async(get_html, args=(3,))
#等待所有任务完成
pool.close()
pool.join()
print(result.get())
imap方法:
加_unordered无序输出,不加按列表序输出
if __name__ == "__main__":
for result in pool.imap_unordered(get_html, [1,5,3]):
print("{} sleep success".format(result))
9.进程间通信
不能用普通的Queue,要用multiprocessing的Queue
共享全局变量不能适用于多进程
multiprocessing 中的 queue 不能用于 pool进程池
要用 multiprocessing 中的 manager 中的 queue
import time
from multiprocessing import Process, Queue, Pool, Manager, Pipe
def producer(queue):
queue.put("a")
time.sleep(2)
def consumer(queue):
time.sleep(2)
data = queue.get()
print(data)
if __name__ == "__main__":
queue = Manager().Queue(10)
pool = Pool(2)
pool.apply_async(producer, args=(queue,))
pool.apply_async(consumer, args=(queue,))
pool.close()
pool.join()
通过pipe管道进行通信
pipe只能适用于两个进程
pipe的性能高于queue
def producer(pipe):
pipe.send("bobby")
def consumer(pipe):
print(pipe.recv())
if __name__ == "__main__":
recevie_pipe, send_pipe = Pipe()
#pipe只能适用于两个进程
my_producer= Process(target=producer, args=(send_pipe, ))
my_consumer = Process(target=consumer, args=(recevie_pipe,))
my_producer.start()
my_consumer.start()
my_producer.join()
my_consumer.join()
使用内存共享,多个子进程修改主进程间的变量
def add_data(p_dict, key, value):
p_dict[key] = value
if __name__ == "__main__":
progress_dict = Manager().dict()
from queue import PriorityQueue
first_progress = Process(target=add_data, args=(progress_dict, "bobby1", 22))
second_progress = Process(target=add_data, args=(progress_dict, "bobby2", 23))
first_progress.start()
second_progress.start()
first_progress.join()
second_progress.join()
print(progress_dict)