《老鸟python 系列》视频上线了,全网稀缺资源,涵盖python人工智能教程,爬虫教程,web教程,数据分析教程以及界面库和服务器教程,以及各个方向的主流实用项目,手把手带你从零开始进阶高手之路!点击 链接 查看详情




多线程

阅读:229679371    分享到

我们前面提到了进程是由若干线程组成的,一个进程至少有一个线程。由于线程是操作系统直接支持的执行单元,因此,高级语言通常都内置多线程的支持,Python 也不例外,并且,Python 的线程是真正的 Posix Thread,而不是模拟出来的线程。

在 Python3 中,通过 threading 模块提供线程的功能。原来的 thread 模块已废弃。但是 threading 模块中有个 Thread 类,是模块中最主要的线程类,不要和废弃的 thread 模块搞混了。

如何使用线程

启动一个线程就是把一个函数传入并创建 Thread 实例,然后调用 start() 开始执行。

import threading

def func(arg):
    print("我是第%d个线程,我的线程号是:%s" % (arg, threading.current_thread().name))

if __name__ == '__main__':
    for i in range(1, 4):  # 我们启动 3 个子线程
        t = threading.Thread(target=func, args=(i,))  # target 是关联的函数名,args 是传入函数的参数,用元组表示
        t.start()  # 启动线程

结果如下

我是第3个线程,我的线程号是:Thread-3
我是第1个线程,我的线程号是:Thread-1
我是第2个线程,我的线程号是:Thread-2

我们仔细分析以上代码,在 for 循环开始执行时,调用代码 threading.Thread 时,操作系统给我们的进程分配一个子线程,当调用 t.start() 函数时,线程就依托于函数 func 来执行。然后回到循环,操作系统又分配一个线程,然后依托于函数 func 来执行(记住函数只是代码段,所有调用他的线程都共享这些代码,函数里面的局部变量每个线程都是独立的), 因为程序从 thread 1 开始执行的,这样看起来,好像应该先打印出 thread 1,然后再打印出 thread 2,但是我们从结果上来看,是先打印出 thread 3 ,这是因为 thread 1 还没执行 print 语句时,操作系统就中断了,去执行另一个线程的代码,也就是说操作系统对每个线程的时间片分配是随机的,所以代码执行的先后具有不确定性。

同上一节课我们讲的多进程一样,如果主线程执行结束,子线程还没执行结束,就有点类似前面讲的僵尸进程的情况,编程时要注意最好不要出现这种情况。

import time
import threading


def func():
    time.sleep(3)
    print("我是子线程")

t = threading.Thread(target=func)
t.start()

print("主线程执行完毕")

执行结果是:

主线程执行完毕
我是子线程

Python 程序默认会等待最后一个线程执行完毕后才结束整个进程。在上面例子中,主线程没有等待子线程 t 执行完毕,而是啥都不管,继续往下执行它自己的代码,执行完毕后也没有结束整个进程,而是等待子线程 t 执行完毕,整个进程才结束。

有时候我们希望主线程等等子线程,不要自己嗨没了,小弟们还在继续嗨。那要怎么办?像前面讲的多进程一样,我们可以使用join()方法。

import time
import threading

def func():
    time.sleep(3)
    print("我是子线程")

t = threading.Thread(target=func)
t.start()
t.join()

print("主线程执行完毕")

执行结果:

我是子线程
主线程执行完毕

如果我们想让主线程结束的时候,强制结束还在活动的子线程,可以使用 setDaemon(True),该函数会把所有的子线程都变成主线程的守护线程,当主线程结束后,守护子线程也会随之结束,整个进程也跟着退出。

import time
import threading


def func():
    time.sleep(3)
    print("我是子线程")

t = threading.Thread(target=func)
t.setDaemon(True)
t.start()

print("主线程执行完毕")

执行结果:

主线程执行完毕

我们通过结果发现,主线程执行完毕,子线程还正在执行中,就跟着结束了,整个进程也结束了。

线程和函数的关系

刚开始接触多线程的同学,经常把线程和函数混淆,误认为线程就是关联的函数,而且还会意淫出一个线程函数的概念。其实线程和函数是八竿子打不着的关系,线程是操作系统给进程分配和管理的,而线程一般是依托于某个函数去执行,所以哪个线程调用哪个函数,这个函数内的代码就属于哪个线程的。

