cpython解释器有,pypy解释器就没有GIL。
GIL使得同一时刻,
只有一个线程在cpu上执行字节码
也无法将多个线程映射到多个cpu上
import dis
def add(a):
a = a + 1
return a
print(dis.dis(add))
"""
多线程的时候,一个线程执行了一定的字节码后,会释放GIL锁
其他的线程,会有一个占用这个GIL锁
释放GIL锁的时机:
1.执行了一定的字节码
2.时间片
3.遇到IO操作的时候
"""
"""
每个进程之下,会有多个进程。
对于IO操作为主的操作来说,使用多线程较为划算。
以一个模拟的爬虫为栗子
程序:
1.一个线程去爬虫页面的url
2.一个线程去取出url,去爬取页面
"""
# mythread.py
import time
import threading
def get_detail_html(url):
print("根据爬去的url,进入了一个页面")
time.sleep(2)
print("页面爬去完成")
def get_detail_url(index_url):
print("进入首页")
time.sleep(2)
print("爬取更多的url")
if __name__ == "__main__":
thread1 = threading.Thread(target=get_detail_url,args=("",))
thread2 = threading.Thread(target=get_detail_html, args=("",))
start_time = time.time()
thread1.start()
thread2.start()
print(time.time()-start_time)
"""
虽然我们启动了2个线程
但是其实这个程序有三个线程
还有一个是主线程,就是我们运行的 python mythread.py 这个线程为主线程
可以看到,主线程运行完后,没有退出,而是等待子线程退出后才退出。
t.setDaemon(True) 在t.start()前加入
则主线程运行完,会关闭掉子线程,子线程中断运行,主线程退出。
t.join() 主线程会阻塞,等待线程t执行完成,然后才会向下执行。
"""
# 如果逻辑比较复杂 ,可以继承Thread类,重写run方法 。
# class A(threading.Thread)
"""
首先要了解,线程间问什么要通信。
"""
import time
import threading
detail_url_list = []
def get_detail_html(detail_url_list):
"""进入文章详情页,抓取信息"""
while True:
if len(detail_url_list):
url = detail_url_list.pop()
print("根据爬去的url,进入了一个页面")
time.sleep(2)
print("页面爬去完成")
# for url in detail_url_list:
# 这样做,不是很合理,因为这样就又是串行抓取了
# print("根据爬去的url,进入了一个页面")
# time.sleep(2)
# print("页面爬去完成")
def get_detail_url(detail_url_list):
"""进入文章列表页,抓取文章的url"""
while True:
print("进入首页")
time.sleep(2)
for i in range(20):
detail_url_list.append(f"http://edu/wenzhang/{i}")
print("爬取更多的url")
if __name__ == "__main__":
thread1 = threading.Thread(target=get_detail_url, args=(detail_url_list,))
for i in range(10):
html_thread = threading.Thread(target=get_detail_html, args=(detail_url_list,))
html_thread.start()
thread1.start()
print("~~~~~~~~")
"""
当我们有许多变量需要维护的时候,比如这个list
可以单独写一个点py文件,去存储这个变量 如/目录/variable.py里
from 目录 import variable
variable.url_list 就可以使用了
注意: 不要使用 from 目录.variable import url_list
因为引入url_list后,其他线程修改了后,我们是看不到也不知道被修改了的。
共享变量的也存在很多问题,线程安全问题
比如list.pop 并不是线程安全的,需要加锁。
如果对锁不够了解,不建议用作线程间通信
"""
from queue import Queue
# 设置最大值
q = Queue(maxsize=1000)
# 将q当参数传入
# 放入
q.put(obj) # block=True,timeout=None 、 如果block=False
# 如果为空,会阻塞到这里,q是线程安全的。
q.get()
q.qsize()#长度
q.empty()#判断是否为空
q.full()#判断是否满
# 异步的方法
put_nowait()
get_nowait()
q.task_done() # 会取消join阻塞
q.join() # 会一直阻塞住,如果想退出,必须在某个地方使用task_done方法
"""
首选用q,实现线程间通信
"""
"""
线程同步,是多线程必须要面对的问题
举个栗子
a = 0
Q(a) # 给a+1
E(a) # 给a-1
多线程运行 Q E,多线程是按时间片,字节等因素切换cpu的,
Q执行过程 q1.声明全局变量a q2.赋值 q3.减法操作 q4.再赋值
E同Q e1 e2 e3 e4
如果执行顺序变为 q1 e1 qqee q4/e4
那么a 最终会为 1 或者 -1 就会出错
我们希望两个线程执行完后,数据a为0
实际线上生产环境中,web电商减库存的问题类似
"""
from threading import Lock
#锁
lock = Lock() # 可以将lock当参数传入到多线程中
lock.acquire() # 获取锁
# do something
lock.release() # 释放锁
"""
获取锁,和释放锁 影响性能,这是必然存在的 。
另外锁,会引起死锁的。
lock.acquire()
lock.acquire()
lock.release()
第二个lock.acquire(),会等待第一次lock.acquire()释放,就造成了死锁
死锁另一种情况,
A(a,b) acquire(a) acquire(b)
B(a,b) acquire(b) acquire(a)
现在
A占有a 等待b资源
B占有b 等待a资源
相互等待,造成死锁
"""
from threading import RLock
"""
另外一种会造成死锁
lock.acquire()
dosomethong(lock)
lock.release()
这种时候,就会无意陷入死锁,所以这时候python引入了可重入的锁
"""
block = RLock() # 就可以重入可
import threading
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__":
cond = threading.Condition()
xiaoai = XiaoAi(cond)
tianmao = TianMao(cond)
xiaoai.start()
tianmao.start()
"""
启动顺序很重要
在调用with cond之后才能调用wait或者notify方法
condition有两层锁, 一把底层锁会在线程调用了wait方法的时候释放,
上面的锁会在每次调用wait的时候分配一把并放入到cond的等待队列中,等到notify方法的唤醒
with self.cond: ==
self.cond.acquire()
#智能语音音响说的所有话
self.cond.notify()
self.cond.wait()
self.cond.notify()
self.cond.wait()
self.release()
"""
"""
Semaphore 是用于控制进入数量的锁
文件读写
写一般只是用于一个线程写,
读可以允许有多个
爬虫 限制爬虫的并发数
"""
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("http://localhost/{}".format(i), self.sem)
html_thread.start()
if __name__ == "__main__":
sem = threading.Semaphore(3)
url_producer = UrlProducer(sem)
url_producer.start()
"""
sem = threading.Semaphore(3)
sem.acquire() # 数量会-1,为0会被锁住,停在sem.acquire()
sem.release() # 释放的时候会+1
这样就可以控制 线程的并发数了。
需要注意的就是sem当做参数进行传递, 还有acquire() release() 使用的位置。
Semaphore是Condition的应用(源码解析)
这里很像线程池,使用线程池会更加简单。
"""
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))
"""
t.done() 判断是否完成
t.result() 阻塞的方法,获取返回值
task2.cancel() 取消任务 取消成功返回True,执行中和执行完成是取消不掉的,会返回True
"""
"""
# 线程执行完毕,主线程立即获取返回值,没有顺序的(比较下一小节)
from concurrent.futures import ThreadPoolExecutor,as_completed
executor = ThreadPoolExecutor(max_workers=2)
# 启动三个线程,并将对象放入all_task中
urls = [3,2,4]
all_task = [executor.submit(get_html, (url)) for url in urls]
# 使用 as_completed future.result()获取返回结果
for future in as_completed(all_task):
data = future.result()
"""
"""
# 线程执行完毕,主线程立即获取返回值,顺序不变(比较上一小节)
from concurrent.futures import ThreadPoolExecutor,as_completed
executor = ThreadPoolExecutor(max_workers=2)
# 通过executor的map实现上面的功能
for future in executor.map(get_html,urls):
# urls是一个可迭代对象,每次将值传入func中,线程池中运行
print(future.result())
# 返回的顺序,和url的顺序是一致的
# 和上面的不同,上面的是谁先执行完毕,打印谁。
"""
"""
阻塞,等待某一个或某一些线程执行完成后,才继续向下执行
from concurrent.futures import ThreadPoolExecutor, as_completed, wait
urls = [3,2,4]
all_task = [executor.submit(get_html, (url)) for url in urls]
wait(all_task)
wait 还有一个参数return_when = ALL_COMPLETED 为默认,所有
还有其他参数
有第一个执行完之后,等等,一共四个参数。
"""
import time
from concurrent.futures import ThreadPoolExecutor, as_completed
from concurrent.futures import ProcessPoolExecutor
# 多进程编程
# 耗cpu的操作,用多进程编程, 对于io操作来说, 使用多线程编程,进程切换代价要高于线程
# 对于耗费cpu的操作,多进程由于多线程
# def fib(n):
# if n<=2:
# return 1
# return fib(n-1)+fib(n-2)
#
# if __name__ == "__main__":
# with ThreadPoolExecutor(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))
#2. 对于io操作来说,多线程优于多进程
def random_sleep(n):
time.sleep(n)
return n
if __name__ == "__main__":
with ProcessPoolExecutor(3) as executor:
all_task = [executor.submit(random_sleep, (num)) for num in [2]*30]
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))
"""
from concurrent.futures import ThreadPoolExecutor, as_completed
from concurrent.futures import ProcessPoolExecutor
以上两个 多进程和多线程的 接口是一样的
"""
import os
#fork只能用于linux/unix中
pid = os.fork()
print("bobby")
if pid == 0:
print('子进程 {} ,父进程是: {}.' .format(os.getpid(), os.getppid()))
else:
print('我是父进程:{}.'.format(pid))
"""
想一下,为什么 if else 都会执行,正常情况下不是只执行其中一个吗?
是因为fork创建了一个子进程,
主进程和子进程,拥有相同的环境资源,所以可以看到打印了两边 bobby
这时候 主进程和子进程 都有if else,都会去执行,根据判断结果,执行了不同的代码而已
os.getpid()当前进程 os.getppid()当前运行进程的父进程。
"""
import time
import multiprocessing
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) #None
progress.start()
print(progress.pid)
progress.join()
print("main progress end")
"""
# 指明 进程数为cpu数量
pool = multiprocessing.Pool(multiprocessing.cpu_count())
# 向进程池添加任务并执行
result = pool.apply_async(get_html, args=(3,))
#等待所有任务完成
pool.close() # 关闭进程池,不再接收任务
pool.join() # 回收
# 获取结果
result.get()
"""
进程池imap和imap_unordered
"""
for result in pool.imap(func,[1,6,3]):
print(result)
# 这里result就是return回的值
# 这里返回顺序为列表顺序
for result in pool.imap_unordered(func,[1,6,3]):
print(result)
# 这里谁先完成,谁打印
"""
from multiprocessing import Process, Queue, Pool, Manager, Pipe
# Queue
"""
Queue 用法同线程一样,不过要注意,是进程的Queue,此外queue不能用于pool里。
进程池pool间进程通信 用Manager().Queue(10)
"""
# Pipe 管道
"""
简化版本的Queue,只适用于两个进程
rec_pipe,send_pipe = Pipe()
# as pipe
pipe.send("xxx")
pipe.recv()
和socket有点像
pipe性能问题是高于queue的,某些特定情况,可以优先考虑pipe
"""
# Manager 进程间的共享内存操作
"""
Manager().list() # 等等 可以进去看看,很多,前边线程有的进程Manager都有。
Manager().dict() 参数一样传入就可以了。
"""