8-2-1python语法基础-并发编程-线程-创建线程,全局解释器锁GIL
前言
先看这个文章:
python进程,线程,协程,对比,思考: https://www.cnblogs.com/andy0816/p/15590085.html
线程
标准库threading
Python多线程依赖于标准库threading,线程类Thread的常用方法如下表:
- 1 start() 创建一个Thread子线程实例并执行该实例的run()方法
- 2 run() 子线程需要执行的目标任务
- 3 join() 主进程阻塞等待子线程直到子线程结束才继续执行,可以设置等待超时时间timeout
- 4 is_alive() 判断子线程是否终止
- 5 daemon 设置子线程是否随主进程退出而退出
如何使用python实现多线程?
- 很多语言都有多线程,我们现在是关注的python如何来实现多线程
- 我要有一个自己的例子,就是多线程验证ip可用性的例子
- 先上代码:
import redis
import queue
import requests
import threading
import logging
logging.basicConfig(level=logging.INFO,
format=
# 日志的时间
'%(asctime)s'
# 日志级别名称 : 当前行号
' %(levelname)s [%(filename)s : %(lineno)d ]'
# 日志信息
' : %(message)s'
# 指定时间格式
, datefmt='[%Y/%m/%d %H:%M:%S]')
logging = logging.getLogger(__name__)
# 第一步,把数据取出来,在redis
conn = redis.Redis(host="127.0.0.1", port="6379")
# proxy_list = conn.hgetall("use_proxy")
# proxy_list = conn.hvals("use_proxy")
proxy_list = conn.hkeys("use_proxy")
# logging.info(proxy_list)
# 第二步,把数据存入队列
proxy_queue = queue.Queue()
for proxy in proxy_list:
proxy_queue.put(str(proxy, encoding="utf-8"))
queue_size = proxy_queue.qsize()
# logging.info(queue_size)
# logging.info(proxy_queue.get())
# 第三步,多线程验证
# 1,先写一个类用来验证ip
class TreadCheck(threading.Thread):
def __init__(self, target_queue, thread_name):
threading.Thread.__init__(self, name=thread_name)
self.target_queue = target_queue
self.thread_name = thread_name
def run(self):
logging.info("{} 开始".format(self.name))
while True:
try:
proxy = self.target_queue.get(block=False)
except queue.Empty:
logging.info("{} 结束".format(self.name))
break
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:34.0) Gecko/20100101 Firefox/34.0',
'Accept': '*/*',
'Connection': 'keep-alive',
'Accept-Language': 'zh-CN,zh;q=0.8'}
proxies = {
"http": "http://" + str(proxy),
"https": "https://" + str(proxy)
}
# 代理验证目标网站
url_http = "http://httpbin.org"
url_https = "https://www.qq.com"
http_code = False
https_code = False
try:
res_http = requests.head(url=url_http, headers=headers, proxies=proxies, timeout=10)
if res_http.status_code == 200:
http_code = True
except Exception as e:
http_code = False
try:
res_https = requests.head(url=url_https, headers=headers, proxies=proxies, timeout=10)
logging.info(res_https.status_code)
if res_https.status_code == 200:
https_code = True
except Exception as e:
https_code = False
logging.info(
"{} http_status:{} https_status:{} 代理:{} ".format(self.name, str(http_code).ljust(6),
str(https_code).ljust(6), proxy.ljust(25)))
# 2,使用多线程
thread_list = []
for index in range(20):
thread_list.append(TreadCheck(proxy_queue, "thread_" + str(index).zfill(2)))
for thread in thread_list:
thread.setDaemon(True)
thread.start()
for thread in thread_list:
thread.join()
示例代码解析
- 通过上面的示例代理,来学习一下python的多线程
创建线程
- 认知:
- 创建线程有两种方式,一种基于函数创建,一种基于类创建
- 简单的多线程可以使用函数来实现,
- 复杂的多线程,需要类的方式来实现,
第一种方式:基于构造函数创建
from threading import Thread
import time
def hi(msg, sec):
print("enter hi() . {} @{}".format(msg, time.strftime("%H:%M:%S")))
time.sleep(sec)
print("{} @{}".format(msg, time.strftime("%H:%M:%S")))
return sec
begin_time = time.time()
begin_time_str = time.strftime("%H:%M:%S", time.localtime(begin_time))
print(f"begin @{begin_time_str}")
t_list = []
for i in range(1, 5):
t = Thread(target=hi, args=(i, i)) # 注册
t.start() # 这是启动了一个线程
t_list.append(t)
# hi(i, i) # 直接调用这个函数,就是不使用并发
for t in t_list:
t.join()
end_time = time.time()
end_time_str = time.strftime("%H:%M:%S", time.localtime(end_time))
print(f"end at {end_time_str}")
print(f"总用时:{int(end_time - begin_time)}秒")
# 使用单线程的这个结果应该是,1+2+3+4=10秒
# 使用多线程的这个结果应该是,4秒
通过一个例子再次体验多线程的加速
- 认知;
- 使用多线程加速的例子,龟速和秒速
- 如果不是多线程,那就是一个一个排队,
- 如果是多线程,就是并发,那就是成倍的提高速度,
from threading import Thread
import time
def hi(sec):
time.sleep(1)
print(sec)
print("begin @{}".format(time.strftime("%H:%M:%S")))
t_list = []
for i in range(256):
t = Thread(target=hi, args=(i,)) # 注册
t.start() # 这是启动了一个线程
t_list.append(t)
# hi(i) # 这是不通过线程,使用同步的方式,
for t in t_list:
t.join()
print("end at {}".format(time.strftime("%H:%M:%S")))
创建线程第二种方式:基于类创建
- 注意两点:
- 1,要继承Thread
- 2,要重写run方法
from threading import Thread
import time, os
class MyTread(Thread):
def __init__(self, arg):
super().__init__()
self.arg = arg
def run(self): # 这个run是重写
print("enter hi() . {} @{}".format(self.arg, time.strftime("%H:%M:%S")))
time.sleep(self.arg)
print("{} @{}".format(self.arg, time.strftime("%H:%M:%S")))
# print(1, os.getpid())
print("begin @{}".format(time.strftime("%H:%M:%S")))
t_list = []
for i in range(1, 5):
t = MyTread(i) # 初始化线程,传递参数
t.start() # 开启线程,
t_list.append(t)
# t.join # 如果你把t.join 放到这里,就会导致线程是一个一个的顺序执行的,这就和普通的for循环一样了,起不到并发的意义了,所以不能放到这个地方,
for t in t_list:
t.join() # 主线程会等待子线程结束了之后才会结束,我们经常会用到这个功能,比如100个线程,我们需要在主线程汇总100个线程的结果就需要用到这个方法
# print("主线程", os.getpid()) # 打印子进程和主进程的进程号,都是一样的
print("end at {}".format(time.strftime("%H:%M:%S")))
join方法
-
必须要深刻理解join:不同地方起到不同的作用,一定要写对地方,否则起不到并发的效果
-
join方法:阻塞线程,主线程进行等待。
-
主线程A中,创建了子线程B,并且在主线程A中调用了B.join(),
那么,主线程A会在调用的地方阻塞,直到子线程B完成操作后,才可以接着往下执行。 -
一般是设置守护线程为true,然后设置join,这样保证子线程结束的时候,子线程是结束的
-
而且也能保证主线程是在每一个子线程结束之后才会结束
-
这里有个小疑问,既然加不加join子线程都会跑完,为什么还加join。因为有些线程之间需要输出参数给其余函数用,所以得等某个函数线程跑完才能执行主线程。
其他线程的使用
Thread类的其他方法:查看当前线程
import threading
from threading import Thread
import time
def hi(msg, sec):
print("enter hi() . {} @{}".format(msg, time.strftime("%H:%M:%S")))
time.sleep(sec)
print(msg, threading.current_thread()) # 这个是子线程1 <Thread(Thread-1, started 123145483644928)>
print("{} @{}".format(msg, time.strftime("%H:%M:%S")))
return sec
print("begin @{}".format(time.strftime("%H:%M:%S")))
for i in range(1, 5):
t = Thread(target=hi, args=(i, i)) # 注册
t.start() # 这是启动了一个线程
print("查看活跃的线程数", threading.active_count()) # 查看活跃的线程数,这个就是5,因为是一个主线程,4个子线程
print(threading.current_thread()) # 这个是主线程<_MainThread(MainThread, started 4680646144)>
print("end at {}".format(time.strftime("%H:%M:%S")))
全局解释器锁GIL
- 最后说一说这个全局解释锁
首先需要明确的一点是GIL并不是Python的特性,它是在实现Python解析器(CPython)时所引入的一个概念。
Python也一样,同样一段代码可以通过CPython,PyPy,Psyco等不同的Python执行环境来执行。
像其中的JPython就没有GIL。然而因为CPython是大部分环境下默认的Python执行环境。
所以在很多人的概念里CPython就是Python,也就想当然的把GIL归结为Python语言的缺陷。
所以这里要先明确一点:GIL并不是Python的特性,Python完全可以不依赖于GIL。
简单来说,在Cpython解释器中,因为有GIL锁的存在同一个进程下开启的多线程,同一时刻只能有一个线程执行,无法利用多核优势。
同一个数据,多个线程去操作,也会出现问题,所以也有线程锁的概念,
这种机制叫做全局解释器锁,英文GIL,
这种机制的结果就是:同一时间只有一个线程去访问cpu,
你想要访问数据,就必须拿到这个钥匙,
这个锁,锁的是线程,不是锁的某一个数据,
这样不好,因为同一时间cpu上面只有一个线程,这样不能充分的利用cpu
这不是Python语言的问题,这是cPython解释器的问题,如果你有一个jPython解释器就行
这的确是一个弊病,导致Python的多线程形同虚设,
那么为什么不解决呢?
java和c++都是编译性语言,Python是一个解释性语言,php也是,
目前解释性语言就是存在这个问题,这个矛盾还不可调和,
是否把数据加锁就可以了,也是不行的,数据太多了,这么大范围的加锁,最终导致的效率,还不如全局解释器锁的性能好,
常见问题1
我们有了GIL锁为什么还要自己在代码中加锁呢?
还是因为时间片轮转,你取到了数据,但是还没有来得及减1,这个值就被其他的线程拿走了,还是10,
常见问题2
有了GIL的存在,同一时刻同一进程中只有一个线程被执行,进程可以利用多核,但是开销大,
而Python的多线程开销小,但却无法利用多核优势,也就是说Python这语言难堪大用。
解答:
其实编程所解决的现实问题大致分为IO密集型和计算密集型。
对于IO密集型的场景,Python的多线程编程完全OK,而对于计算密集型的场景,
Python中有很多成熟的模块或框架如Pandas等能够提高计算效率。
在cPython解释器下的Python程序,在同一时间,多个线程只能有一个线程在cpu执行,这不意味着多线程就没有意义了,
解答:
因为只有涉及到计算的地方才会使用到CPU,
高CPU:所以在计算类的高cpu利用率的,Python不占优势,
高IO:我们写的代码很多都涉及到这种,
比如qq聊天,处理日志文件,爬取网页,处理web请求,读写数据库,都是高io的,都是有Python用武之地的,
所以Python不能处理计算类高的问题,这不影响他在web编程的作用,
如果你真的需要高并发呢,你可以使用多进程,就不会有GIL锁了,
看看python官方文档,对于python多线程的解释:
由于存在全局解释器锁,同一时刻只有一个线程可以执行 Python 代码(虽然某些性能导向的库可能会去除此限制)。
如果你想让你的应用更好地利用多核心计算机的计算资源,
推荐你使用 multiprocessing 或 concurrent.futures.ProcessPoolExecutor。
但是,如果你想要同时运行多个 I/O 密集型任务,则多线程仍然是一个合适的模型。
所以说,这个多线程不是没有用处的,而是要分你干的是什么事情,
注意两个概念,:
1,计算密集型任务,计算密集型就是计算、逻辑判断量非常大而且集中的类型
2,io密集型任务,IO密集型就是磁盘的读取数据和输出数据非常大的时候就是属于IO密集型
而io又分为,磁盘io和网络io,
这个多线程,还是适合io密集型的任务的,
技术改变命运