import threading
def func():
    print('func 函数当前线程 %s' % threading.current_thread().name)

def xx():
    print('xx 函数当前线程 %s' % threading.current_thread().name)
    func()

if __name__ == '__main__':
    print('main 函数当前线程 %s' % threading.current_thread().name)
    t1 = threading.Thread(target=func)
    t2 = threading.Thread(target=xx)
    t1.start()
    t2.start()
    func()
    t1.join()
    t2.join()

执行结果:

main 函数当前线程 MainThread
func 函数当前线程 Thread-1     # t1.start()调用
func 函数当前线程 MainThread   # 主线程直接调用 func()
xx 函数当前线程 Thread-2       # t2.start() 调用
func 函数当前线程 Thread-2     # t2.start() 调用的 xx() 函数中调用 func()

有时候我们想查看进程中有多少个活动的线程,可以使用 threading.enumerate() 方法进行查看。

import threading
import time

def func():
    print('func 函数当前线程 %s' % threading.current_thread().name)
    time.sleep(2)  # 防止函数过快的退出

def xx():
    print('xx 函数当前线程 %s' % threading.current_thread().name)
    while True:  # 死循环
        pass

if __name__ == '__main__':
    print('main 函数当前线程 %s' % threading.current_thread().name)
    t1 = threading.Thread(target=func)
    t2 = threading.Thread(target=xx)
    t1.start()
    t2.start()

    threadingnames = threading.enumerate()  # 枚举出正在活动的线程,返回列表
    print("目前正在活动的线程:", threadingnames)

    time.sleep(3)  # 3 秒后 t1 线程关联的函数 func 退出,t1 线程结束。

    threadingnames = threading.enumerate()  # 枚举出正在活动的线程,返回列表
    print("目前正在活动的线程:", threadingnames)  # 目前只有主线程和 t2 线程存在

执行结果:

main 函数当前线程 MainThread
func 函数当前线程 Thread-1
xx 函数当前线程 Thread-2
目前正在活动的线程: [<_MainThread(MainThread, started 2564)>, <Thread(Thread-1, started 11412)>, <Thread(Thread-2, started 9948)>]
目前正在活动的线程: [<_MainThread(MainThread, started 2564)>, <Thread(Thread-2, started 9948)>]

由以上结果可知,当线程依托存活的函数执行结束,线程本身也就结束了。这才是结束线程最正规的方法。

停止线程

我们都知道 threading 的 start 函数可以启动线程,但是 threading 并没有提供暂停, 恢复和停止线程的方法, 一旦线程对象调用 start 启动后,线程将由操作系统来全权管理,此时,对我们的应用程序来说该线程就属于失控状态,只能等到对应的方法函数运行完毕,线程才会退出。

之所以想终止线程,是因为线程被卡在了一个地方,可能是 while True 循环,也可能是需要运算时间很长的语句,总之,让线程退出是我们常见的需求,下面我们就介绍几种方案。

第一种方案,调用 setDaemon(True) 方法,我们可以让子线程成为主线程的守护线程,这些线程会在主线程终止时自动销毁,但是,这种方案局限性很大,比如,我们无法在主线程存活的情况下,主动结束子线程,上面有该方法的代码案例,在此我们就不在重复写代码了。

第二种方案,子线程带一个退出请求标志,在线程中不停的去轮询这个标志值,看是不是该自己离开了,当你想让该线程退出的时候,你可以在其它线程中设置这个退出标志。

import threading
import time

def terminate():
    global flag_running
    flag_running = False

def func():
    while flag_running:
        print("I'm running...")
        time.sleep(1)

flag_running = True
t = threading.Thread(target=func)
t.start()

count = 10
while count:  # 主线程 10 秒后退出
    count -= 1
    if count == 5:  # 让子线程 5 秒后退出
        terminate()
    time.sleep(1)

如果线程执行一些像 I/O 这样的阻塞操作,那么通过轮询来终止线程将使得线程之间的协调变得非常棘手。比如,如果一个线程一直阻塞在一个 I/O 操作上,它就永远无法返回,也就无法检查自己是否已经被结束了。我们可以利用超时循环来退出阻塞,让程序可以继续轮询退出标志。

