python多线程与GIL

目录

1.GIL      

1.1 为什么要有GIL

1.2 GIL的运作方式

1.3 GIL带来的问题

2.多线线程

2.1 线程的调度和启动

3.线程构造与使用

3.1调用Thread类构造器创建线程

3.2继承Thread类创建线程类


在学习python多线程过程,学习一些很不错的文章和教程,这里进行摘抄并总结归纳一下,便于加深自己的理解

1.GIL      

    GIL的全称是全局解释器锁(Global Interpreter Lock),GIL并不是Python的特性,它是在实现Python解析器(CPython)时所引入的一个概念。就好比C++,可以用不同的编译器来编译成可执行代码。有名的编译器例如GCC,INTEL C++,Visual C++等。Python也一样,同样一段代码可以通过CPython,PyPy,Psyco等不同的Python执行环境来执行。像其中的JPython就没有GIL。然而因为CPython是大部分环境下默认的Python执行环境。所以在很多人的概念里CPython就是Python,也就想当然的把GIL归结为Python语言的缺陷。所以这里要先明确一点:GIL并不是Python的特性,Python完全可以不依赖于GIL。

1.1 为什么要有GIL

相比于使用更细粒度的锁,GIL具有以下优点:

  1. 在单线程任务中更快;
  2. 在多线程任务中,对于I/O密集型程序运行更快;
  3. 在多线程任务中,对于用C语言包来实现CPU密集型任务的程序运行更快;
  4. 在写C扩展的时候更加容易,因为除非你在扩展中允许,否则Python解释器不会切换线程;
  5. 在打包C库时更加容易。我们不用担心线程安全性。,因为如果该库不是线程安全的,则只需在调用GIL时将其锁定即可。

1.2 GIL的运作方式

  1. 某个线程拿到GIL
  2. 该线程执行代码,直到达到了check_interval*
  3. 解释器让当前线程释放GIL
  4. 所有的线程开始竞争GIL
  5. 竞争到GIL锁的线程又从第1步开始执行

1.3 GIL带来的问题

因为有GIL的存在,由CPython做解释器(虚拟机)的多线程Python程序只能利用多核处理器的一个核来运行。

例如,我们将一个8线程的JAVA程序运行在4核的处理器上,那么每个核会运行1个线程,然后利用时间片轮转,轮流运行每一个线程。但是,我们将一个8线程的Python程序(由CPython作解释器)运行在一个4核处理器上,那么总共只会有1个核在工作,8个线程都要在这一个核上面时间片轮转。

所以说,用Python做多线程的最佳场景是处理I/O密集型应用,例如网络爬虫,因为I/O的速度比CPU的速度慢非常多,这样不同线程的上下文切换(context switch)的时间就可以忽略不计了。如果做CPU密集型任务,则使用多进程可以更好地利用CPU的多核性能。另外,使用gevent协程可以避免多线程的上下文切换的问题

在python中使用都是操作系统级别的线程,linux中使用的pthread,window使用的是其原生线程

2.多线线程

多线程的好处自不必赘述,通过一个简单的例子来看一下,参考关于播放音乐和视频

先看看单线程的

#coding=utf-8
import threading
from time import ctime,sleep

def music(func):
    for i in range(2):
        print("I was listening to %s. %s" %(func,ctime()))
        sleep(1)

def move(func):
    for i in range(2):
        print("I was at the %s! %s" %(func,ctime()))
        sleep(5)



if __name__ == '__main__':
    print("all start %s" %ctime())
    music(u'Music')
    move(u'Vedio')
    print("all over %s" %ctime())

输出如下:

但实际上诗就像MV一样,这两个是一般是同时开展的

#coding=utf-8
import threading
from time import ctime,sleep


def music(func):
    for i in range(2):
        print("I was listening to %s. %s" %(func,ctime()))
        sleep(1)

def move(func):
    for i in range(2):
        print("I was at the %s! %s" %(func,ctime()))
        sleep(5)

threads = []
t1 = threading.Thread(target=music,args=(u'Music',))
threads.append(t1)
t2 = threading.Thread(target=move,args=(u'Vedio',))
threads.append(t2)

if __name__ == '__main__':
    print("all start %s" % ctime())
    for t in threads:
        t.setDaemon(True)
        t.start()
    t.join()
    print("all over %s" %ctime())

 


1.join()方法,用于等待线程终止。join()的作用是,在子线程完成运行之前,这个子线程的父线程将一直被阻塞等待子线程结束

2.程序中一般有主线程和子线程,两种线程都执行完毕,认为是程序执行结束。setDaemon(True)将线程声明为守护线程,此类线程的特点是,当程序中主线程及所有非守护线程执行结束时,未执行完毕的守护线程也会随之消亡(进行死亡状态),程序将结束运行。Python 解释器的垃圾回收机制就是守护线程的典型代表,当程序中所有主线程及非守护线程执行完毕后,垃圾回收机制也就没有再继续执行的必要了。需要注意的一点是,线程对象调用 daemon 属性必须在调用 start() 方法之前,否则 Python 解释器将报 RuntimeError 错误

