什么是python多线程,创建多线程方式详解

概念

线程是处理器调度和分配的基本单位,进程则作为资源拥有的基本单位。每个进程是由私有的虚拟地址空间、代码、数据和其它各种系统资源组成。线程是进程内部的一个执行单元。每一个进程至少有一个主执行线程,它无需由用户去主动创建,是由系统自动创建的。 用户根据需要在应用程序中创建其它线程,多个线程并发地运行于同一个进程中。

python多线程

创建线程的方式-threading

方法1

在实例化一个线程对象时,将要执行的任务函数以参数的形式传入threading

# -*- coding: utf-8 -*-
import time
import threading
import datetime


def printNumber(n: int) -> None:
    while True:
        times = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
        print(f'{times}-{n}')
        time.sleep(n)


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

# 输出
2022-12-16 11:04:40-1
2022-12-16 11:04:40-2
2022-12-16 11:04:41-1
2022-12-16 11:04:42-2
2022-12-16 11:04:42-1
2022-12-16 11:04:43-1
2022-12-16 11:04:44-2
2022-12-16 11:04:44-1
2022-12-16 11:04:45-1
2022-12-16 11:04:46-2
2022-12-16 11:04:46-1
2022-12-16 11:04:47-1
....
Process finished with exit code -1

创建两个线程,一个线程每隔一秒打印一个“1”,另一个线程每隔2秒打印一个“2”
Thread 类提供了如下的 init() 构造器,可以用来创建线程:

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

此构造方法中,以上所有参数都是可选参数,即可以使用,也可以忽略。其中各个参数的含义如下:

  • group:指定所创建的线程隶属于哪个线程组(此参数尚未实现,无需调用);
  • target:指定所创建的线程要调度的目标方法(最常用);
  • args:以元组的方式,为 target 指定的方法传递参数;
  • kwargs:以字典的方式,为 target 指定的方法传递参数;
  • daemon:指定所创建的线程是否为后代线程。

这些参数,初学者只需记住 target、args、kwargs 这 3 个参数的功能即可。
但是线程需要手动启动才能运行,threading 模块提供了 start() 方法用来启动线程。因此在上面程序的基础上,添加如下语句:
t.start()

方法2

通过继承 Thread 类,我们可以自定义一个线程类,从而实例化该类对象,获得子线程。

需要注意的是,在创建 Thread 类的子类时,必须重写从父类继承得到的 run() 方法。因为该方法即为要创建的子线程执行的方法,其功能如同第一种创建方法中的 printNumber() 自定义函数。

# -*- coding: utf-8 -*-
import datetime
import time
import threading


class MyThread(threading.Thread):

    def __init__(self, n):
        self.n = n
        # 注意:一定要调用父类的初始化函数
        super().__init__()

    def run(self) -> None:
        while True:
            times = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
            print(f'{times}-{self.n}')
            time.sleep(self.n)


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

# 输出
2022-12-16 12:43:24-1
2022-12-16 12:43:24-2
2022-12-16 12:43:25-1
2022-12-16 12:43:26-2
2022-12-16 12:43:26-1
2022-12-16 12:43:27-1
2022-12-16 12:43:28-2
...

主线程和子线程

# -*- coding: utf-8 -*-
import datetime
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()
            threading_name = threading.current_thread().getName()
            times = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
            print(f'{times}-{self.n}-"当前活跃的线程个数:{_count}"-"当前线程的名称是":{threading_name}')
            time.sleep(self.n)


for i in range(1, 3):
    t = MyThread(i)
    t.start()
    print(threading.current_thread().getName())

# 输出
2022-12-16 13:18:00-1-"当前活跃的线程个数:2"-"当前线程的名称是":Thread-1
MainThread
2022-12-16 13:18:00-2-"当前活跃的线程个数:3"-"当前线程的名称是":Thread-2
MainThread

2022-12-16 13:18:01-1-"当前活跃的线程个数:3"-"当前线程的名称是":Thread-1
2022-12-16 13:18:02-2-"当前活跃的线程个数:3"-"当前线程的名称是":Thread-2
2022-12-16 13:18:02-1-"当前活跃的线程个数:3"-"当前线程的名称是":Thread-1
2022-12-16 13:18:03-1-"当前活跃的线程个数:3"-"当前线程的名称是":Thread-1
2022-12-16 13:18:04-2-"当前活跃的线程个数:3"-"当前线程的名称是":Thread-2
2022-12-16 13:18:04-1-"当前活跃的线程个数:3"-"当前线程的名称是":Thread-1
...

注意: 第一次t.start()后,当前存在两个线程(主线程+子线程),第二次t.start()的时候又创建了一个子线程所以当前存在三个线程

如果程序中不显式创建任何线程,则所有程序的执行,都将由主线程 MainThread 完成,程序就只能按照顺序依次执行。

