# -*- coding: utf-8 -*-
# @Time : 2019/11/28 16:42
# @Author : John
# @Email : 2398344557@qq.com
# @File : 111.py
# @Software: PyCharm
當(dāng)使用單線程時(shí)袱衷,耗費(fèi)時(shí)間特長
【例】單線程
import time
def run(n):
print("task ", n)
time.sleep(2)
start_time = time.time()
run("1")
run("2")
print(f'執(zhí)行所消耗的時(shí)間:{time.time() - start_time}秒')
# task 1
# task 2
# 執(zhí)行所消耗的時(shí)間:4.00098729133606秒
執(zhí)行上面這個(gè)程序要4秒,如果用多線程的話种玛,函數(shù)“同時(shí)執(zhí)行”藐鹤,只需一半的時(shí)間即可H块堋!娱节!因此我們要引入多線程概念 ==>
【例】啟動(dòng)多個(gè)線程(函數(shù)方式)
import threading
import time
def run(n):
print(f'任務(wù){(diào)n}')
time.sleep(2)
# run('1')
# run('2')
start = time.time()
# t1和t2同時(shí)執(zhí)行
t1 = threading.Thread(target=run, args=('1',))
t2 = threading.Thread(target=run, args=('2',))
t1.start()
t2.start()
# 阻塞作用挠蛉,等t1和t2執(zhí)行完畢后再執(zhí)行下面的內(nèi)容
t1.join()
t2.join()
print('finished')
end = time.time()
print(f'此程序執(zhí)行時(shí)間為:{end-start}秒')
# 任務(wù)1
# 任務(wù)2
# finished
# 此程序執(zhí)行時(shí)間為:2.001213788986206秒
【例】啟動(dòng)多個(gè)線程(類方式)
import threading
import time
class MyThread(threading.Thread):
def __init__(self, n, sleep_time):
super().__init__()
self.n = n
self.sleep_time = sleep_time
def run(self):
print(f'任務(wù){(diào)self.n}開始')
time.sleep(self.sleep_time)
print(f'任務(wù){(diào)self.n}結(jié)束了')
t1 = MyThread(1, 1) # 傳入n=1,sleep_time=1
t2 = MyThread(2, 2) # 傳入n=2肄满,sleep_time=2
t1.start()
t2.start()
t1.join()
t2.join()
print('finished')
# 任務(wù)1開始
# 任務(wù)2開始
# 任務(wù)1結(jié)束了
# 任務(wù)2結(jié)束了
# finished
【例】同時(shí)啟動(dòng)20個(gè)線程
import threading
import time
def run(n):
print(f'任務(wù){(diào)n}開始')
time.sleep(2)
print(f'任務(wù){(diào)n}結(jié)束了')
start_time = time.time()
for i in range(10):
t = threading.Thread(target=run, args=(i,))
t.start()
print("----------all threads has finished...")
print("消耗的時(shí)間:", time.time() - start_time)
# 結(jié)果略
# 分析:
# 主線程直接結(jié)束谴古,沒有等子線程,2s后子線程分別task done
# 代碼共有51個(gè)線程稠歉,一個(gè)主線程與50個(gè)子線程掰担,主線程無法計(jì)算子線程執(zhí)行時(shí)間
# 因此,我們需要設(shè)置主線程等待子線程執(zhí)行結(jié)束怒炸,通過一個(gè)臨時(shí)列表带饱,在線程啟動(dòng)后分別join等待,子線程分別結(jié)束后阅羹,結(jié)束主進(jìn)程勺疼,計(jì)算耗時(shí)約2.011415958s
【例】計(jì)算所有線程執(zhí)行時(shí)間
import threading
import time
def run(n):
print(f'任務(wù){(diào)n}')
time.sleep(2)
print(f'任務(wù){(diào)n}已完成')
start_time = time.time()
t_objs = [] # 存線程實(shí)例
for i in range(10):
t = threading.Thread(target=run, args=(i,))
t.start()
t_objs.append(t) # 為了不阻塞后面線程的啟動(dòng),不在這里join灯蝴,先放到一個(gè)列表里
for t in t_objs:
print(t.is_alive())
for t in t_objs: # 循環(huán)線程實(shí)例列表恢口,等待所有線程執(zhí)行完畢
t.join()
for t in t_objs:
print(t.is_alive()) # is_alive()方法可以用來判斷一個(gè)線程是否結(jié)束,返回True或False
print("----------all threads has finished...")
print("消耗的時(shí)間:", time.time() - start_time)
結(jié)果如下:
任務(wù)0
任務(wù)1
任務(wù)2
任務(wù)3
任務(wù)4
任務(wù)5
任務(wù)6
任務(wù)7
任務(wù)8
任務(wù)9True
True
True
True
True
True
True
True
True
True
任務(wù)1已完成任務(wù)2已完成任務(wù)3已完成
任務(wù)0已完成
任務(wù)7已完成任務(wù)6已完成
任務(wù)5已完成
任務(wù)9已完成
任務(wù)4已完成
任務(wù)8已完成
False
False
False
False
False
False
False
False
False
False
----------all threads has finished...
消耗的時(shí)間: 2.12958025932312
【例】根據(jù)當(dāng)前線程(Thread)活著的數(shù)量來查看線程生命周期
import threading
import time
def sing():
for i in range(3):
print('正在唱歌穷躁。耕肩。。问潭。%d' % i)
# time.sleep(random.random()*3)
time.sleep(1)
def dance():
for i in range(3):
print('正在跳舞猿诸。。狡忙。梳虽。%d' % i)
# time.sleep(random.random()*5)
time.sleep(1)
if __name__=='__main__':
# print('晚會(huì)開始:%s'%time.time())返回的是長串的時(shí)間戳
print('晚會(huì)開始:%s' % time.ctime())
t1 = threading.Thread(target=sing)
t2 = threading.Thread(target=dance)
t1.start()
t2.start()
while True:
length = len(threading.enumerate()) # 枚舉返回個(gè)列表
print('當(dāng)前運(yùn)行的線程數(shù)為:%d' % length)
time.sleep(0.7)
if length <= 1:
break
結(jié)果如下:
晚會(huì)開始:Sun Dec 8 17:12:54 2019
正在唱歌。灾茁。窜觉。。0
正在跳舞北专。禀挫。。拓颓。0
當(dāng)前運(yùn)行的線程數(shù)為:3
當(dāng)前運(yùn)行的線程數(shù)為:3
正在唱歌语婴。。。砰左。1
正在跳舞匿醒。。缠导。廉羔。1
當(dāng)前運(yùn)行的線程數(shù)為:3
正在唱歌。酬核。蜜另。适室。2
正在跳舞嫡意。。捣辆。蔬螟。2
當(dāng)前運(yùn)行的線程數(shù)為:3
當(dāng)前運(yùn)行的線程數(shù)為:3
當(dāng)前運(yùn)行的線程數(shù)為:1
多線程互斥鎖
作用:為了防止不同的線程訪問同一共享資源造成混亂
import threading
# 生成鎖對(duì)象,全局唯一
lock = threading.Lock()
# 獲取鎖汽畴。未獲取到會(huì)阻塞程序旧巾,直到獲取到鎖才會(huì)往下執(zhí)行
lock.acquire()
# 釋放鎖,歸回倘忍些,其他人可以拿去用了
lock.release()
# 注:lock.acquire() 和 lock.release()必須成對(duì)出現(xiàn)鲁猩,否則就有可能造成死鎖
# 可以使用使用上下文管理器來加鎖(常用)
# with 語句會(huì)在這個(gè)代碼塊執(zhí)行前自動(dòng)獲取鎖,在執(zhí)行結(jié)束后自動(dòng)釋放鎖
import threading
lock = threading.Lock()
with lock:
pass
不上鎖的時(shí)候罢坝,對(duì)少量數(shù)據(jù)的修改有一定的作用廓握;
但對(duì)大量數(shù)據(jù)的修改,不上鎖的話會(huì)出現(xiàn)資源競爭問題嘁酿,從而數(shù)據(jù)結(jié)果會(huì)不正確
【例】測(cè)試1:未上鎖前隙券,用多線程對(duì)全局變量進(jìn)程修改
import threading
import time
g_num = 0
def work1(num):
global g_num
for i in range(num):
g_num += 1
print("----in work1, g_num is %d---" % g_num)
def work2(num):
global g_num
for i in range(num):
g_num += 1
print("----in work2, g_num is %d---" % g_num)
print("---線程創(chuàng)建之前g_num is %d---" % g_num)
t1 = threading.Thread(target=work1, args=(100,))
t1.start()
t2 = threading.Thread(target=work2, args=(100,))
t2.start()
while len(threading.enumerate()) != 1:
time.sleep(1)
print("2個(gè)線程對(duì)同一個(gè)全局變量操作之后的最終結(jié)果是:%s" % g_num)
# ---線程創(chuàng)建之前g_num is 0---
# ----in work1, g_num is 100---
# ----in work2, g_num is 200---
# 2個(gè)線程對(duì)同一個(gè)全局變量操作之后的最終結(jié)果是:200
【例】測(cè)試2:未上鎖前,用多線程對(duì)全局變量進(jìn)程修改
import threading
import time
g_num = 0
def work1(num):
global g_num
for i in range(num):
g_num += 1
print("----in work1, g_num is %d---" % g_num)
def work2(num):
global g_num
for i in range(num):
g_num += 1
print("----in work2, g_num is %d---" % g_num)
print("---線程創(chuàng)建之前g_num is %d---"%g_num)
t1 = threading.Thread(target=work1, args=(1000000,))
t1.start()
t2 = threading.Thread(target=work2, args=(1000000,))
t2.start()
while len(threading.enumerate()) != 1:
time.sleep(1)
print("2個(gè)線程對(duì)同一個(gè)全局變量操作之后的最終結(jié)果是:%s" % g_num)
# ---線程創(chuàng)建之前g_num is 0---
# ----in work1, g_num is 1208926---
# ----in work2, g_num is 1264177---
# 2個(gè)線程對(duì)同一個(gè)全局變量操作之后的最終結(jié)果是:1264177
死鎖
在線程間共享多個(gè)資源的時(shí)候闹司,如果兩個(gè)線程分別占有一部分資源并且同時(shí)等待對(duì)方的資源娱仔,就會(huì)造成死鎖,一旦發(fā)生就會(huì)造成應(yīng)用的停止響應(yīng)游桩。
【例】死鎖
#coding=utf-8
import threading
import time
class MyThread1(threading.Thread):
def run(self):
# 對(duì)mutexA上鎖
mutexA.acquire()
# mutexA上鎖后牲迫,延時(shí)1秒,等待另外那個(gè)線程 把mutexB上鎖
print(self.name+'----do1---up----')
time.sleep(1)
# 此時(shí)會(huì)堵塞借卧,因?yàn)檫@個(gè)mutexB已經(jīng)被另外的線程搶先上鎖了
mutexB.acquire()
print(self.name+'----do1---down----')
mutexB.release()
# 對(duì)mutexA解鎖
mutexA.release()
class MyThread2(threading.Thread):
def run(self):
# 對(duì)mutexB上鎖
mutexB.acquire()
# mutexB上鎖后盹憎,延時(shí)1秒,等待另外那個(gè)線程 把mutexA上鎖
print(self.name+'----do2---up----')
time.sleep(1)
# 此時(shí)會(huì)堵塞谓娃,因?yàn)檫@個(gè)mutexA已經(jīng)被另外的線程搶先上鎖了
mutexA.acquire()
print(self.name+'----do2---down----')
mutexA.release()
# 對(duì)mutexB解鎖
mutexB.release()
mutexA = threading.Lock()
mutexB = threading.Lock()
if __name__ == '__main__':
t1 = MyThread1()
t2 = MyThread2()
t1.start()
t2.start()
t1.join()
t2.join()
print('done')
# Thread-1----do1---up----
# Thread-2----do2---up----
# (程序沒有結(jié)束)
# 分析:
# 對(duì)mutexA和mutexB同時(shí)上鎖脚乡,但沒有釋放他們時(shí),那就不能對(duì)他們繼續(xù)訪問,他們現(xiàn)在是阻塞狀態(tài)奶稠。
GIL(全局鎖)
區(qū)分多進(jìn)程和多線程:
(1)多進(jìn)程是真正意義上的并行俯艰,
(2)而多線程只是偽并行(交替執(zhí)行)多線程偽并行(交替執(zhí)行)的原因:
GIL(Global Interpreter Lock,全局解釋器鎖)分析:
任何Python線程執(zhí)行前锌订,必須先獲得GIL鎖竹握,
然后每執(zhí)行100條字節(jié)碼時(shí),解釋器就會(huì)自動(dòng)釋放GIL鎖辆飘,讓別的線程有機(jī)會(huì)執(zhí)行啦辐。
而這個(gè)GIL全局鎖實(shí)際上把所有線程的執(zhí)行代碼都上鎖了。
因此蜈项,多線程在Python中只能交替執(zhí)行芹关,
即使100個(gè)線程拋在100核CPU上,也只能用到1個(gè)核紧卒。注:
GIL并不是Python的特性侥衬,它是實(shí)現(xiàn)Python解釋器(CPython)時(shí)所引入的一個(gè)概念。
而Python解釋器并不只有CPython跑芳,還有PyPy轴总,Psyco,JPython博个,IronPython等我們通常認(rèn)為Python == CPython怀樟,所以也就默許了Python具有GIL鎖
GIL影響性能,如何避免受到GIL影響盆佣?
(1)使用多線程代替多線程(常用)
(2)更換Python解釋器往堡,不使用CPython(不怎么用,因?yàn)镃Pthon挺好用的罪塔,真香~~)
Queue隊(duì)列
控制線程的觸發(fā)執(zhí)行 — — 用Queue隊(duì)列
格式:
from queue import Queue
q = Queue(maxsize=0) # maxsize默認(rèn)為0投蝉,此時(shí)隊(duì)列長度不受限
q.get() # 等待隊(duì)列信息
q.get(timeout=5) # 設(shè)置超時(shí)時(shí)間,時(shí)間到之后執(zhí)行其他
q.put() # 發(fā)送信息
q.join() # 等待所有的消息被消費(fèi)完
【例】生產(chǎn)者消費(fèi)者
import random
import threading
import time
import queue
q = queue.Queue(maxsize=10) # 設(shè)置隊(duì)列的最大長度為10
# 生產(chǎn)者
def producer(name):
count = 1
while True:
q.put("雪碧%s" % count)
print("[Timmy]生產(chǎn)了雪碧", count)
count += 1
time.sleep(random.randrange(3))
# 消費(fèi)者
def consumer(name):
while True:
print("[%s]取到[%s]并且喝了它..." % (name, q.get()))
time.sleep(random.randrange(5))
p = threading.Thread(target=producer, args=("Timmy",))
c1 = threading.Thread(target=consumer, args=("King",))
c2 = threading.Thread(target=consumer, args=("Wang",))
p.start()
c1.start()
c2.start()
結(jié)果如下:
[Timmy]生產(chǎn)了雪碧 1
[Timmy]生產(chǎn)了雪碧 2
[King]取到[雪碧1]并且喝了它...
[Wang]取到[雪碧2]并且喝了它...
[Timmy]生產(chǎn)了雪碧 3
[Wang]取到[雪碧3]并且喝了它...
[Timmy]生產(chǎn)了雪碧 4
[King]取到[雪碧4]并且喝了它...
[Timmy]生產(chǎn)了雪碧 5
[Wang]取到[雪碧5]并且喝了它...
[Timmy]生產(chǎn)了雪碧 6
[Timmy]生產(chǎn)了雪碧 7
[Wang]取到[雪碧6]并且喝了它...
[Wang]取到[雪碧7]并且喝了它...
# (程序沒有結(jié)束)
線程池
存儲(chǔ)線程征堪,需要用的時(shí)候調(diào)出線程
跑完任務(wù)之后瘩缆,這些線程不會(huì)被銷毀,而是返回到線程池等待下一次任務(wù)
- submit():返回一個(gè)future對(duì)象
- future對(duì)象:在未來的某一時(shí)刻完成操作的對(duì)象
【例】創(chuàng)建線程池
import time
from concurrent.futures.thread import ThreadPoolExecutor
# 線程執(zhí)行的函數(shù)
def add(n1,n2):
v = n1 + n2
print('add :', v, ', tid:', threading.currentThread().ident)
time.sleep(n1)
return v
# 通過submit把需要執(zhí)行的函數(shù)扔進(jìn)線程池中.
# submit 直接返回一個(gè)future對(duì)象
ex = ThreadPoolExecutor(max_workers=3) # 制定最多運(yùn)行N個(gè)線程
f1 = ex.submit(add, 2, 3)
f2 = ex.submit(add, 2, 2)
print('main thread running')
print(f1.done()) # future對(duì)象名.done():看看任務(wù)結(jié)束了沒
print(f2.done())
print(f1.result()) # future對(duì)象名.result():獲取結(jié)果 ,阻塞方法
print(f2.result())
print(f2.done())
print(f1.done())
# add : 5 , tid: 6324
# add : 4 , tid: 6712
# main thread running
# False
# False
# 5
# 4
# True
# True
- map():返回是跟你提交序列是一致的佃蚜,是有序的
【例】map()的使用
import requests
from concurrent.futures.thread import ThreadPoolExecutor
URLS = ['http://www.sina.com.cn', 'http://www.baidu.com', 'http://www.qq.com']
def get_html(url):
print('thread id:', threading.currentThread().ident, ' 訪問了:', url)
return requests.get(url) # 這里使用了requests 模塊
ex = ThreadPoolExecutor(max_workers=3) # 制定最多運(yùn)行3個(gè)線程
# thread id: 5596 訪問了: http://www.sina.com.cn
# thread id: 8968 訪問了: http://www.baidu.com
# thread id: 4316 訪問了: http://www.qq.com
# sumbit()函數(shù):
lst = []
for i in range(3):
f = ex.submit(get_html, URLS[i]) # f為future對(duì)象庸娱,提交一個(gè)任務(wù),放入線程池中,準(zhǔn)備執(zhí)行
lst.append(f)
print(lst)
# [<Future at 0x1330990 state=running>, <Future at 0x3015370 state=running>, <Future at 0x3015870 state=running>]
# map()函數(shù):
res_iter = ex.map(get_html, URLS) # 返回生成器res_iter
for res in res_iter:
print(res.url)
# 分別獲取sina、baidu谐算、qq的網(wǎng)頁網(wǎng)址
# https://www.sina.com.cn/
# http://www.baidu.com/
# https://www.qq.com/
for res in res_iter:
print(res.text)
# 分別獲取sina熟尉、baidu、qq的網(wǎng)頁源代碼
# 略
# - as_completed()函數(shù):
from concurrent.futures import as_completed
# 下面用到的f洲脂,是上面第一個(gè)循環(huán)里面的futrue對(duì)象
for future in as_completed([f]): # as_completed()接受一個(gè)可迭代的Future序列斤儿,返回一個(gè)生成器,在完成或異常時(shí)返回這個(gè)Future對(duì)象
print('一個(gè)任務(wù)完成剧包。')
print(future.result())
# thread id: 5484 訪問了: http://www.sina.com.cn
# 一個(gè)任務(wù)完成。
# <Response [200]>
# thread id: 5484 訪問了: http://www.baidu.com
# 一個(gè)任務(wù)完成往果。
# <Response [200]>
# thread id: 10280 訪問了: http://www.qq.com
# 一個(gè)任務(wù)完成疆液。
# <Response [200]>
【例】as_completed 完整的例子
# as_completed 返回一個(gè)生成器,用于迭代陕贮, 一旦一個(gè)線程完成(或失敗) 就返回
import time
import requests
from concurrent.futures.thread import ThreadPoolExecutor
from concurrent.futures import as_completed
URLS = ['http://www.baidu.com', 'http://www.qq.com', 'http://www.sina.com.cn']
def get_html(url):
time.sleep(1)
print('thread id:', threading.currentThread().ident,' 訪問了:', url)
return requests.get(url) # 這里使用了requests 模塊
ex = ThreadPoolExecutor(max_workers=3) # 最多3個(gè)線程
future_tasks = [ex.submit(get_html, url) for url in URLS] #創(chuàng)建3個(gè)future對(duì)象
for future in as_completed(future_tasks): # 迭代生成器
try:
resp = future.result()
except Exception as e:
print('%s' % e)
else:
print('%s has %d bytes!' % (resp.url, len(resp.text)))
# thread id: 4912 訪問了: http://www.sina.com.cn
# thread id: 9060 訪問了: http://www.qq.com
# thread id: 5644 訪問了: http://www.baidu.com
# http://www.baidu.com/ has 2381 bytes!
# https://www.qq.com/ has 223356 bytes!
# https://www.sina.com.cn/ has 543959 bytes!
wait:阻塞函數(shù)
第一個(gè)參數(shù)和as_completed一樣堕油,一個(gè)可迭代的future序列,返回一個(gè)元組,包含2個(gè)set肮之,一個(gè)完成的掉缺,一個(gè)未完成的
【例】wait()函數(shù)的使用
import time
import requests
from concurrent.futures import wait, as_completed, FIRST_COMPLETED, ALL_COMPLETED
from concurrent.futures.thread import ThreadPoolExecutor
import threading
from concurrent import futures
URLS = ['http://www.baidu.com', 'http://www.qq.com', 'http://www.sina.com.cn']
def get_html(url):
"""
FIRST_COMPLETED 當(dāng)任何未來完成或被取消時(shí),該函數(shù)將返回戈擒。
FIRST_EXCEPTION 當(dāng)任何未來通過提出異常完成時(shí)眶明,函數(shù)將返回。如果沒有未來引發(fā)異常峦甩,那么它等同于 ALL_COMPLETED赘来。
ALL_COMPLETED(默認(rèn)) 當(dāng)所有期貨完成或被取消時(shí)现喳,函數(shù)將返回凯傲。
:param url:
:return:
"""
time.sleep(1)
print('thread id:', threading.currentThread().ident, ' 訪問了:', url)
return requests.get(url) # 這里使用了requests 模塊
ex = ThreadPoolExecutor(max_workers=3) # 最多3個(gè)線程
future_tasks = [ex.submit(get_html, url) for url in URLS] # 創(chuàng)建3個(gè)future對(duì)象
try:
result = wait(future_tasks, return_when=futures.ALL_COMPLETED)
done_set = result[0]
for future in done_set:
resp = future.result()
print('第一個(gè)網(wǎng)頁任務(wù)完成 url:%s , len:%d bytes! ' % (resp.url, len(resp.text)))
except Exception as e:
print('exception :', e)
# return_when=futures.FIRST_COMPLETED
# thread id: 11404 訪問了: http://www.sina.com.cn
# thread id: 10196 訪問了: http://www.baidu.com
# thread id: 2588 訪問了: http://www.qq.com
# 第一個(gè)網(wǎng)頁任務(wù)完成 url:http://www.baidu.com/ , len:2381 bytes!
# return_when=futures.FIRST_EXCEPTION
# thread id: 4776 訪問了: http://www.sina.com.cn
# thread id: 12104 訪問了: http://www.baidu.com
# thread id: 10692 訪問了: http://www.qq.com
# 第一個(gè)網(wǎng)頁任務(wù)完成 url:https://www.sina.com.cn/ , len:544154 bytes!
# 第一個(gè)網(wǎng)頁任務(wù)完成 url:https://www.qq.com/ , len:223122 bytes!
# 第一個(gè)網(wǎng)頁任務(wù)完成 url:http://www.baidu.com/ , len:2381 bytes!
# return_when=futures.ALL_COMPLETED
# thread id: 4612 訪問了: http://www.sina.com.cn
# thread id: 11460 訪問了: http://www.qq.com
# thread id: 7744 訪問了: http://www.baidu.com
# 第一個(gè)網(wǎng)頁任務(wù)完成 url:http://www.baidu.com/ , len:2381 bytes!
# 第一個(gè)網(wǎng)頁任務(wù)完成 url:https://www.sina.com.cn/ , len:544154 bytes!
# 第一個(gè)網(wǎng)頁任務(wù)完成 url:https://www.qq.com/ , len:223122 bytes!
回調(diào)函數(shù):add_done_callback(fn)
import os, sys, time, requests, threading
from concurrent import futures
URLS = [
'http://baidu.com',
'http://www.qq.com',
'http://www.sina.com.cn'
]
def load_url(url):
print('tid:', threading.currentThread().ident, ',url:', url)
with requests.get(url) as resp:
return resp.content
def call_back(obj):
print('->>>>>>>>>call_back , tid:', threading.currentThread().ident, ',obj:', obj)
with futures.ThreadPoolExecutor(max_workers=3) as ex:
# mp = {ex.submit(load_url,url) : url for url in URLS}
mp = dict()
for url in URLS:
f = ex.submit(load_url, url)
mp[f] = url
f.add_done_callback(call_back)
for f in futures.as_completed(mp):
url = mp[f]
try:
data = f.result()
except Exception as exc:
print(exc, ',url:', url)
else:
print('url:', url, ',len:', len(data), ',data[:20]:', data[:20])
# tid: 6036 ,url: http://baidu.com
# tid: 6044 ,url: http://www.qq.com
# tid: 3864 ,url: http://www.sina.com.cn
# ->>>>>>>>>call_back , tid: 6036 ,obj: <Future at 0x2f076d0 state=finished returned bytes>
# url: http://baidu.com ,len: 81 ,data[:20]: b'<html>\n<meta http-eq'
# ->>>>>>>>>call_back , tid: 6044 ,obj: <Future at 0x30158b0 state=finished returned bytes>
# url: http://www.qq.com ,len: 237492 ,data[:20]: b'<!doctype html>\n<htm'
# ->>>>>>>>>call_back , tid: 3864 ,obj: <Future at 0x3015c70 state=finished returned bytes>
# url: http://www.sina.com.cn ,len: 544154 ,data[:20]: b'<!DOCTYPE html>\n<!--'