python 多线程

https://zhuanlan.zhihu.com/p/490353142

 

1. 进程vs线程

进程(process)指的是正在运行的程序的实例,即an instance of a computer that is being executed。用拆字法理解就是:进行中的程序。程序是一个没有生命的实体,只有处理器执行它的时候才能成为一个活动的实体,称之为进程。[1]

线程(thread)包含于进程之中,是操作系统能够进行运算调度的最小单元。一条进程中可以并发多个线程,而同一条线程将共享该进程中的全部系统资源。[2]

每个进程都有自己独立的地址空间、内存和数据栈,因此进程之间通讯不方便,要是用进程间通讯(InterProcess Communication, IPC)。而同一个进程中的线程共享资源,因此线程间通讯非常方便,但要注意数据同步与互斥的问题。

虽然一条进程中可以并发多个线程,但是对于单核CPU而言,同一时间CPU只能运行一个线程。

2. thread vs threading

Python处理线程的模块有两个:thread和threading。Python 3已经停用了thread模块[3],并改名为_thread模块。Python 3在_thread模块的基础上开发了更高级的threading模块,因此以下的讲解都是基于threading模块。

3. 如何创建一个线程?

根据threading底层代码的说明,创建一个线程通常有两种方法:(1)在实例化一个线程对象时,将要执行的任务函数以参数的形式传入;(2)继承Thread类的同时重写它的run方法。

threading.Class类

现在我准备创建两个线程,一个线程每隔一秒打印一个“1”,另一个线程每隔2秒打印一个“2”,如何创建并执行呢?两种方法如下:

3.1 方法一

import time
import threading

def printNumber(n: int) -> None:
    while True:
        print(n)
        time.sleep(n)

for i in range(1, 3):
    t = threading.Thread(target=printNumber, args=(i, ))
    t.start()

运行结果如下,控制台会不停地、交错地打印“1”和“2”:

运行结果

3.2 方法二

import time
import threading

class MyThread(threading.Thread):

    def __init__(self, n):
        self.n = n
        # 注意:一定要调用父类的初始化函数,否则否发创建线程
        super().__init__()

    def run(self) -> None:
        while True:
            print(self.n)
            time.sleep(self.n)

for i in range(1, 3):
    t = MyThread(i)
    t.start()

运行结果如下,控制台会不停地、交错地打印“1”和“2”:

运行结果

4. 主线程和子线程

我们先把上述的代码简单做一下修改,让它在打印的同时打印活跃的线程个数,代码如下

import time
import threading

class MyThread(threading.Thread):

    def __init__(self, n):
        self.n = n
        super().__init__()

    def run(self) -> None:
        while True:
            _count = threading.active_count()
            print(self.n, f"当前活跃的线程个数:{_count}")
            time.sleep(self.n)

for i in range(1, 3):
    t = MyThread(i)
    t.start()

该代码执行结果如下:

这里活跃的线程个数怎么理解?

好那么问题来了:当我创建了线程1并开始执行的时候,程序却告诉我有2个活跃的线程呢?同样地,我最终只创建了2个线程,为什么程序却告诉我有3个活跃的线程呢?

让我们回到进程和线程的定义,当我们开始执行这个程序的时候,这个程序成为一个“有生命的”进程,进程至少有一个线程,这个线程就是主线程。当程序执行到第一次t.start()的时候,程序创建了一个子线程,此时活跃的线程个数是2。进一步,当执行第二次t.start()的时候,程序又创建了一个子线程,因此最终活跃的线程个数是3。

注意每个进程只有一个主线程。

5. 守护线程(Daemon Thread)

守护线程(Daemon Thread)也叫后台进程,它的目的是为其他线程提供服务。如果其他线程被杀死了,那么守护线程也就没有了存在的必要。因此守护线程会随着非守护线程的消亡而消亡。Thread类中,子线程被创建时默认是非守护线程,我们可以通过setDaemon(True)将一个子线程设置为守护线程。

我们把上面这个例子中创建的两个子线程改写为守护线程,看看会发生什么:

import time
import threading

class MyThread(threading.Thread):

    def __init__(self, n):
        self.n = n
        super().__init__()

    def run(self) -> None:
        while True:
            _count = threading.active_count()
            print(self.n, f"当前活跃的线程个数:{_count}")
            time.sleep(self.n)

