一.创建线程的另一种方式(类的继承)
相比较函数而言,使用类创建线程,会比较麻烦一点。 首先,我们要自定义一个类,对于这个类有两点要求,
必须继承 threading.Thread 这个父类;
必须覆写 run 方法。
这里的 run 方法,可以写我们的业务逻辑程序。在 start() 后将会调用。
例1:任务不需要传任何参数
import threading
# 类的继承
class IpThread(threading.Thread):
# 重写构造方法;
def __init__(self, jobname):
super(IpThread, self).__init__()
self.jobname = jobname
# 将多线程需要执行的任务重写到run方法中;
def run(self):
print("this is a job")
t1 = IpThread(jobname="new job")
t1.start()
例2:任务需要传参数
import json
import threading
# 类的继承
from urllib.error import HTTPError
from urllib.request import urlopen
import time
class IpThread(threading.Thread):
# 重写构造方法;如果执行的任务需要传递参数, 那将参数通过构造函数与self绑定;
def __init__(self, jobname, ip):
super(IpThread, self).__init__()
self.jobname = jobname
self.ip = ip
# 将多线程需要执行的任务重写到run方法中;
def run(self):
try:
# 需要有一个参数, 传ip;
url = "http://ip.taobao.com/service/getIpInfo.php?ip=%s" % (self.ip)
# 根据url获取网页的内容, 并且解码为utf-8格式, 识别中文;
text = urlopen(url).read().decode('utf-8')
except HTTPError as e:
print("Error: %s获取地理位置网络错误" %(self.ip))
else:
# 将获取的字符串类型转换为字典, 方便处理
d = json.loads(text)['data']
country = d['country']
city = d['city']
print("%s:" % (self.ip), country, city)
def use_thread():
start_time = time.time()
ips = ['172.25.254.250', '8.8.8.8',
'172.25.254.250', '8.8.8.8',
'172.25.254.250', '8.8.8.8']
threads = []
for ip in ips:
t = IpThread(jobname="爬虫", ip=ip)
threads.append(t)
t.start()
# 等待所有的子线程执行结束
[thread.join() for thread in threads]
print("Success, 运行时间为%s" % (time.time() - start_time))
if __name__ == "__main__":
use_thread()
二.线程同步之线程锁
如果多个线程共同对某个数据修改,则可能出现不可预料的结果,为了保证数据的正确性,需要对多个线程进行同步。使用Thread对象的Lock和Rlock可以实现简单的线程同步,这两个对象都有acquire方法和release方法,对于那些需要每次只允许一个线程操作的数据,可以将其操作放到acquire和release方法之间。
如下:多线程的优势在于可以同时运行多个任务(至少感觉起来是这样)。但是当线程需要共享数据时,可能存在数据不同步的问题。考虑这样一种情况:一个列表里所有元素都是0,线程"set"从后向前把所有元素改成1,而线程"print"负责从前往后读取列表并打印。那么,可能线程"set"开始改的时候,线程"print"便来打印列表了,输出就成了一半0一半1,这就是数据的不同步。为了避免这种情况,引入了锁的概念。
锁有两种状态——锁定和未锁定。每当一个线程比如"set"要访问共享数据时,必须先获得锁定;如果已经有别的线程比如"print"获得锁定了,那么就让线程"set"暂停,也就是同步阻塞;等到线程"print"访问完毕,释放锁以后,再让线程"set"继续。
经过这样的处理,打印列表时要么全部输出0,要么全部输出1,不会再出现一半0一半1的尴尬场面。
用加钱、减钱举一个例子
1.不使用线程锁
import threading
def add():
global money
for i in range(1000000):
money += 1
def reduce():
global money
for i in range(1000000):
money -= 1
if __name__ == '__main__':
money = 0
t1 = threading.Thread(target=add)
t2 = threading.Thread(target=reduce)
t1.start()
t2.start()
# 等待所有子线程执行结束
t1.join()
t2.join()
print("最终金额为:%s" %(money))
不管循环多少次,最终金额都因该为0,但不使用线程锁,当循环基数比较大时,我们发现最终金额是不定的。
2.使用线程锁
import threading
def add(lock):
# 2. 操作变量之前进行加锁
lock.acquire()
global money
for i in range(1000000):
money += 1
# 3. 操作变量完成后进行解锁
lock.release()
def reduce(lock):
# 2. 操作变量之前进行加锁
lock.acquire()
global money
for i in range(1000000):
money -= 1
# 3. 操作变量完成后进行解锁
lock.release()
if __name__ == '__main__':
money = 0
# 1. 实例化一个锁对象
lock = threading.Lock()
t1 = threading.Thread(target=add, args=(lock, ))
t2 = threading.Thread(target=reduce, args=(lock, ))
t1.start()
t2.start()
# 等待所有子线程执行结束
t1.join()
t2.join()
print("最终金额为:%s" %(money))
三.GIL全局解释器锁
GIL并不是Python的特性,它是在实现Python解析器(CPython)时所引入的一个概念,而CPython是大部分环境下默认的Python执行环境。GIL 全称 gloabl interpreter lock (全局解释器锁)
1.要想了解GIL,首先要知道什么是并行和并发?
并发:进行交替处理多件事情。
并行:多个cpu同时处理多个事,只在多核上能实现。
2.执行过程:
1). 设置GIL
2). 切换到线程去运行对应的任务;
3). 运行
- 执行完了
- time.sleep()
- 获取其他信息才能继续执行, eg: 从网络上获取网页信息等;
4). 把线程设置为睡眠状态
5). 解锁GIL
再次重复执行上述内容;
3.Python并不支持真正意义上的多线程。Python中提供了多线程包,但是如果你想通过多线程提高代码的速度,使用多线程包并不是个好主意。Python中有一个被称为Global Interpreter Lock(GIL)的东西,它会确保任何时候你的多个线程中,只有一个被执行。线程的执行速度非常之快,会让你误以为线程是并行执行的,但是实际上都是轮流执行。经过GIL这一道关卡处理,会增加执行的开销。这意味着,如果你想提高代码的运行速度,使用threading包并不是一个很好的方法。
在IO密集型型操作下,多线程还是可以的。比如在网络通信,time.sleep()延时的时候。
在CPU密集型操作下,多线程性能反而不如单线程,此时只能用多进程。
import threading
from mytimeit import timeit
def job(l):
sum(l)
@timeit
def use_thread():
li = range(1,10000)
for i in range(5):
t = threading.Thread(target=job, args=(li, ))
t.start()
@timeit
def use_no_thread():
li = range(1, 10000)
for i in range(5):
job(li)
if __name__ == "__main__":
use_thread()
use_no_thread()
四.队列与多线程
队列以一种先进先出的方式管理数据,如果你试图向一个 已经满了的阻塞队列中添加一个元素或者是从一个空的阻塞队列中移除一个元索,将导致线程阻塞.在多线程进行合作时,阻塞队列是很有用的工具。工作者线程可以定期地把中间结果存到阻塞队列中而其他工作者线线程把中间结果取出并在将来修改它们。队列会自动平衡负载。如果第一个线程集运行得比第二个慢,则第二个 线程集在等待结果时就会阻塞。如果第一个线程集运行得快,那么它将等待第二个线程集赶上来
(理论上多线程执行任务, 会产生一些数据, 为其他程序执行作铺垫;多线程是不能返回任务执行结果的, 因此需要一个容器来存储多线程产生的数据)
import threading
from collections import Iterable
from mytimeit import timeit
from queue import Queue
def job(l, queue):
# 将任务的结果存储到队列中;
queue.put(sum(l))
@timeit
def use_thread():
# 实例化一个队列, 用来存储每个线程执行的结果;
q = Queue()
# # 入队
# q.put(1)
li = [[1,2,3,4,5], [2,3,4,5,6], [2,3,4,5,6,7,8], [2,3,4,5,6]]
threads = []
for i in li:
t = threading.Thread(target=job, args=(i, q))
threads.append(t)
t.start()
# join方法等待所有子线程之心结束
[thread.join() for thread in threads]
# 从队列里面拿出所有的运行结果
result = [q.get() for _ in li]
print(result)
# print(isinstance(q, Iterable))
if __name__ == "__main__":
use_thread()
五.生产者消费者模型
import threading
from urllib.request import urlopen
import requests
def create_data():
with open('ips.txt', 'w') as f:
for i in range(200):
f.write('172.25.254.%d\n' % (i + 1))
def create_url():
portlist = [80, 443, 7001, 7002, 8000, 8080]
with open('ips.txt') as f:
ips = [ip.strip() for ip in f]
urls = ['http://%s:%s' %(ip, port) for ip in ips for port in portlist]
return urls
def job(url):
try:
urlObj = urlopen(url)
except Exception as e:
print("%s unknown url" %(url))
else:
print("%s is ok" % (url))
if __name__ == '__main__':
urls = create_url()
threads = []
for url in urls:
t = threading.Thread(target=job, args=(url,))
threads.append(t)
t.start()
[thread.join() for thread in threads]
import threading
from queue import Queue
from urllib.request import urlopen
class Procuder(threading.Thread):
def __init__(self, q):
super(Procuder, self).__init__()
self.q = q
def run(self):
portlist = [80, 443, 7001, 7002, 8000, 8080]
with open('ips.txt') as f:
ips = [ip.strip() for ip in f]
# urls = ['http://%s:%s' % (ip, port) for ip in ips for port in portlist]
# return urls
for ip in ips:
for port in portlist:
url = 'http://%s:%s' % (ip, port)
self.q.put(url)
class Consumer(threading.Thread):
def __init__(self, q):
super(Consumer, self).__init__()
self.q = q
def run(self):
try:
url = self.q.get()
urlObj = urlopen(url)
except Exception as e:
print("%s unknown url" % (url))
else:
print("%s is ok" % (url))
if __name__ == '__main__':
q = Queue(5)
p1 = Procuder(q)
p1.start()
for i in range(10):
c = Consumer(q)
c.start()
q.join()