多线程编程
1.线程是什么
线程(英语:thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。
每个进程至少有一个线程,即进程本身,称为主线程。进程可以启动多个线程。操作系统像并行“进程”一样执行这些线程。
线程的分类
有两种不同的线程:
- 内核线程
- 用户空间线程或用户线程
内核线程是操作系统的一部分,而内核中没有实现用户空间线程。
2.进程和线程的区别
进程是资源分配的最小单位,线程是程序执行的最小单位(CPU调度的最小单位)。
进程有自己的独立地址空间。线程是共享进程中的数据的,使用相同的地址空间.
进程之间的通信需要以通信的方式(IPC)进行。线程之间的通信更方便,同一进程下的线程共享全局变量、静态变量等数据,难点:处理好同步与互斥。
3.多线程编程的实现方式
3.1 实例化对象
python的thread模块是⽐较底层的模块,python的threading 模块是对thread做了⼀些包装的,可以更加⽅便的被使⽤。
import time
import threading
def task():
"""当前要执行的任务"""
print("听音乐........")
time.sleep(1)
if __name__ == '__main__':
start_time = time.time()
threads = []
for count in range(5):
t = threading.Thread(target=task)
# 让线程开始执行任务
t.start()
threads.append(t)
# 等待所有的子线程执行结束, 再执行主线程;
[thread.join() for thread in threads]
end_time = time.time()
print(end_time-start_time)
多线程程序的执⾏顺序是不确定的。
当执⾏到sleep语句时,线程将被阻塞(Blocked),到sleep结束后,线程进⼊就绪(Runnable)状态,等待调度。⽽线程调度将⾃⾏选择⼀个线程执⾏。
代码中只能保证每个线程都运⾏完整个run函数,但是线程的启动顺序、 run函数中每次循环的执⾏顺序都不能确定。
3.1.1 基于多线程的IP归属地查询
import requests,json
import sqlalchemy
from sqlalchemy import Column,Integer,String
from sqlalchemy.orm import Session,sessionmaker
from sqlalchemy.ext.declarative import declarative_base
import pymysql
from threading import Thread
pymysql.install_as_MySQLdb()
content = []
#创建数据库的连接
engine = sqlalchemy.create_engine('mysql://root:westos@localhost/ip',encoding='utf-8')
#创建缓存对象
Session = sessionmaker(bind=engine)
session = Session()
#声明基类
Base = declarative_base()
#创建数据库表
class IpQuery(Base):
__tablename__ = 'ips'
id = Column(Integer,primary_key=True,autoincrement=True)
ip = Column(String(20),nullable=False)
city = Column(String(20))
country = Column(String(20))
def __repr__(self):
return '%s,%s,%s,%s\n' %(self.id,self.ip,self.city,self.country)
#获取ip归属地的函数
def task(ip):
"""获取指定IP的所在城市和国家,并存储到数据库中"""
#获取网址的返回内容
url = 'http://ip-api.com/json/%s' %(ip)
try:
response = requests.get(url)
except Exception as e:
print('网页获取错误',e)
else:
#默认返回的是字符串
"""
{"as":"AS174 Cogent Communications","city":"Beijing","country":"China","countryCode":"CN","isp":"China Unicom Shandong Province network","lat":39.9042,"lon":116.407,"org":"NanJing XinFeng Information Technologies, Inc.","query":"114.114.114.114","region":"BJ","regionName":"Beijing","status":"success","timezone":"Asia/Shanghai","zip":""}
"""
contentPage = response.text
#将页面的json字符串转换成便于处理的字典
data_dict = json.loads(contentPage)
#获取对应的城市和国家
city = data_dict.get('city','null') #None对象无法直接存储到数据库中
country = data_dict.get('country','null')
content.append([ip,city,country])
if __name__ == '__main__':
ipList = []
threads = []
for i in range(255):
ip = '1.1.1.%s' %(str(i+1))
thread = Thread(target=task,args=(ip,))
thread.start()
threads.append(thread)
[thread.join() for thread in threads]
Base.metadata.create_all(engine)
for i in content:
p = IpQuery(ip=i[0],city=i[1],country=i[2])
ipList.append(p)
session.add_all(ipList)
session.commit()
def __str__(self):
return '%s,%s,%s,%s\n' %(self.id,self.ip,self.city,self.country)
Base.metadata.create_all(engine)
threads = []
for item in range(10):
ip = '192.168.1.'+str(item+1)
task(ip)
#多线程执行任务
thread = Thread(target=task,args=(ip,))
thread.start()
threads.append(thread)
[thread.join() for thread in threads]
print('任务结束')
print(session.query(IpQuery).all)
3.2 创建子类
3.2.1 基于多线程的批量主机存活探测
如果要在本地网络中确定哪些地址处于活动状态或哪些计算机处于活动状态,则可以使用此脚本。我们将依次ping地址, 每次都要等几秒钟才能返回值。这可以在Python中编程,在IP地址的地址范围内有一个for循环和一个os.system(“ping -c -w”+ ip)。
但是,没有线程的解决方案效率非常低,因为脚本必须等待每次ping。
from threading import Thread
class GetHostAliveThread(Thread):
"""
创建子线程, 执行的任务:判断指定的IP是否存活
"""
def __init__(self, ip):
super(GetHostAliveThread, self).__init__()
self.ip = ip
def run(self):
# # 重写run方法: 判断指定的IP是否存活
# """
# >>> # os.system() 返回值如果为0, 代表命令正确执行,没有报错; 如果不为0, 执行报错;
# ...
# >>> os.system('ping -c1 -w1 192.168.22.49 &> /dev/null')
# 0
# >>> os.system('ping -c1 -w1 192.168.22.1 &> /dev/null')
# 256
# """
import os
# 需要执行的shell命令
cmd = 'ping -c1 -w1 %s &> /dev/null' %(self.ip)
result = os.system(cmd)
# 返回值如果为0, 代表命令正确执行,没有报错; 如果不为0, 执行报错;
if result != 0:
print("%s主机没有ping通" %(self.ip))
if __name__ == '__main__':
print("打印192.168.22.0网段没有使用的IP地址".center(50, '*'))
for i in range(1, 255):
ip = '192.168.22.' + str(i)
thread = GetHostAliveThread(ip)
thread.start()
4.共享全局变量
优点: 在⼀个进程内的所有线程共享全局变量,能够在不使⽤其他⽅式的前提 下完成多线程之间的数据共享(这点要⽐多进程要好)
缺点: 线程是对全局变量随意遂改可能造成多线程之间对全局变量 的混乱(即线程⾮安全)
money = 0
def add():
for i in range(1000000):
global money
money +=1
def reduce():
for i in range(1000000):
global money
money -= 1
if __name__ == '__main__':
from threading import Thread,Lock
lock = Lock()
t1 = Thread(target=add)
t2 = Thread(target=reduce)
t1.start()
t2.start()
t1.join()
t2.join()
print(money)
在这个代码中,全局变量money一开始为0,实例化了两个线程类,其中t1对money进行+1操作,t2对money进行-1操作。按理来说,加一减一应该是0,但是结果并非如此。(循环次数过少,不会出现线程非安全问题)多个线程在对同一个数据进行修改时,可能会出现不可预料的情况。
4.1 GIL是什么
如何解决 线程非安全的问题呢? 加锁!在一个线程对数据进行操作时,禁止其他的线程对此数据操作。
GIL(global interpreter lock)全局解释器锁: python解释器中任意时刻都只有一个线程在执行;
Python代码的执行由Python 虚拟机(也叫解释器主循环,CPython版本)来控制,Python 在设计之初就考虑到要在解释器的主循环中,同时只有一个线程在执行,即在任意时刻,只有一个线程在解释器中运行。对Python 虚拟机的访问由全局解释锁(GIL)来控制,正是这个锁能保证同一时刻只有一个线程在运行。
想要实现多线程,可以用Jpython解释器,或者多进程+协程的方式实现。
4.2 线程同步
线程同步:即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作, 其他线程才能对该内存地址进行操作.
4.3 如何实现线程锁
- 实例化一个锁对象lock = threading.Lock()
- 操作变量之前,进行加锁。lock.acquire()
- 操作变量之后,进行解锁。lock.release()
money = 0
def add():
for i in range(1000000):
global money
lock.acquire()
money +=1
lock.release()
def reduce():
for i in range(1000000):
global money
lock.acquire()
money -= 1
lock.release()
if __name__ == '__main__':
from threading import Thread,Lock
lock = Lock()
t1 = Thread(target=add)
t2 = Thread(target=reduce)
t1.start()
t2.start()
t1.join()
t2.join()
print(money)
再看输出的结果,一定是0