for i in range(1, 3):
    t = MyThread(i)
    t.setDaemon(True)
    t.start()
print("结束!")

运行结果如下:

和前面不同,程序打印完“结束!”彻底结束了

和前面完全不同的是:程序打印完“结束!”后就彻底结束了,不再打印任何内容。这是为什么呢?

因为当程序执行完print("结束!")以后,主线程就可以结束了,这时候被设定为守护线程的两个子线程会被杀死,然后主线程结束。

现在,如果我把两个子线程的其中一个设置为守护线程,另一个设置为非守护线程,会怎样呢?代码如下:

import time
import threading

class MyThread(threading.Thread):

    def __init__(self, n):
        self.n = n
        super().__init__()

    def run(self) -> None:
        while True:
            _count = threading.active_count()
            print(self.n, f"当前活跃的线程个数:{_count}")
            time.sleep(self.n)

for i in range(1, 3):
    t = MyThread(i)
    if i == 1:
        t.setDaemon(True)       # 将其中一个线程设置为守护线程
    t.start()
print("结束!")

你可能会想,守护线程会被杀死,非守护线程继续执行。但实际情况并非如此,结果如下:

两个子线程都在继续执行

这是因为非守护线程作为前台程序还在继续执行,守护线程就还有“守护”的意义,就会继续执行。

需要注意的是:将子线程设置为守护线程必须在调用start()方法之前,否则回引发RuntimeError异常。

6. join()方法

join()会使主线程进入等待状态(阻塞),直到调用join()方法的子线程运行结束。同时你也可以通过设置timeout参数来设定等待的时间,如:

import time
import threading

class MyThread(threading.Thread):

    def __init__(self, n):
        self.n = n
        super().__init__()

    def run(self) -> None:
        while True:
            _count = threading.active_count()
            print(f"线程-{self.n}", f"当前活跃的线程个数:{_count}")
            time.sleep(self.n)

for i in range(1, 3):
    t = MyThread(i)
    t.start()
    t.join(3)

执行结果如下:

通过join()方法实现了主线程阻塞

7. 数据安全与线程锁

现在假设你创建了两个子线程操作同一个全局变量number,number被初始化为0,两个子线程通过for循环对这个number进行+1,每个子线程循环1000000次,两个子线程同时进行。如果一切正常的话,最终这个number会变成2000000,然而现实并非如此。代码如下

import time
import threading

number = 0

class MyThread(threading.Thread):

    def __init__(self, n):
        self.n = n
        super().__init__()

    def run(self) -> None:
        global number
        for i in range(1000000):
            number += 1

for i in range(1, 3):
    t = MyThread(i)
    t.start()

# 给5秒钟让两个子线程执行完毕
time.sleep(5)
# 确保两个子线程执行完毕
print("活跃的线程个数:", threading.active_count())
# 输出最终数值
print("number: ", number)

执行结果如下:

结果并不是2000000

这种情况称为“脏数据”。产生脏数据的原因是,当一个线程在对数据进行修改时,修改到一半时另一个线程读取了未经修改的数据并进行修改。如何避免脏数据的产生呢?一个办法就是用join方法,即先让一个线程执行完毕再执行另一个线程。但这样的本质是把多线程变成了单线程,失去了多线程的意义。另一个办法就是用线程锁,threading模块中有如下几种线程锁[4]

7.1 Lock互斥锁

import time
import threading

number = 0
lock = threading.Lock()             # 实例化一个锁

class MyThread(threading.Thread):

    def __init__(self, n):
        self.n = n
        super().__init__()

    def run(self) -> None:
        global number
        for i in range(1000000):
            lock.acquire()          # 开锁,只允许当前线程访问共享的数据
            number += 1
            lock.release()          # 释放锁,允许其他线程访问共享数据

for i in range(1, 3):
    t = MyThread(i)
    t.start()

# 给5秒钟让两个子线程执行完毕
time.sleep(5)
# 确保两个子线程执行完毕
print("活跃的线程个数:", threading.active_count())
# 输出最终数值
print("number: ", number)

执行结果如下:

输出正常

7.2 RLock

