- 全局变量
- Queue消息队列
假设我们现在继续来完成这个爬虫的正常逻辑。
1. 线程间的变量传递
1.1 全局变量
importtimeimportthreading
detail_url_list=[]defget_detail_html():globaldetail_url_listif len(detail_url_list)==0:returnurl=detail_url_list.pop()print("get detail html start :{}".format(url))
time.sleep(2)print("get detail html stop :{}".format(url))defget_detail_url():globaldetail_url_listprint("url start")for i in range(20):
detail_url_list.append("htttp://www.baidu.com/{id}".format(id=i))
time.sleep(2)print("url end")if __name__=="__main__":
start_time=time.time()
thread1= threading.Thread(target=get_detail_url)
thread1.start()for i in range(10):
thread_2=threading.Thread(target=get_detail_html)
thread_2.start()print("lasttime :{}".format(time.time()-start_time))pass
实际上,还可以更方便。将变量作为参数传递,在方法中就不需要global了。
importtimeimportthreading
detail_url_list=[]defget_detail_html(detail_url_list):if len(detail_url_list)==0:returnurl=detail_url_list.pop()print("get detail html start :{}".format(url))
time.sleep(2)print("get detail html stop :{}".format(url))defget_detail_url(detail_url_list):print("url start")for i in range(20):
detail_url_list.append("htttp://www.baidu.com/{id}".format(id=i))
time.sleep(2)print("url end")if __name__=="__main__":
start_time=time.time()
thread1= threading.Thread(target=get_detail_url,args=(detail_url_list,))
thread1.start()for i in range(10):
thread_2=threading.Thread(target=get_detail_html,args=(detail_url_list,))
thread_2.start()print("lasttime :{}".format(time.time()-start_time))pass
但是这样是不能应用于多进程的。
还可以生成一个variables.py文件,直接import这个文件,这种情况变量过多的时候,这种方法比较方便。
但是如果我们直接import变量名,是不能看到其他进程对这个变量的修改的。
但是以上的方法都是线程不安全的操作。想要达到我们要的效果,就必须要加锁。所以除非对锁足够了解,知道自己在干嘛,否则并不推荐这种共享变量的方法来进行通信。
1.2 queue消息队列
a.queue实现上述
importtimeimportthreadingfrom queue importQueuedefget_detail_html(queue):
url=queue.get()print("get detail html start :{}".format(url))
time.sleep(2)print("get detail html stop :{}".format(url))defget_detail_url(queue):print("url start")for i in range(20):
queue.put("htttp://www.baidu.com/{id}".format(id=i))
time.sleep(2)print("url end")if __name__=="__main__":
start_time=time.time()
url_queue=Queue()
thread1= threading.Thread(target=get_detail_url,args=(url_queue,))
thread1.start()for i in range(10):
thread_2=threading.Thread(target=get_detail_html,args=(url_queue,))
thread_2.start()
b.queue是如何实现线程安全的?
我们并不推荐1.1中直接用全局变量的方法,是因为需要我们自己花精力去维护其中的锁操作才能实现线程安全。而python的Queue是在内部帮我们实现了线程安全的。
queue使用了deque deque是在字节码的程度上就实现了线程安全的
c.queue的其他方法
get_nowait(立即取出一个元素,不等待)(异步)
put_nowait(立即放入一个元素,不等待)(异步)
join: 一直block住,从quque的角度阻塞住线程。调用task_done()函数退出。
2.线程间的同步问题
2.1 线程为什么需要同步?同步到底是个啥意思?
这是在多线程中,必须要面对的问题。
例子:我们有个共享变量total,一个方法对total进行加法,一个方法对加完之后的total进行减法。
如果循环对total进行加减的次数比较大的时候,就会比较明显的发现,每次运行的时候,得到的taotal可能是不一样的。
importthreading
total=0defadd():globaltotalfor i in range(100000000):
total+= 1
defdesc():globaltotalfor i in range(100000000):
total= total - 1
if __name__=="__main__":
add_total=threading.Thread(target=add)
desc_total=threading.Thread(target=desc)
add_total.start()
desc_total.start()
add_total.join()
desc_total.join()print(total)
为什么不会像我们希望的最后的total为0呢?
从字节码的角度上看,我们看看简化后的add和desc的字节码。
#input
defadd1(a):
a+= 1
defdesc1(a):
a-= 1
importdisprint(dis.dis(add1))print(dis.dis(desc1))#output
220 LOAD_FAST 0 (a)2 LOAD_CONST 1 (1)4INPLACE_ADD6STORE_FAST 0 (a)8LOAD_CONST 0 (None)10RETURN_VALUE
None250 LOAD_FAST 0 (a)2 LOAD_CONST 1 (1)4INPLACE_SUBTRACT6STORE_FAST 0 (a)8LOAD_CONST 0 (None)10RETURN_VALUE
None
从字节码来看流程为:#1.load a #2.load 1 #3.add #4.赋值给a
任何一步字节码都是有可能被切换出去另外一个线程的字节码去操作a,可能在1线程运行到4字节码(a和1相加)的时候,开始运行2线程的6字节码(赋值给a)。
类似的有银行存取钱、商品库存等也会有这个问题。
2.2 线程如何同步?
用锁将这段代码段锁住,锁住时,不进行切换。直接运行完这段代码段。
a.Lock和Rlock
threading中有提供lock。
importthreadingfrom threading importLock
total=0
lock=Lock()defadd():globaltotalgloballockfor i in range(1000000):
lock.acquire()
total+= 1lock.release()defdesc():globaltotalgloballockfor i in range(1000000):
lock.acquire()
total-= 1lock.release()if __name__=="__main__":
add_total=threading.Thread(target=add)
desc_total=threading.Thread(target=desc)
add_total.start()
desc_total.start()
add_total.join()
desc_total.join()print(total)pass
注意acquire和release成对存在。运行的时候会发现比不加锁的时候慢比较多。所以其实锁的问题也很明显:锁会影响性能,锁会引起死锁。死锁里有个非常常见的问题资源竞争是很容易发生的。
那能不能我锁里套着锁呢?Lock方法是不可以的,但是threading提供了Rlock可重入锁。
Rlock在同一个线程里面,可以连续调用多次acquire,但是注意acquire和release也一定是要成对存在的。
from threading importRLock
total=0
lock=RLock()defadd():globaltotalgloballockfor i in range(1000000):
lock.acquire()
lock.acquire()
total+= 1lock.release()
lock.release()
3.condition使用以及源码分析
condition是条件变量,用于复杂的线程间同步。
3.1 condition的使用
例子:现有一个需求,要求 天猫精灵和小爱一人一句进行对话。如果我们现用lock来实现是没办法做到这边说完一句,那边就说一句的。所以有了condition。
在这个例子中,需要用到condition的两个重要方法 notify()和wait()。notify()用于通知这边动作完成,wait()用于阻塞住等待消息。
#input
importthreadingclassXiaoAi(threading.Thread):def __init__(self,cond):
self.cond=cond
super().__init__(name="小爱")defrun(self):
with self.cond:print("小爱: 天猫在吗 我是小爱")
self.cond.notify()#小爱print完了,信号发送
self.cond.wait() #小爱等待接受信号
print("小爱: 我们来背诗吧")
self.cond.notify()classTianMao(threading.Thread):def __init__(self,cond):
self.cond=cond
super().__init__(name="天猫")defrun(self):
with self.cond:
self.cond.wait()print("天猫: 在 我是天猫")
self.cond.notify()
self.cond.wait()print("天猫: 好啊")
self.cond.notify()if __name__=="__main__":
condition=threading.Condition()
xiaoai=XiaoAi(condition)
tianmao=TianMao(condition)
tianmao.start()
xiaoai.start()#output:
小爱: 天猫在吗 我是小爱
天猫: 在 我是天猫
小爱: 我们来背诗吧
天猫: 好啊
ps:需要注意的是
condition必须先with 再调用 notify和wait方法
这么写的时候,线程的start()顺序很重要
3.2 Condition源码分析
condition其实是有两层锁的。一把底层锁,会在线程调用了wait()的时候释放。
上层锁会在wait()的时候放入双端队列中,在调用notify()的时候被唤醒。
a.condition=threading.Condition()
condition初始化的时候申请了一把锁
b.self.cond.wait()
先释放了condition初始化的时候申请的底层锁,然后又申请了锁放入双端队列。
c. self.cond.notify()
4.信号量 semaphore
是可以用来控制线程执行数量的锁。
4.1 semaphore的使用
需求:现在有个文件,对文件可以进行读和写,但是写是互斥的,读是共享的。并且对读的共享数也是有控制的。
例:爬虫。控制爬虫的并发数。
importthreadingimporttimeclassHtmlSpider(threading.Thread):def __init__(self,url,sem):
super().__init__()
self.url=url
self.sem=semdefrun(self):
time.sleep(2)print("got html text success")
self.sem.release()classUrlProducer(threading.Thread):def __init__(self,sem):
super().__init__()
self.sem=semdefrun(self):for i in range(20):
self.sem.acquire()
html_test=HtmlSpider("www.baidu.com/{}".format(i),self.sem)
html_test.start()if __name__=="__main__":
sem=threading.Semaphore(3) #设置控制的数量为3
urlproducer=UrlProducer(sem)
urlproducer.start()
ps:
每acquire一次,数量就会被减少一,release的时候数量会自动回来。
需要注意sem释放的地方,应该是在HtmlSpider运行完之后进行释放。
4.2 semaphore源码
实际上semaphore就是对condition的简单应用。
a.sem=threading.Semaphore(3)
实际上就是在初始化的时候,调用了Condition。
b.self.sem.acquire()
我们简单看这个逻辑就是,如果设置的数用完了,就让condition进入wait状态,否则就把数量减一。
c.self.sem.release()
release 也是很简单的数量加一和condition的notify。
5.除了上述的对Condition的应用,queue模块中的Queue也对Condition做了更为复杂的应用。特别是queue中的put。
classQueue:def __init__(self, maxsize=0):
self.maxsize=maxsize
self._init(maxsize)
。。。
self.mutex=threading.Lock()
self.not_empty=threading.Condition(self.mutex)
self.not_full=threading.Condition(self.mutex)
self.all_tasks_done=threading.Condition(self.mutex)
self.unfinished_tasks=0def put(self, item, block=True, timeout=None):
with self.not_full:if self.maxsize >0:if notblock:if self._qsize() >=self.maxsize:raiseFullelif timeout isNone:while self._qsize() >=self.maxsize:
self.not_full.wait()elif timeout <0:raise ValueError("'timeout' must be a non-negative number")else:
endtime= time() +timeoutwhile self._qsize() >=self.maxsize:
remaining= endtime -time()if remaining <= 0.0:raiseFull
self.not_full.wait(remaining)
self._put(item)
self.unfinished_tasks+= 1self.not_empty.notify()
。。。。。。