专栏首页Python 大数据与SQL优化笔记一篇文章搞懂Python多线程简单实现和GIL
原创

一篇文章搞懂Python多线程简单实现和GIL

个人公众号:pythonislover

今天开始打算开一个新系列,就是python的多线程和多进程实现,这部分可能有些新手还是比较模糊的,都知道python中的多线程是假的,但是又不知道怎么回事,首先我们看一个例子来看看python多线程的实现。

import threading
import time

def say(name):
        print('你好%s at %s' %(name,time.ctime()))
        time.sleep(2)
        print("结束%s at %s" %(name,time.ctime()))

def listen(name):
    print('你好%s at %s' % (name,time.ctime()))
    time.sleep(4)
    print("结束%s at %s" % (name,time.ctime()))

if __name__ == '__main__':
    t1 = threading.Thread(target=say,args=('tony',))  #Thread是一个类,实例化产生t1对象,这里就是创建了一个线程对象t1
    t1.start() #线程执行
    t2 = threading.Thread(target=listen, args=('simon',)) #这里就是创建了一个线程对象t2
    t2.start()

    print("程序结束=====================")

结果:
你好tony at Thu Apr 25 16:46:22 2019
你好simon at Thu Apr 25 16:46:22 2019
程序结束=====================
结束tony at Thu Apr 25 16:46:24 2019
结束simon at Thu Apr 25 16:46:26 2019
Process finished with exit code 0

python的多线程是通过threading模块的Thread类来实现的。 创建线程对象 t1 = threading.Thread(target=say,args=('tony',)) #Thread是一个类,实例化产生t1对象,这里就是创建了一个线程对象t1 启动线程 t1.start() #线程执行

下面我们分析下上面代码的结果:
你好tony at Thu Apr 25 16:46:22 2019  --t1线程执行
你好simon at Thu Apr 25 16:46:22 2019 --t2线程执行
程序结束===================== --主线程执行
结束tony at Thu Apr 25 16:46:24 2019 --sleep之后,t1线程执行
结束simon at Thu Apr 25 16:46:26 2019 --sleep之后,t2线程执行
Process finished with exit code 0 --主线程结束

我们可以看到主线程的print并不是等t1,t2线程都执行完毕之后才打印的,这是因为主线程和t1,t2 线程是同时跑的。但是主进程要等非守护子线程结束之后,主线程才会退出。上面其实就是python多线程的最简单用法,但是可能有人会和我有一样的需求,一般开发中,我们需要主线程的print打印是在最后面的,表明所有流程都结束了,也就是主线程结束了。这里就引入了一个join的概念。

import threading
import time

def say(name):
        print('你好%s at %s' %(name,time.ctime()))
        time.sleep(2)
        print("结束%s at %s" %(name,time.ctime()))

def listen(name):
    print('你好%s at %s' % (name,time.ctime()))
    time.sleep(4)
    print("结束%s at %s" % (name,time.ctime()))

if __name__ == '__main__':
    t1 = threading.Thread(target=say,args=('tony',))  #Thread是一个类,实例化产生t1对象,这里就是创建了一个线程对象t1
    t1.start() #线程执行
    t2 = threading.Thread(target=listen, args=('simon',)) #这里就是创建了一个线程对象t2
    t2.start()

    t1.join() #join等t1子线程结束,主线程打印并且结束
    t2.join() #join等t2子线程结束,主线程打印并且结束
    print("程序结束=====================")

结果:
你好tony at Thu Apr 25 16:57:32 2019
你好simon at Thu Apr 25 16:57:32 2019
结束tony at Thu Apr 25 16:57:34 2019
结束simon at Thu Apr 25 16:57:36 2019
程序结束=====================

上面代码中加入join方法后实现了,我们上面所想要的结果,主线程print最后执行,并且主线程退出,注意主线程执行了打印操作和主线程结束不是一个概念,如果子线程不加join,则主线程也会执行打印,但是主线程不会结束,还是需要待非守护子线程结束之后,主线程才结束。上面的情况,主进程都需要等待非守护子线程结束之后,主线程才结束。那我们是不是注意到一点,我说的是“非守护子线程”,那什么是非守护子线程?默认的子线程都是主线程的非守护子线程,但是有时候我们有需求,当主进程结束,不管子线程有没有结束,子线程都要跟随主线程一起退出,这时候我们引入一个“守护线程”的概念。如果某个子线程设置为守护线程,主线程其实就不用管这个子线程了,当所有其他非守护线程结束,主线程就会退出,而守护线程将和主线程一起退出,守护主线程,这就是守护线程的意思看看具体代码,我们这里分2种情况来讨论守护线程,加深大家的理解,

还有一点,这个方法一定要设置在start方法前面

1.设置t1线程为守护线程,看看执行结果

import threading
import time