此程序中,子线程 Thread-1和Thread-2 执行的是 run() 方法中的代码,而 MainThread 执行的是主程序中的代码,它们以快速轮换 CPU 的方式在执行。

守护线程(Daemon Thread)

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

# -*- coding: utf-8 -*-
import datetime
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()
            threading_name = threading.current_thread().getName()
            times = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
            print(f'{times}-{self.n}-"当前活跃的线程个数:{_count}"-"当前线程的名称是":{threading_name}')
            time.sleep(self.n)


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

    print(threading.current_thread().getName())
    
    
# 输出
2022-12-16 13:27:46-1-"当前活跃的线程个数:2"-"当前线程的名称是":Thread-1
MainThread
2022-12-16 13:27:46-2-"当前活跃的线程个数:3"-"当前线程的名称是":Thread-2
MainThread

将两个子线程改写为守护线程,因为当主程序中的代码执行完后,主线程就可以结束了,这时候被设定为守护线程的两个子线程会被杀死,然后主线程结束。

注意,当前台线程死亡后,Python 解释器会通知后台线程死亡,但是从它接收指令到做出响应需要一定的时间。如果要将某个线程设置为后台线程,则必须在该线程启动之前进行设置。也就是说,将 daemon 属性设为 True,必须在 start() 方法调用之前进行,否则会引发 RuntimeError 异常。

若将两个子线程的其中一个设置为守护线程,另一个设置为非守护线程

# -*- coding: utf-8 -*-
import datetime
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()
            threading_name = threading.current_thread().getName()
            times = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
            print(f'{times}-{self.n}-"当前活跃的线程个数:{_count}"-"当前线程的名称是":{threading_name}')
            time.sleep(self.n)


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

    print(threading.current_thread().getName())
 
# 输出
2022-12-16 13:30:17-1-"当前活跃的线程个数:2"-"当前线程的名称是":Thread-1
MainThread

2022-12-16 13:30:17-2-"当前活跃的线程个数:3"-"当前线程的名称是":Thread-2
MainThread

2022-12-16 13:30:18-1-"当前活跃的线程个数:3"-"当前线程的名称是":Thread-1
2022-12-16 13:30:19-1-"当前活跃的线程个数:3"-"当前线程的名称是":Thread-1
2022-12-16 13:30:19-2-"当前活跃的线程个数:3"-"当前线程的名称是":Thread-2
2022-12-16 13:30:20-1-"当前活跃的线程个数:3"-"当前线程的名称是":Thread-1
...

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

join()方法

不使用join方法:

当设置多个线程时,在一般情况下(无守护线程,setDeamon=False),多个线程同时启动,主线程执行完,会等待其他子线程执行完,程序才会退出。

# -*- coding: utf-8 -*-
import datetime
import time
import threading


class MyThread(threading.Thread):

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

    def run(self) -> None:
        _count = threading.active_count()
        threading_name = threading.current_thread().getName()
        times = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
        time.sleep(1)
        print(f'{times}-{self.n}-"当前活跃的线程个数:{_count}"-"当前线程的名称是":{threading_name}')


start_time = time.time()
print(f'{start_time},这是主线程:', threading.current_thread().name)
for i in range(5):
    t = MyThread(i)
    # t.setDaemon(True)
    t.start()
    # t.join()
end_time = time.time()
print(f'{end_time},主线程结束了!', threading.current_thread().name)
print('一共用时:', end_time - start_time)

# 输出
1671174404.6552384,这是主线程: MainThread
1671174404.656239,主线程结束了! MainThread
一共用时: 0.0010006427764892578
2022-12-16 15:06:44-0-"当前活跃的线程个数:2"-"当前线程的名称是":Thread-1
2022-12-16 15:06:44-1-"当前活跃的线程个数:3"-"当前线程的名称是":Thread-2
2022-12-16 15:06:44-2-"当前活跃的线程个数:4"-"当前线程的名称是":Thread-3
2022-12-16 15:06:44-3-"当前活跃的线程个数:5"-"当前线程的名称是":Thread-4
2022-12-16 15:06:44-4-"当前活跃的线程个数:6"-"当前线程的名称是":Thread-5

我们的计时是对主线程计时,主线程结束,计时随之结束,打印出主线程的用时。
主线程的任务完成之后,主线程随之结束,子线程继续执行自己的任务,直到全部的子线程的任务全部结束,程序结束。

使用join()方法:

主线程任务结束之后,进入阻塞状态,一直等待调用join方法的子线程执行结束之后,主线程才会终止。下面的例子是让t调用join()方法。

# -*- coding: utf-8 -*-
import datetime
import time
import threading