参考 Python daemon守护线程详解


输入结果如下:

2.1 线程的调度和启动

python 中一个线程对应c语言中一个线程。GIL使得同一个时刻只有一个线程运行在cpu上执行字节码,无法将多个线程映射到多个cpu上执行,GIL会根据执行的字节码行数以及时间释放GIL,在遇到IO操作时会主动释放。python提供了_thread 和threading 两个模块来支持多线程,其中_thread提供低级别的,原始的线程支持,以及一个简单的锁,但是一般不建议使用_thread模块;而threading模块则提供了功能丰富的多线程支持。

通过一个对一个全局变量进行增减操作,来解释上述蓝色字体描述

total = 0
def add(name):
    global total
    for i in range(10000):
        total += 1
        print('当前是%s线程,i的值为%s' % (name,total))

def desc(name):
    global total
    for i in range(10000):
        total -= 1
        print('当前是%s线程,i的值为%s' % (name,total))

import threading
thread1 = threading.Thread(target=add,args=('thread1',))
thread2 = threading.Thread(target=desc,args=('thread2',))

thread1.start()
thread2.start()

thread1.join()
thread2.join()

print(total)

输入结果如下所示:

从输出结果来看,并不是等待其中一个thread跑完了再去跑另一个线程任务

3.线程构造与使用

python 主要提供两种方式来创建线程:

  • 使用threading模块的Thread类的构造器创建线程
  • 继承 threading模块的Thread类创建线程类

3.1调用Thread类构造器创建线程

调用Thread类的构造器创建线程,直接调用threading.Thread类的如下构造器创建线程

__init__(self, group= None, target=None, name= None, args=(), kwargs= None, *, daemon= None)

target :指定该线程要调度的目标方法。只传函数名,不传函数,即不加()
args :指定一个元组,以位置参数的形式为target指定的函数传入参数。元组的第一个参数传给target的第一个,以此类推。
kwargs:指定一个字典,以关键字参数的形式为target指定的函数传入参数
daemon: 指定所构建的线程是否为后台线程。

调用Thread类的构造器创建并启动多线程的步骤:

  1. 调用Thread类的构造器创建线程对象。在创建线程对象时。target参数指定的函数将作为线程的执行实体

  2. 调用线程对象的start()方法启动线程。

import threading
#定义一个普通action方法,该方法准备作为线程的执行实体
def action(max):
   for i in range(max):
       #调用threading模块的current_thread()函数获取当前线程
       #调用线程对象的getName()方法获取单前线程的名字
       print(threading.current_thread().getName() + " " +str(i))
#下面是主程序
for i in range(100):
    #调用threading模块的current_thread()函数获取当前线程
     print(threading.current_thread().getName() + " " +str(i))
     if  i == 20:
         t1 = threading.Thread(target=action, args=(100,))
         t1.start()
         t2 = threading.Thread(target =action, args=(100,))
         t2.start()
print("主线程执行完成!")

执行结果如下:

可以看到在循环变量达到20时,创建并且启动了两个新线程。所以此时一共有三个线程,但是三个线程之间的执行没有没有先后顺序,他们的执行方式为Thread-1执行一段时间Thread-2或MainThread获得CPU执行一段时间。

  • threading.current_thread():它是threading模块的函数,该函数总是返回当前正在执行的线程对象
  • getName() 它是Thread类的实例方法,该方法返回调用它的线程名字
    -setName() 方法可以为线程设置名字,getName和setName 两个方法可通过name属性来代替。

3.2继承Thread类创建线程类

  • 定义Thread类的子类,并重写该类的run()方法。run()方法的方法体就代表了线程需要完成的任务,因此把run()方法称为线程执行体。

  • 创建Thread子类的实例,即创建线程对象

  • 调用线程对象的start()方法来启动线程

import threading
import time

class GetDetailHtml(threading.Thread):
    def __init__(self, name):
        super().__init__(name=name)


    def run(self):
        print("get detail html started")
        time.sleep(2)
        print("get detail html end")


class GetDetailUrl(threading.Thread):
    def __init__(self, name):
        super().__init__(name=name)


    def run(self):
        print("get detail url started")
        time.sleep(2)
        print("get detail url end")


if __name__ == "__main__":
    thread1 = GetDetailHtml("get_detail_html")
    thread2 = GetDetailUrl("get_detail_url")
    start_time = time.time()
    thread1.start()
    thread2.start()
    """join进行线程阻塞,只有两个线程都执行完成后才执行后边的"""
    thread1.join()
    thread2.join()
    print("last time :{}".format(time.time() - start_time))

输出结果如下:

 

参考 python中的GIL详解

参考 python 多线程

参考 python并发编程

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值