def say(name):
        print('你好%s at %s' %(name,time.ctime()))
        time.sleep(2)
        print("结束%s at %s" %(name,time.ctime()))

def listen(name):
    print('你好%s at %s' % (name,time.ctime()))
    time.sleep(4)
    print("结束%s at %s" % (name,time.ctime()))

if __name__ == '__main__':
    t1 = threading.Thread(target=say,args=('tony',))  #Thread是一个类,实例化产生t1对象,这里就是创建了一个线程对象t1
    t1.setDaemon(True)
    t1.start() #线程执行
    t2 = threading.Thread(target=listen, args=('simon',)) #这里就是创建了一个线程对象t2
    t2.start()

    print("程序结束=====================")

结果:
你好tony at Thu Apr 25 17:11:41 2019
你好simon at Thu Apr 25 17:11:41 2019
程序结束=====================
结束tony at Thu Apr 25 17:11:43 2019
结束simon at Thu Apr 25 17:11:45 2019
注意执行顺序,
这里如果设置t1为Daemon,那么主线程就不管t1的运行状态,只管等待t2结束, t2结束主线程就结束了
因为t2的时间4秒,t1的时间2秒,主线程在等待t2线程结束的过程中,t1线程自己结束了,所以结果是:
你好tony at Thu Apr 25 14:11:54 2019
你好simon at Thu Apr 25 14:11:54 2019程序结束===============
结束tony at Thu Apr 25 14:11:56 2019  (也会打印,因为主线程在等待t2线程结束的过程中, t1线程自己结束了)
结束simon at Thu Apr 25 14:11:58 2019

2.设置t2为守护线程

import threading
import time

def say(name):
        print('你好%s at %s' %(name,time.ctime()))
        time.sleep(2)
        print("结束%s at %s" %(name,time.ctime()))

def listen(name):
    print('你好%s at %s' % (name,time.ctime()))
    time.sleep(4)
    print("结束%s at %s" % (name,time.ctime()))

if __name__ == '__main__':
    t1 = threading.Thread(target=say,args=('tony',))  #Thread是一个类,实例化产生t1对象,这里就是创建了一个线程对象t1
    t1.start() #线程执行
    t2 = threading.Thread(target=listen, args=('simon',)) #这里就是创建了一个线程对象t2
    t2.setDaemon(True)
    t2.start()

    print("程序结束=====================")


结果:
你好tony at Thu Apr 25 17:15:36 2019
你好simon at Thu Apr 25 17:15:36 2019
程序结束=====================
结束tony at Thu Apr 25 17:15:38 2019
注意执行顺序:
这里如果设置t2为Daemon,那么主线程就不管t2的运行状态,只管等待t1结束, t1结束主线程就结束了
因为t2的时间4秒,t1的时间2秒, 主线程在等待t1线程结束的过程中, t2线程自己结束不了,所以结果是:
你好tony at Thu Apr 25 14:14:23 2019
你好simon at Thu Apr 25 14:14:23 2019
程序结束 == == == == == == == == == == =
结束tony at Thu Apr 25 14:14:25 2019
结束simon at Thu Apr 25 14:11:58 2019 不会打印,因为主线程在等待t1线程结束的过程中, t2线程自己结束不了,t2的时间4秒,t1的时间2秒

不知道大家有没有弄清楚上面python多线程的实现方式以及join,守护线程的用法。主要方法:

join():在子线程完成运行之前,这个子线程的父线程将一直被阻塞。
setDaemon(True):
将线程声明为守护线程,必须在start() 方法调用之前设置, 如果不设置为守护线程程序会被无限挂起。这个方法基本和join是相反的。
当我们在程序运行中,执行一个主线程,如果主线程又创建一个子线程,主线程和子线程 就分兵两路,分别运行,那么当主线程完成
想退出时,会检验子线程是否完成。如 果子线程未完成,则主线程会等待子线程完成后再退出。但是有时候我们需要的是 只要主线程
     完成了,不管子线程是否完成,都要和主线程一起退出,这时就可以 用setDaemon方法啦

其他方法:

run():  线程被cpu调度后自动执行线程对象的run方法
start():启动线程活动。
isAlive(): 返回线程是否活动的。
getName(): 返回线程名。
setName(): 设置线程名。

threading模块提供的一些方法:
threading.currentThread(): 返回当前的线程变量。
threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。
threading.activeCount():返回正在运行的线程数量,与len(threading.enumerate())有相同的结果。

上面的例子中我们注意到两如果个任务如果顺序执行要6s结束,如果是多线程执行4S结束,性能是有所提升的,但是我们要知道这里的性能提升实际上是由于cpu并发实现性能提升,也就是cpu线程切换(多道技术)带来的,而并不是真正的多cpu并行执行。

上面提到了并行和并发,那这两者有什么区别呢?

并发:是指一个系统具有处理多个任务的能力(cpu切换,多道技术) 并行:是指一个系统具有同时处理多个任务的能力(cpu同时处理多个任务)