class MyThread(threading.Thread):

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

    def run(self) -> None:
        _count = threading.active_count()
        threading_name = threading.current_thread().getName()
        times = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
        time.sleep(1)
        print(f'{times}-{self.n}-"当前活跃的线程个数:{_count}"-"当前线程的名称是":{threading_name}')


start_time = time.time()
print(f'{start_time},这是主线程:', threading.current_thread().name)
for i in range(5):
    t = MyThread(i)
    # t.setDaemon(True)
    t.start()
    t.join()
end_time = time.time()
print(f'{end_time},主线程结束了!', threading.current_thread().name)
print('一共用时:', end_time - start_time)


# 输出
1671174502.0245655,这是主线程: MainThread
2022-12-16 15:08:22-0-"当前活跃的线程个数:2"-"当前线程的名称是":Thread-1
2022-12-16 15:08:23-1-"当前活跃的线程个数:2"-"当前线程的名称是":Thread-2
2022-12-16 15:08:24-2-"当前活跃的线程个数:2"-"当前线程的名称是":Thread-3
2022-12-16 15:08:25-3-"当前活跃的线程个数:2"-"当前线程的名称是":Thread-4
2022-12-16 15:08:26-4-"当前活跃的线程个数:2"-"当前线程的名称是":Thread-5
1671174507.0313594,主线程结束了! MainThread
一共用时: 5.006793975830078

Process finished with exit code 0

join()方法的timeout参数

join的语法结构为join(timeout=None),可以看到join()方法有一个timeout参数,其默认值为None,而参数timeout可以进行赋值,其含义是指定等待被join的线程的时间最长为timeout秒,也就是说当在timeout秒内被join的线程还没有执行结束的话,就不再进行等待了。

# -*- coding: utf-8 -*-
import datetime
import time
import threading


class MyThread(threading.Thread):

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

    def run(self) -> None:
        _count = threading.active_count()
        threading_name = threading.current_thread().getName()
        times = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
        time.sleep(5)
        print(f'{times}-{self.n}-"当前活跃的线程个数:{_count}"-"当前线程的名称是":{threading_name}')


start_time = time.time()
print(f'{start_time},这是主线程:', threading.current_thread().name)
for i in range(5):
    t = MyThread(i)
    # t.setDaemon(True)
    t.start()
    t.join(2)
end_time = time.time()
print(f'{end_time},主线程结束了!', threading.current_thread().name)
print('一共用时:', end_time - start_time)

# 输出
1671175114.663927,这是主线程: MainThread
2022-12-16 15:18:34-0-"当前活跃的线程个数:2"-"当前线程的名称是":Thread-1
2022-12-16 15:18:36-1-"当前活跃的线程个数:3"-"当前线程的名称是":Thread-2
2022-12-16 15:18:38-2-"当前活跃的线程个数:4"-"当前线程的名称是":Thread-3
1671175124.6681008,主线程结束了! MainThread
一共用时: 10.004173755645752
2022-12-16 15:18:40-3-"当前活跃的线程个数:4"-"当前线程的名称是":Thread-4
2022-12-16 15:18:42-4-"当前活跃的线程个数:4"-"当前线程的名称是":Thread-5

Process finished with exit code 0

线程锁

# -*- coding: utf-8 -*-
import datetime
import threading
import time

number = 0


def add():
    global number  # global声明此处的number是外面的全局变量number
    for _ in range(10000000):  # 进行一个大数级别的循环加一运算
        number += 1
    times = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
    print(f'{times}-"当前活跃的线程个数:{threading.active_count()}"')
    print("子线程%s运算结束后,number = %s" % (threading.current_thread().getName(), number))
    print('------------------------------')


for i in range(2):  # 用2个子线程,就可以观察到脏数据
    t = threading.Thread(target=add)
    t.start()

time.sleep(2)  # 等待2秒,确保2个子线程都已经结束运算。

print("主线程执行完毕后,number = ", number)


# 输出
2022-12-20 13:13:05-"当前活跃的线程个数:3"
子线程Thread-1运算结束后,number = 11966305
------------------------------
2022-12-20 13:13:05-"当前活跃的线程个数:2"
子线程Thread-2运算结束后,number = 12272268
------------------------------
主线程执行完毕后,number =  12272268

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

可以很明显地看出脏数据的情况。这是因为两个线程在运行过程中,CPU随机调度,你算一会我算一会,在没有对number进行保护的情况下,就发生了数据错误
注意此时两个线程是同时开启的。

若是使用了join()的方法

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

虽然结果是对的,但是这样的本质是把多线程变成了单线程,失去了多线程的意义。

互斥锁Lock

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

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

RLock可重入锁

用于防止访问共享资源时出现不必要的阻塞。如果共享资源在RLock中,那么可以安全地再次调用它。 RLocked资源可以被不同的线程重复访问,即使它在被不同的线程调用时仍然可以正常工作。