import threading
import time
import socket

def terminate():
    global flag_running
    flag_running = False

def func():
    sv_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  # 创建套接字
    sv_ipport = ("0.0.0.0", 8889)  # 监听的 ip 和端口
    sv_socket.bind(sv_ipport)      # 绑定服务地址
    sv_socket.listen(5)            # 协议栈缓冲区最大套接字存放个数
    print('启动服务器,等待客户端连接......')

    sv_socket.settimeout(5)  # 设置 5 秒钟收不到网络发来的数据,阻塞出就抛出超时异常,
    while flag_running:
        try:
            server_socket, addr = sv_socket.accept()  # 等待新客户端的连接请求数据
            break
        except socket.timeout:
            print("继续等待连接请求...")
            continue
    return

flag_running = True
t = threading.Thread(target=func)
t.start()

count = 20
while count:         # 主线程 20 秒后退出
    count -= 1
    if count == 10:  # count 等于 10 让子线程退出
        terminate()
    time.sleep(1)

网上有人说可以用用 ctypes 的 python api 接口结束线程,此种方式只能强制结束正在运行的线程而不报错,无法结束被卡主不动的线程,并且,此种方式无法结束 python3 的线程。

线程调用类中的函数

对于 threading 模块中的 Thread 类,本质上是执行了它的 run 方法。因此可以自定义线程类,让它继承 Thread 类,然后重写 run 方法。

import threading

class MyThreading(threading.Thread):
    def __init__(self):
        super(MyThreading,self).__init__()

    def run(self):
        print("I'm running...")

obj = MyThreading()
obj.start()
obj.join()

当然你也可以直接用线程关联类的成员函数,像上面我们讲的启动一般函数那样启动成员函数。

import threading

class Mytest(object):
    def func(self):
        print('当前线程%s' % threading.current_thread().name)


myt = Mytest()
t = threading.Thread(target=Mytest.func, args=(myt,))  # 别忘了把对象本身作为成员函数的第一个参数传过去
t.start()

多线程的“返回值”

我们突然间发现,我们没有办法获取线程所启动函数的返回值,这是一件很尴尬的事情。但是作为一个码农,一定要有开放性的思维,不然会被打的。

import threading

rst = None    # 保存线程关联函数的返回值
flag = False  # 作为线程关联函数结束的标志

def func(data):
    global rst
    global flag
    rst = data * data
    flag = True

if __name__ == '__main__':
    t = threading.Thread(target=func, args=(3,))
    t.start()
    while True:
        if flag:  # flag 为 True 时,线程关联的函数运行结束
            print(rst)
            break

分析以上代码,我们定义一个全局变量 rst 来存储子线程启动的 func 函数所计算的结果。注意:因为有可能主线程中的 print语句先于子线程的 rst = data * data语句执行,我们定义一个 flag 标志变量来进行逻辑上的判断。

线程锁

由于线程之间的任务执行是 CPU 进行随机调度的,并且每个线程可能只执行了 n 条指令之后就被切换到别的线程了。当多个线程同时操作一个对象,如果没有很好地保护该对象,会造成程序结果的不可预期,这被称为“线程不安全”。为了保证数据安全,我们设计了线程锁,即同一时刻只允许一个线程操作该数据。线程锁用于锁定资源,可以同时使用多个锁,当你需要独占某一资源时,任何一个锁都可以锁这个资源,就好比你用不同的锁都可以把相同的一个箱子锁住是一个道理。

我们先看一下没有锁的情况下,是如何导致错误的结果的。

import threading

def calculate(num):
    global totalnum

    for _ in range(10000):
        totalnum = totalnum + num
        totalnum = totalnum - num

if __name__ == '__main__':
    totalnum = 0

    t1 = threading.Thread(target=calculate, args=(3,))
    t2 = threading.Thread(target=calculate, args=(7,))

    t1.start()
    t2.start()

    t1.join()
    t2.join()

    print(totalnum)

执行结果(每次数值可能都不一样):

298

我们定义了一个共享变量 totalnum,初始值为 0,并且启动两个线程,先存后取,理论上结果应该为 0,但是,由于线程的调度是由操作系统决定的,当 t1、t2 交替执行时,只要循环次数足够多,totalnum 的结果就不一定是 0 了。