并行是并发的一种情况,子集那为什么python在多线程中为什么不能实现真正的并行操作呢?就是在多cpu中执行不同的线程(我们知道JAVA中多个线程可以在不同的cpu中,实现并行运行)这就要提到python中大名鼎鼎GIL,那什么是GIL?

GIL:全局解释器锁 无论你启多少个线程,你有多少个cpu, Python在执行的时候只会的在同一时刻只允许一个线程(线程之间有竞争)拿到GIL在一个cpu上运行,当线程遇到IO等待或到达者轮询时间的时候,cpu会做切换,把cpu的时间片让给其他线程执行,cpu切换需要消耗时间和资源,所以计算密集型的功能(比如加减乘除)不适合多线程,因为cpu线程切换太多,IO密集型比较适合多线程。

任务: IO密集型(各个线程都会都各种的等待,如果有等待,线程切换是比较适合的),也可以采用可以用多进程+协程 计算密集型(线程在计算时没有等待,这时候去切换,就是无用的切换),python不太适合开发这类功能

我们前面举得例子里面模拟了sleep操作,其实就是相当于遇到IO,这种场景用多线程是可以增加性能的,但是如果我们用多线程来计算数据的计算,性能反而会降低。

下面是GIL的一段原生解释:

In CPython, the global interpreter lock, or GIL, is a mutex that prevents multiple native threads from executing Python bytecodes at once.
This lock is necessary mainly because CPython’s memory management is not thread-safe.
(However, since the GIL exists, other features have grown to depend on the guarantees that it enforces.)

个人见解,望指教

更多文章:

1.Python基础-不一样的切片操作 --https://mp.weixin.qq.com/s/3pute7-xrdhAt2Yk41nw9w

2.Python面向对象三大特征之继承 --https://mp.weixin.qq.com/s/RZrClpQ-YU-DrigF08b-rw

3.Python面向对象三大特征之封装 --https://mp.weixin.qq.com/s/rC-f0nUaLTJMYrBAz-w08Q

4.Python面向对象三大特征之多态 --https://mp.weixin.qq.com/s/j16IkZJ2Z2bV1p83OD416A

5.Python面向对象中的反射 --https://mp.weixin.qq.com/s/RD-Id8THxhevyPKNnYoLZA

6.Python的装饰器原来是这么用的 --https://mp.weixin.qq.com/s/Ak6RjM-I6JbV1MFmWhU45A

公众号

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 一篇文章理清Python多线程同步锁,死锁和递归锁

    但是不知道大家有没有注意到一点就是前面说的两个功能是相互独立的,相互不干涉的,不会用到同享的资源或者数据,如果我们多个线程要用到相同的数据,那么就会存在资源争用...

    南山烟雨
  • 一篇文章理清Python多线程之同步条件,信号量和队列

    今天这篇文章大概介绍下python多线程中的同步条件Event,信号量(Semaphore)和队列(queue),这是我们多线程系列的最后一篇文章,以后将会进入...

    南山烟雨
  • 躁动不安的年代,你需要读几本好书(python爬虫及数据分析)

    当今社会,速度已经深入人心了,“快”成了大家默认的办事境界,看机器上一件件飞一般传递着的产品,听办公室一族打电话时那种无人能及的语速......休闲的概念已日渐...

    南山烟雨
  • JUC线程池ThreadPoolExecutor源码分析

    很早之前就打算看一次JUC线程池ThreadPoolExecutor的源码实现,由于近段时间比较忙,一直没有时间整理出源码分析的文章。之前在分析扩展线程池实现可...

    Throwable
  • 并发编程之线程第一篇

    Java虚拟机栈 JVM中由堆、栈、方法区所组成,其中栈内存是给线程使用,每个线程启动后,虚拟机就会为其分配一块栈内存。

    海仔
  • 并发编程之进程与线程

    单核CPU下,线程实际还是串行执行的。操作系统中有一个组件叫做任务调度器,将CPU的时间片(windows下时间片最小约为15毫秒)分给不同的线程使用,只是由于...

    海仔
  • ReentrantLock 源码解读

    | 傻瓜源码-内容简介 |

    傻瓜源码
  • Java中的并发编程

    (1)异步化技术 MQ(消息队列):ActiveMQ、Kafka、RocketMQ、ZeroMQ、RabbitMQ。MQ的控制范围是跨进程的。 线程:控制范...

    魏晓蕾
  • Java并发编程之线程池

    java.util.concurrent.Executors提供了一个 java.util.concurrent.Executor接口的实现用于创建线程池

    日薪月亿
  • JUC学习笔记(四)—线程池

    线程池 【死磕Java并发】—–J.U.C之线程池:ThreadPoolExecutor

    Monica2333

扫码关注云+社区

领取腾讯云代金券