在同一个线程中,RLock.acquire()可以被多次调用,利用该特性,可以解决部分死锁问题。

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

在上面的程序中,两个线程同时尝试访问共享资源number,这里当一个线程当前正在访问共享资源number时,另一个线程将被阻止访问它。 当两个或多个线程试图访问相同的资源时,有效地阻止了彼此访问该资源,这就是所谓的死锁,因此上述程序没有生成任何输出。

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

这两种锁的主要区别是:RLock允许在同一线程中被多次acquire。而Lock却不允许这种情况。注意:如果使用RLock,那么acquire和release必须成对出现,即调用了n次acquire,必须调用n次的release才能真正释放所占用的锁

Semaphore信号

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

可以看出用Semaphore来控制后,使得同一个时刻只有两个线程在请求页面
虽然当前活跃的子线程个数很多,但真正运行的子线程个数只有两个。

事件Event

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

  • set():将Flag设置为True
  • wait():等待
  • clear():将Flag设置为False
  • is_set():返回bool值,判断Flag是否为True
__init__(self, group=None, target=None, name=None, args=(), kwargs=None, *,daemon=None)
5

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

condition条件变量

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

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

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

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

Python提供的Condition对象提供了对复杂线程同步问题的支持。Condition被称为条件变量,除了提供与Lock类似的 acquire和release方法外,还提供了wait和notify方法。线程首先acquire一个条件变量,然后判断一些条件。如果条件不满足则 wait;如果条件满足,进行一些处理改变条件后,通过notify方法通知其他线程,其他处于wait状态的线程接到通知后会重新判断条件。不断的重复 这一过程,从而解决复杂的同步问题。

可以认为Condition对象维护了一个锁(Lock/RLock)和一个waiting池。线程通过acquire获得Condition对 象,当调用wait方法时,线程会释放Condition内部的锁并进入blocked状态,同时在waiting池中记录这个线程。当调用notify 方法时,Condition对象会从waiting池中挑选一个线程,通知其调用acquire方法尝试取到锁。

Condition对象的构造函数可以接受一个Lock/RLock对象作为参数,如果没有指定,则Condition对象会在内部自行创建一个RLock。

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

我们可以根据所谓的wait池构建一个带有缓冲区的生产者-消费者模型,即缓冲区好比一个火锅,生产者可以不断生产鱼丸知道火锅装满,然后告知消费者消费,而消费者也可以判断火锅是否空了从而告知生产者继续生产鱼丸:

定时器Timer

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

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

cancel()函数,可以在定时器被触发前,取消这个Timer。

通过with语句使用线程锁

Python Threading中的Lock模块有acquire()和release()两种方法,这两种方法与with语句的搭配相当于,进入with语句块时候会先执行acquire()方法,语句块结束后会执行release方法。

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

与之对应的有

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

线程池ThreadPoolExecutor

# -*- coding: utf-8 -*-
import datetime
import time
import threading


class MyThread(threading.Thread):

    def __init__(self, n):
        self.n = n
        # 注意:一定要调用父类的初始化函数
        super().__init__()

    def run(self) -> None:
        while True:
            times = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
            print(f'{times}-{self.n}')
            time.sleep(self.n)


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

# 输出
2022-12-16 12:43:24-1
2022-12-16 12:43:24-2
2022-12-16 12:43:25-1
2022-12-16 12:43:26-2
2022-12-16 12:43:26-1
2022-12-16 12:43:27-1
2022-12-16 12:43:28-2
...
0
  • max_workers参数用来控制线程池中运行的最大线程数
  • 通过submit方法将任务提交到线程池中,一次只能提交一个.submit会立即返回结果,第一个参数是函数名,第二个参数是函数的参数,他是一个元组
  • submit方法返回是一个future对象
  • as_completed函数会将运行完的任务一个一个yield出来,它返回任务的结果与提交任务的顺序无关,谁先执行完返回谁

或者

# -*- coding: utf-8 -*-
import datetime
import time
import threading


class MyThread(threading.Thread):

    def __init__(self, n):
        self.n = n
        # 注意:一定要调用父类的初始化函数
        super().__init__()

    def run(self) -> None:
        while True:
            times = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
            print(f'{times}-{self.n}')
            time.sleep(self.n)


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

# 输出
2022-12-16 12:43:24-1
2022-12-16 12:43:24-2
2022-12-16 12:43:25-1
2022-12-16 12:43:26-2
2022-12-16 12:43:26-1
2022-12-16 12:43:27-1
2022-12-16 12:43:28-2
...
1
  • 使用map方法,无需提前使用submit方法,map方法与python标准库中的map含义相同,都是将序列中的每个元素都执行同一个函数
  • map方法会将任务运行的结果yield出来
  • map方法返回任务结果的顺序与提交任务的顺序一致
阅读剩余
THE END