原因是因为高级语言的一条语句在 CPU 执行时是若干条语句,即使一个简单的计算:

totalnum = totalnum + num

当 CPU 对该语句进行计算时分为如下两步:

    计算 totalnum + num,存入临时变量中;
    将临时变量的值赋给 totalnum。

也就是可以看成:

temp = totalnum + n
totalnum = temp

由于 temp 是临时变量,两个线程各自都有自己的 temp,当代码正常执行时:

初始值 totalnum = 0

t1: temp1 = totalnum + 3  # temp1 = 0 + 3 = 3
t1: totalnum = temp1      # totalnum = 3
t1: temp1 = totalnum - 3  # temp1 = 3 - 3 = 0
t1: totalnum = temp1      # totalnum = 0

t2: temp2 = totalnum + 7  # temp2 = 0 + 7 = 8
t2: totalnum = temp2      # totalnum = 7
t2: temp2 = totalnum - 7  # temp2 = 7 - 7 = 0
t2: totalnum = temp2      # totalnum = 0
t2: totalnum = temp2      # totalnum = 0

结果 totalnum = 0

但是 t1 和 t2 是当拿到操作系统的时间片是才执行,可能刚执行某一条语句,CPU 的使用权就被切换到另一个线程中了,如果操作系统以下面的顺序执行 t1、t2:

初始值 totalnum = 0

t1: temp1 = totalnum + 3  # temp1 = 0 + 3 = 3

t2: temp2 = totalnum + 7  # temp2 = 0 + 7 = 7
t2: totalnum = temp2      # totalnum = 7

t1: totalnum = temp1      # totalnum = 3
t1: temp1 = totalnum - 3  # temp1 = 3 - 3 = 0
t1: totalnum = temp1      # totalnum = 0

t2: temp2 = totalnum - 7  # temp2 = 0 - 7 = -7
t2: totalnum = temp2      # totalnum = -7

结果 totalnum = -7

究其原因,是因为修改 totalnum 需要多条语句,而执行这几条语句时,线程可能中断,从而导致多个线程把同一个对象的内容改乱了。

两个线程同时一存一取,就可能导致余额不对,你肯定不希望你的银行存款莫名其妙地变成了负数,所以,我们必须确保一个线程在修改 totalnum 的时候,别的线程一定不能改。

如果我们要确保 totalnum 计算正确,就要给写数据的操作上一把锁,当某个线程开始执行 totalnum = totalnum + numtotalnum = totalnum - num 时,我们说,该线程因为获得了锁,因此其他线程不能同时执行这条语句,只能等待,直到锁被释放后,获得该锁以后才能改。由于锁只有一个,无论多少线程,同一时刻最多只有一个线程持有该锁,所以,不会造成修改的冲突。创建一个锁就是通过 threading.Lock() 来实现:

当然,我们可以分别对 totalnum = totalnum + numtotalnum = totalnum - num加锁,也可以把这两个语句加一把锁,在此为了方便,我就给这两条语句加一把锁。

Python 在 threading 模块中定义了几种线程锁类,分别是:

  • Lock 互斥锁
  • RLock 可重入锁
  • Semaphore 信号
  • Event 事件
  • Condition 条件
  • Barrier “阻碍”

互斥锁 Lock

互斥锁是一种独占锁,同一时刻只有一个线程可以访问共享的数据。使用很简单,初始化锁对象,然后将锁当做参数传递给任务函数,在任务中加锁,使用后释放锁。

import threading

def calculate(num):
    global totalnum
    for _ in range(10000):
        lock.acquire()
        totalnum = totalnum + num
        totalnum = totalnum - num
        lock.release()

if __name__ == '__main__':
    lock = threading.Lock()
    totalnum = 0

    t1 = threading.Thread(target=calculate, args=(3,))
    t2 = threading.Thread(target=calculate, args=(7,))

    t1.start()
    t2.start()

    t1.join()
    t2.join()

    print(totalnum)

RLock 的使用方法和 Lock 一模一样,只不过它支持重入锁。该锁对象内部维护着一个 Lock 和一个 counter 对象。counter 对象记录了 acquire 的次数,使得资源可以被多次 require。最后,当所有 RLock 被 release 后,其他线程才能获取资源。在同一个线程中,RLock.acquire() 可以被多次调用,利用该特性,可以解决部分死锁问题。

