42、线程

并发编程 / 2021-01-13

一、线程基础

养成看源码的习惯;前期我们需要立志成为拷贝忍者卡卡西,不需要创新;等到拥有一定技术能力,能看懂比如开源大项目的源码并理解别人的思维方式,就可以走自己的路了!

1.1、什么是线程?

进程:资源单位,相当于公司领导,专门负责给你调配资源好让你大展身手!

线程:执行单位,相当于程序猿,实际负责干活的家伙!

每一个进程自带线程,就好比领导下面必有手下干活,领导怎么会干粗活呢!真正被CPU执行的也是线程,线程就是代码的执行过程,执行代码中所需要的资源都找进程要!当然了,进程和线程都是概念而已,只是为了更加方便地区分和描述问题!

1.2、为何要用线程

1.2.1、开启进程所需:
  • 为进程申请内存空间
  • 读取代码到内存
1.2.2、开启线程所需:

一个进程内可以开设多个线程,而同进程下线程之间的内存空间是共享的!因此在同一个进程内开设多个线程无需再次申请内存空间和读取代码的操作,开线程的开销远比进程要小!

1.3、如何使用线程

线程模块是threading,语法跟multiprocessing基本一样,简单过一下!提醒:即使是Windows,线程也不用非得在main下面执行代码,直接写就行,但建议还是写在main下面,形成一种规范!

实例化对象
from threading import Thread
import time


def say_goodbye(name):
    # time.sleep(1)
    print('good bye', name)


if __name__ == '__main__':
    t1 = Thread(target=say_goodbye, args=('sanxi',))
    t1.start()
    print('我是主线程')
    
# 获得结果
good bye我是主线程 sanxi

上面打印结果混乱是因为线程开销小所以速度快,几乎是代码一旦执行线程就已经创建好了, 因此有可能比主进程还快!

继承类
from threading import Thread
import time


class MyThread(Thread):
    def __init__(self, name):
        super(MyThread, self).__init__()  # 重写了别人的方法,又不知道方法里有什么,就调用父类的方法!
        self.name = name

    def run(self):
        time.sleep(1)
        print('hello', self.name)


if __name__ == '__main__':
    t1 = MyThread('chrystal')
    t1.start()
    print('我是主线程')
    
# 获得结果
我是主线程
hello chrystal

1.4、threading的其它方法

1.4.1、Thread对象的其它方法
  isAlive(): 返回线程是否活动的。
  getName(): 返回线程名。
  setName(): 设置线程名。
1.4.2、threading模块一些方法
  threading.currentThread(): 返回当前的线程名。
  threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。
  threading.activeCount(): 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果。

1.5、守护线程

跟进程的守护进程一样,线程的守护线程也是等待主线程运行完毕后回收相关资源!但它们之间还是有一些不一样的地方!

对守护进程来说

守护进程等待主进程运行完毕指的是等待主进程的代码运行完毕,当主进程代码运行完毕后守护进程即刻被回收,然后主进程会等待子进程都执行完毕后回收子进程的相关资源,接着结束程序运行!

对守护线程来说

守护线程等待主线程运行完毕指的是等待主线程内非守护线程都执行完毕,才算执行完毕,接着开始回收守护进程,然后结束程序运行!

主线程运行结束后不立即结束,会等待所有其它非守护线程结束才会结束,因为主线程的结束意味着所在的进程的结束

from threading import Thread
import time


def say_goodbye(name):
    time.sleep(1)
    print('good bye', name)


if __name__ == '__main__':
    t1 = Thread(target=say_goodbye, args=('sanxi',))
    t1.daemon = True  # 也可以用t1.setDaemon(True)
    t1.start()
    print('我是主线程')

# 执行结果,因为没额外的线程,因此主线程结束函数都没机会运行
我是主线程

二、线程锁

2.1、GIL全局解释器锁

先来个CPython解释器官方说明:

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.)

简单翻译一下原文:

即同一个进程下开启的多个线程,同一时刻只能执行一个线程,因为CPython的内存管理不是线程安全的。这意味着Python它多线程无法利用多核优势,那多线程还有意义吗?

回答这个问题前,先说一下GIL不是Python的特点,而是CPython解释器的特点,而Python常见的解释器有CPython、JPython、IPython、PyPy等!那么在CPython解释器中GIL是一把互斥锁,用来阻止同一个进程下的多个线程的同时执行!为什么要阻止???

2.1.1、查看解释器

Python有很多解释器,最常用该是CPython(默认的官方解释器),如何查看自己的解释器?

>>> import platform
>>> platform.python_implementation()
'CPython'
2.1.2、内存管理

内存管理即垃圾回收机制,此前曾经提到过!当一个变量引用计数为0时,该机制会立即将其回收!想象一下这种极端场景:

GIL

如何避免这种数据安全问题呢?像我们上一节说加个互斥锁呗,但这里是在CPython解释器上加,我抢到了锁我就可以先运行我的代码,你们其它线程都给我一边等着去!因此,GIL是保证解释器级别的数据安全的互斥锁,同样的,它保证安全的代价就是同一个进程下无法同时运行多个线程!

2.2、GIL与普通锁区别

图片来源于网络

GIL&lock

2.3、多进程VS多线程

据上可知,同一个进程下的多线程无法利用多核优势,那它多线程还有用吗?

多线程是否有用要看具体情况,我们来模拟一下吧!模拟两种场景:IO密集型任务\计算密集型任务

2.3.1、计算密集型
多线程
from threading import Thread
import time
import os


def count():
    total = 0
    for i in range(1, 10000000):
        total *= i


if __name__ == '__main__':
    task_list = []
    start_time = time.time()
    for thread in range(os.cpu_count()):
        t = Thread(target=count)
        t.start()
        task_list.append(t)
    for j in task_list:
        j.join()
    print(time.time() - start_time)
    
# 执行结果
3.344895362854004
多进程
# 就把上面这行改成process就行,其它不变
t = Process(target=count)

# 执行结果
0.658888578414917
2.3.2、IO密集型
多线程
from threading import Thread
from multiprocessing import Process
import time
import os


def count():
    time.sleep(2)  # 模拟器IO阻塞


if __name__ == '__main__':
    task_list = []
    start_time = time.time()
    for thread in range(4000):
        t = Thread(target=count)
        t.start()
        task_list.append(t)
    for j in task_list:
        j.join()
    print(time.time() - start_time)
    
# 执行结果
2.3118152618408203
多进程
# 就把上面这行改成process就行,其它不变
t = Process(target=count)

# 执行结果
17.961480379104614

2.4、总结:

上面演示结果,在计算密集型任务中多进程有优势;而在IO密集型任务中多线程优势大!由此看来,多进程和多线程都有各自的优势,后面写项目的时候我们通常可以通过多进程下面再开设多线程,这样既可以利用多核也可以节省资源消耗!

世间微尘里 独爱茶酒中