RLock和Lock的用法相同,区别在于:Lock只能开一次然后释放一次,不能开多次,而RLock可以开多次,再进行多次释放[5]。当然需要注意的是:RLock中虽然可以开多次,但是acquire和release的次数必须对应。

7.3 Semaphore

BoundedSemaphore类可以设置同一时间更改数据的线程个数

import time
import threading

semaphore = threading.BoundedSemaphore(3)

class MyThread(threading.Thread):

    def __init__(self, n):
        self.n = n
        super().__init__()

    def run(self) -> None:
        semaphore.acquire()
        for i in range(100):
            _count = threading.active_count() - 1
            print(f"线程-{self.n}", f"当前活跃的子线程个数:{_count}")
            time.sleep(1)
        semaphore.release()            

for i in range(1, 10):
    t = MyThread(i)
    t.start()

执行结果如下:

Semaphore设置了同时执行的线程的个数

7.4 Event

Event类会在全局定义一个Flag,当Flag=False时,调用wait()方法会阻塞所有线程;而当Flag=True时,调用wait()方法不再阻塞。形象的比喻就是“红绿灯”:在红灯时阻塞所有线程,而在绿灯时又会一次性放行所有排队中的线程。Event类有四个方法:

  • set():将Flag设置为True
  • wait():等待
  • clear():将Flag设置为False
  • is_set():返回bool值,判断Flag是否为True

Event的一个好处是:可以实现线程间通信,通过一个线程去控制另一个线程。

import time
import threading

event = threading.Event()
event.set()     # 设定Flag = True

class MyThread(threading.Thread):

    def __init__(self, n):
        self.n = n
        super().__init__()

    def run(self) -> None:
        if self.n in [3, 4]:
            event.clear()   # 设定Flag = False
            event.wait()    # 线程3和4进入等待
        
        for i in range(2):
            _count = threading.active_count() - 1
            print(f"线程-{self.n}", f"当前活跃的子线程个数:{_count}")
            time.sleep(2)
            if self.n == 2 and i == 1:
                # 通过线程2来控制线程3和4
                event.set()

for i in range(1, 5):
    t = MyThread(i)
    t.start()

执行结果如下:

通过一个线程去控制另一个线程

8. 一些小技巧

8.1 with上下门管理器

在使用Lock和RLock时,正确的开锁-释放锁非常重要。通过with上下文管理器,可以保证线程锁被正确释放,而且代码也更加简洁。如:

import time
import threading

number = 0
lock = threading.Lock()             # 实例化一个锁

class MyThread(threading.Thread):

    def __init__(self, n):
        self.n = n
        super().__init__()

    def run(self) -> None:
        global number
        for i in range(1000000):
            with lock:      # with上下文管理器
                number += 1

for i in range(1, 3):
    t = MyThread(i)
    t.start()

# 给5秒钟让两个子线程执行完毕
time.sleep(5)
# 确保两个子线程执行完毕
print("活跃的线程个数:", threading.active_count())
# 输出最终数值
print("number: ", number)

8.2 Timer计时器

通过threading.Timer类可以实现n秒后执行某操作。注意一个timer对象相当于一个新的子线程。

for i in range(1, 5):
    t = MyThread(i)
    if i == 4: 
        timer = Timer(0.1, t.start)       # 5秒后再开始线程4
        timer.start()
    else:
        t.start()

 

参考

  1. ^https://baike.baidu.com/item/%E8%BF%9B%E7%A8%8B/382503#:~:text=%E8%BF%9B%E7%A8%8B%EF%BC%88Process%EF%BC%89%E6%98%AF%E8%AE%A1%E7%AE%97%E6%9C%BA%E4%B8%AD,%E8%BF%9B%E7%A8%8B%E6%98%AF%E7%A8%8B%E5%BA%8F%E7%9A%84%E5%AE%9E%E4%BD%93%E3%80%82
  2. ^https://baike.baidu.com/item/%E7%BA%BF%E7%A8%8B/103101
  3. ^https://peps.python.org/pep-3108/#obsolete
  4. ^https://www.liujiangblog.com/course/python/79
  5. ^https://stackoverflow.com/questions/22885775/what-is-the-difference-between-lock-and-rlock
posted @ 2022-12-07 15:54  kimiandkevin  阅读(156)  评论(0编辑  收藏  举报