信号 Semaphore

类名:BoundedSemaphore。这种锁允许一定数量的线程同时更改数据,它不是互斥锁。比如地铁安检,排队人很多,工作人员只允许一定数量的人进入安检区,其它的人继续排队。

import time
import threading

def run(n, se):
    se.acquire()
    print("run the thread: %s" % n)
    time.sleep(1)
    se.release()

# 设置允许 10 个线程同时运行
semaphore = threading.BoundedSemaphore(10)
for i in range(20):
    t = threading.Thread(target=run, args=(i, semaphore))
    t.start()

运行后,可以看到 10 个一批的线程被放行。

事件 Event

事件线程锁的运行机制:全局定义了一个 Flag,如果 Flag 的值为 False,那么当程序执行 wait() 方法时就会阻塞,如果 Flag 值为 True,线程不再阻塞。这种锁,类似交通红绿灯(默认是红灯),它属于在红灯的时候一次性阻挡所有线程,在绿灯的时候,一次性放行所有排队中的线程。

事件主要提供了四个方法 set()、wait()、clear()和 is_set()。

调用 clear()方法会将事件的 Flag 设置为 False。

调用 set()方法会将 Flag设置为 True。

调用 wait()方法将等待“红绿灯”信号。

is_set():判断当前是否"绿灯放行"状态

下面是一个模拟红绿灯,然后汽车通行的例子:

# 利用 Event 类模拟红绿灯
import threading
import time

event = threading.Event()

def lighter():
    green_time = 10       # 绿灯时间
    red_time = 10         # 红灯时间
    event.set()           # 初始设为绿灯
    while True:
        print("\33[32;0m 绿灯亮...\033[0m")
        time.sleep(green_time)
        event.clear()
        print("\33[31;0m 红灯亮...\033[0m")
        time.sleep(red_time)
        event.set()

def run(name):
    while True:
        if event.is_set():  # 判断当前是否绿灯
            print("一辆[%s] 呼啸开过..." % name)
            time.sleep(1)
        else:
            print("一辆[%s]开来,看到红灯,无奈的停下了..." % name)
            event.wait()
            print("[%s] 看到绿灯亮了,瞬间飞起....." % name)

if __name__ == '__main__':

    light = threading.Thread(target=lighter,)
    light.start()

    for name in ['雷克萨斯', '宾利', '劳斯莱斯']:
        car = threading.Thread(target=run, args=(name,))
        car.start()

运行结果:

 绿灯亮...
一辆[雷克萨斯] 呼啸开过...
一辆[宾利] 呼啸开过...
一辆[劳斯莱斯] 呼啸开过...
一辆[雷克萨斯] 呼啸开过...
一辆[劳斯莱斯] 呼啸开过...
一辆[宾利] 呼啸开过...
一辆[宾利] 呼啸开过...
红灯亮...
一辆[劳斯莱斯]开来,看到红灯,停了下来...
一辆[宾利]开来,看到红灯,停了下来...
一辆[雷克萨斯]开来,看到红灯,停了下来...
[雷克萨斯] 看到绿灯亮了,开动起来.....
一辆[雷克萨斯] 呼啸开过...
绿灯亮...
[劳斯莱斯] 看到绿灯亮了,开动起来.....
一辆[劳斯莱斯] 呼啸开过...
[宾利] 看到绿灯亮了,开动起来.....
一辆[宾利] 呼啸开过...
一辆[劳斯莱斯] 呼啸开过...一辆[雷克萨斯] 呼啸开过...

条件 Condition

Condition 称作条件锁,依然是通过 acquire()/release()加锁解锁。

wait([timeout])方法将使线程进入 Condition 的等待池等待通知,并释放锁。使用前线程必须已获得锁定,否则将抛出异常。

notify() 方法将从等待池挑选一个线程并通知,收到通知的线程将自动调用 acquire() 尝试获得锁定(进入锁定池),其他线程仍然在等待池中。调用这个方法不会释放锁定。使用前线程必须已获得锁定,否则将抛出异常。

