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密集型的任务的,

posted @ 2020-02-28 09:41  技术改变命运Andy  阅读(156)  评论(0编辑  收藏  举报