notifyAll() 方法将通知等待池中所有的线程,这些线程都将进入锁定池尝试获得锁定。调用这个方法不会释放锁定。使用前线程必须已获得锁定,否则将抛出异常。

下面的例子,有助于你理解 Condition 的使用方法:

import threading
import time

num = 0
con = threading.Condition()

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

    def run(self):
        global num
        con.acquire()
        print("%s开始执行..." % self.name)
        while True:
            if self.action == "add":
                num += 1
            elif self.action == 'reduce':
                num -= 1
            else:
                exit(1)
            print("num当前为:", num)
            time.sleep(1)
            if num == 5 or num == 0:
                print("暂停执行%s!" % self.name)
                con.notify()
                con.wait()
                print("%s开始执行..." % self.name)
        con.release()

if __name__ == '__main__':
    t1 = Mythread("线程 A", 'add')
    t2 = Mythread("线程 B", 'reduce')

    t1.start()
    t2.start()

    t1.join()
    t2.join()

如果不强制停止,程序会一直执行下去,并循环下面的结果:

线程 A开始执行...
num当前为: 1
num当前为: 2
num当前为: 3
num当前为: 4
num当前为: 5
暂停执行线程 A!
线程 B开始执行...
num当前为: 4
num当前为: 3
num当前为: 2
num当前为: 1
num当前为: 0
暂停执行线程 B!
线程 A开始执行...

定时器 Timer

定时器 Timer 类是 threading 模块中的一个小工具,用于指定 n 秒后执行某操作。一个简单但很实用的东西。

from threading import Timer

def func():
    print("老鸟python")

# 表示 3 秒后执行 hello 函数
t = Timer(3, func)
t.start()
t.join()

全局解释器锁(GIL)

既然介绍了多线程和线程锁,那就不得不提及 Python 的 GIL 问题。

在大多数环境中,单核 CPU 情况下,本质上某一时刻只能有一个线程被执行,多核 CPU 时则 可以支持多个线程同时执行。但是在 Python 中,无论 CPU 有多少核,同时只能执行一个线程。这是由于 GIL 的存在导致的。

GIL 的全称是 Global Interpreter Lock(全局解释器锁),是 Python 设计之初为了数据安全所做的决定。Python 中的某个线程想要执行,必须先拿到 GIL。可以把 GIL 看作是执行任务的“通行证”,并且在一个 Python 进程中,GIL 只有一个。拿不到通行证的线程,就不允许进入 CPU 执行。GIL 只在 CPython 解释器中才有,因为 CPython 调用的是 c 语言的原生线程,不能直接操作 cpu,只能利用 GIL 保证同一时间只能有一个线程拿到数据。在 PyPy 和 JPython 中没有 GIL。

Python 多线程的工作流程:

    拿到公共数据

    申请 GIL

    Python 解释器调用操作系统原生线程

    cpu 执行运算

    当该线程执行一段时间消耗完,无论任务是否已经执行完毕,都会释放 GIL

    下一个被 CPU 调度的线程重复上面的过程

Python 针对不同类型的任务,多线程执行效率是不同的:

对于 CPU 密集型任务(各种循环处理、计算等等),由于计算工作多,ticks 计数很快就会达到阈值,然后触发 GIL 的释放与再竞争(多个线程来回切换是需要消耗资源的),所以 Python 下的多线程对 CPU 密集型任务并不友好。

IO 密集型任务(文件处理、网络通信等涉及数据读写的操作),多线程能够有效提升效率(单线程下有 IO 操作会进行 IO 等待,造成不必要的时间浪费,而开启多线程能在线程A等待时,自动切换到线程 B,可以不浪费 CPU 的资源,从而能提升程序执行效率)。所以 Python 的多线程对 IO 密集型任务比较友好。

对于计算密集型的业务,我们可以使用多进程来充分利用多核 CPU。下面我们分别用单线程,多线程,多进程对计算密集型任务来做性能评估。

单线程:

import time


def counter():
    count = 1
    data = 1
    while count < 500000:  # 计算密集型
        count += 1
        data += data


if __name__ == '__main__':
    bgtime = time.time()
    counter()
    counter()
    endtime = time.time()
    print("单线程用时", endtime - bgtime)

运行结果:

单线程用时 5.936356735229492

多线程:

from threading import Thread
import time


def counter():
    count = 1
    data = 1
    while count < 500000:  # 计算密集型
        count += 1
        data += data


if __name__ == '__main__':
    bgtime = time.time()
    t1 = Thread(target=counter)
    t2 = Thread(target=counter)
    t1.start()
    t2.start()
    t1.join()
    t2.join()
    endtime = time.time()
    print("多线程用时", endtime - bgtime)

运行结果:

多线程用时 6.170341682434082

多进程:

from multiprocessing import Process
import time


def counter():
    count = 1
    data = 1
    while count < 500000:  # 计算密集型
        count += 1
        data += data


if __name__ == '__main__':
    bgtime = time.time()
    p1 = Process(target=counter)
    p2 = Process(target=counter)
    p1.start()
    p2.start()
    p1.join()
    p2.join()
    endtime = time.time()
    print("多进程用时", endtime - bgtime)

运行结果:

多进程用时 3.7912168502807617

为什么不能去掉GIL?

首先,在早期的 Python 解释器依赖较多的全局状态,传承下来,使得想要移除当今的 GIL 变得更加困难。其次,对于程序员而言,仅仅是理解 GIL 的实现就需要对操作系统设计、多线程编程、C语言、解释器设计和 CPython 解释器的实现有着非常彻底的理解,更不用说对它进行修改删除了。总之,整体技术难度大,会对当前内部框架产生根本性的影响,牵一发而动全身。

在 1999 年,针对 Python1.5,一个叫做“freethreading”的补丁已经尝试移除 GIL,用细粒度的锁来代替。然而,GIL 的移除给单线程程序的执行速度带来了一定的负面影响。当用单线程执行时,速度大约降低了 40%。虽然使用两个线程时在速度上得到了提高,但这个提高并没有随着核数的增加而线性增长。因此这个补丁没有被采纳。

虽然,在 Python 的不同解释器实现中,如 PyPy 就移除了 GIL,其执行速度更快(不单单是去除 GIL 的原因)。但是,我们通常使用的 CPython 解释器版本占有着统治地位的使用量,所以,你懂的。

在实际使用中的建议:

Python 中想要充分利用多核 CPU,就用多进程。因为每个进程有各自独立的 GIL,互不干扰,这样就可以真正意义上的并行执行。在 Python 中,多进程的执行效率优于多线程(仅仅针对多核 CPU 而言)。同时建议在 IO 密集型任务中使用多线程,在计算密集型任务中使用多进程。另外,深入研究 Python 的协程机制,你会有惊喜的。

更多的详细介绍和说明请参考下面的文献: 英文原版:Python's Hardest Problem 中文翻译:Python 最难的问题

本节重要知识点

会使用多线程。

会处理共享变量。

了解线程和函数的关系。

作业

著名公司(某度)笔试题:有一个子线程里面有死循环存在,主线程里面有个 count,我们想让 count 值等于 5 的时候,结束掉该子线程。在子线程里面补充完成代码。

import threading
import time

def func():
    while True:
        time.sleep(1)
        print('func 函数当前线程 %s' % threading.current_thread().name)
        pass  # 在此完成代码

if __name__ == '__main__':
    print('main 函数当前线程 %s' % threading.current_thread().name)
    t1 = threading.Thread(target=func)
    t1.start()

    count = 10
    while count:
        count -= 1
        time.sleep(1)

如果以上内容对您有帮助,请老板用微信扫一下赞赏码,赞赏后加微信号 birdpython 领取免费视频。


登录后评论

user_image
怎么改名字
2020年10月20日 00:58 回复

谢谢 答案很清晰 我想知道配合协程 能不能解决python多线程的一些问题 又或者还有哪些问题用上协程也没法解决呢


user_image
Hush-MSFT
2020年6月28日 05:24 回复

如果我爬虫要下载100个资源,是应该用全局解释器的线程还是用多进程呢?CPU是四核8线程,会影响我开的进程或线程数目吗


user_image
Gaulermat
2019年9月1日 08:03 回复

楼主解释得太优秀了


user_image
1不2
2019年4月19日 18:42 回复

python可以用多进程处理cpu密集型任务吗?不是多线程。


user_image
yonka
2019年1月14日 13:22 回复

写的挺好,我可以转载到我的csdn嘛,我会标注你的来源网址