python 多线程详解
Posted 奈非天
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了python 多线程详解相关的知识,希望对你有一定的参考价值。
概念
线程是处理器调度和分配的基本单位,进程则作为资源拥有的基本单位。每个进程是由私有的虚拟地址空间、代码、数据和其它各种系统资源组成。线程是进程内部的一个执行单元。每一个进程至少有一个主执行线程,它无需由用户去主动创建,是由系统自动创建的。 用户根据需要在应用程序中创建其它线程,多个线程并发地运行于同一个进程中。
创建线程的方式-threading
方法1
在实例化一个线程对象时,将要执行的任务函数以参数的形式传入threading
创建两个线程,一个线程每隔一秒打印一个“1”,另一个线程每隔2秒打印一个“2”
Thread 类提供了如下的 init() 构造器,可以用来创建线程:
此构造方法中,以上所有参数都是可选参数,即可以使用,也可以忽略。其中各个参数的含义如下:
-
group:指定所创建的线程隶属于哪个线程组(此参数尚未实现,无需调用);
-
target:指定所创建的线程要调度的目标方法(最常用);
-
args:以元组的方式,为 target 指定的方法传递参数;
-
kwargs:以字典的方式,为 target 指定的方法传递参数;
-
daemon:指定所创建的线程是否为后代线程。
这些参数,初学者只需记住 target、args、kwargs 这 3 个参数的功能即可。
但是线程需要手动启动才能运行,threading 模块提供了 start() 方法用来启动线程。因此在上面程序的基础上,添加如下语句:t.start()
方法2
通过继承 Thread 类,我们可以自定义一个线程类,从而实例化该类对象,获得子线程。
需要注意的是,在创建 Thread 类的子类时,必须重写从父类继承得到的 run() 方法。因为该方法即为要创建的子线程执行的方法,其功能如同第一种创建方法中的 printNumber() 自定义函数。
主线程和子线程
注意: 第一次t.start()后,当前存在两个线程(主线程+子线程),第二次t.start()的时候又创建了一个子线程所以当前存在三个线程
如果程序中不显式创建任何线程,则所有程序的执行,都将由主线程 MainThread 完成,程序就只能按照顺序依次执行。
此程序中,子线程 Thread-1和Thread-2 执行的是 run() 方法中的代码,而 MainThread 执行的是主程序中的代码,它们以快速轮换 CPU 的方式在执行。
守护线程(Daemon Thread)
守护线程(Daemon Thread)也叫后台进程,它的目的是为其他线程提供服务。如果其他线程被杀死了,那么守护线程也就没有了存在的必要。因此守护线程会随着非守护线程的消亡而消亡。Thread类中,子线程被创建时默认是非守护线程,我们可以通过setDaemon(True)将一个子线程设置为守护线程。
将两个子线程改写为守护线程,因为当主程序中的代码执行完后,主线程就可以结束了,这时候被设定为守护线程的两个子线程会被杀死,然后主线程结束。
注意,当前台线程死亡后,Python 解释器会通知后台线程死亡,但是从它接收指令到做出响应需要一定的时间。如果要将某个线程设置为后台线程,则必须在该线程启动之前进行设置。也就是说,将 daemon 属性设为 True,必须在 start() 方法调用之前进行,否则会引发 RuntimeError 异常。
若将两个子线程的其中一个设置为守护线程,另一个设置为非守护线程
此时非守护线程作为前台程序还在继续执行,守护线程就还有“守护”的意义,就会继续执行。
join()方法
不使用join方法:
当设置多个线程时,在一般情况下(无守护线程,setDeamon=False),多个线程同时启动,主线程执行完,会等待其他子线程执行完,程序才会退出。
我们的计时是对主线程计时,主线程结束,计时随之结束,打印出主线程的用时。
主线程的任务完成之后,主线程随之结束,子线程继续执行自己的任务,直到全部的子线程的任务全部结束,程序结束。
使用join()方法:
主线程任务结束之后,进入阻塞状态,一直等待调用join方法的子线程执行结束之后,主线程才会终止。下面的例子是让t调用join()方法。
join()方法的timeout参数
join的语法结构为join(timeout=None)
,可以看到join()方法有一个timeout参数,其默认值为None,而参数timeout可以进行赋值,其含义是指定等待被join的线程的时间最长为timeout秒,也就是说当在timeout秒内被join的线程还没有执行结束的话,就不再进行等待了。
线程锁
这里创建两个子线程操作同一个全局变量number,number被初始化为0,两个子线程通过for循环对这个number进行+1,每个子线程循环10000000次,两个子线程同时进行。如果一切正常的话,最终这个number会变成20000000,然而现实并非如此。
可以很明显地看出脏数据的情况。这是因为两个线程在运行过程中,CPU随机调度,你算一会我算一会,在没有对number进行保护的情况下,就发生了数据错误
注意此时两个线程是同时开启的。
若是使用了join()的方法
虽然结果是对的,但是这样的本质是把多线程变成了单线程,失去了多线程的意义。
互斥锁Lock
互斥锁是一种独占锁,同一时刻只有一个线程可以访问共享的数据。使用很简单,初始化锁对象,然后将锁当做参数传递给任务函数,在任务中加锁,使用后释放锁。
RLock可重入锁
用于防止访问共享资源时出现不必要的阻塞。如果共享资源在RLock中,那么可以安全地再次调用它。 RLocked资源可以被不同的线程重复访问,即使它在被不同的线程调用时仍然可以正常工作。
在同一个线程中,RLock.acquire()可以被多次调用,利用该特性,可以解决部分死锁问题。
在上面的程序中,两个线程同时尝试访问共享资源number,这里当一个线程当前正在访问共享资源number时,另一个线程将被阻止访问它。 当两个或多个线程试图访问相同的资源时,有效地阻止了彼此访问该资源,这就是所谓的死锁,因此上述程序没有生成任何输出。
这两种锁的主要区别是:RLock允许在同一线程中被多次acquire。而Lock却不允许这种情况。注意:如果使用RLock,那么acquire和release必须成对出现,即调用了n次acquire,必须调用n次的release才能真正释放所占用的锁
Semaphore信号
可以看出用Semaphore来控制后,使得同一个时刻只有两个线程在请求页面
虽然当前活跃的子线程个数很多,但真正运行的子线程个数只有两个。
事件Event
Event类会在全局定义一个Flag,当Flag=False时,调用wait()方法会阻塞所有线程;而当Flag=True时,调用wait()方法不再阻塞。形象的比喻就是“红绿灯”:在红灯时阻塞所有线程,而在在绿灯的时候,一次性放行所有排队中的线程。Event类有四个方法:
-
set():将Flag设置为True
-
wait():等待
-
clear():将Flag设置为False
-
is_set():返回bool值,判断Flag是否为True
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。
我们可以根据所谓的wait池构建一个带有缓冲区的生产者-消费者模型,即缓冲区好比一个火锅,生产者可以不断生产鱼丸知道火锅装满,然后告知消费者消费,而消费者也可以判断火锅是否空了从而告知生产者继续生产鱼丸:
定时器Timer
通过threading.Timer类可以实现n秒后执行某操作。注意一个timer对象相当于一个新的子线程。
cancel()
函数,可以在定时器被触发前,取消这个Timer。
通过with语句使用线程锁
Python Threading中的Lock模块有acquire()和release()两种方法,这两种方法与with语句的搭配相当于,进入with语句块时候会先执行acquire()方法,语句块结束后会执行release方法。
与之对应的有
线程池ThreadPoolExecutor
-
max_workers参数用来控制线程池中运行的最大线程数
-
通过submit方法将任务提交到线程池中,一次只能提交一个.submit会立即返回结果,第一个参数是函数名,第二个参数是函数的参数,他是一个元组
-
submit方法返回是一个future对象
-
as_completed函数会将运行完的任务一个一个yield出来,它返回任务的结果与提交任务的顺序无关,谁先执行完返回谁
或者
-
使用map方法,无需提前使用submit方法,map方法与python标准库中的map含义相同,都是将序列中的每个元素都执行同一个函数
-
map方法会将任务运行的结果yield出来
-
map方法返回任务结果的顺序与提交任务的顺序一致
本文来自博客园,作者:奈非天,转载请注明原文链接:https://www.cnblogs.com/Nephalem-262667641/p/17332736.html
详解python中的多线程
文章目录
本文会直接举几个例子来看看python怎么实现多线程。之前在c++的 一篇文章中,对多线程的概念进行了介绍,这里就不继续展开讲解了,有兴趣可以点击链接进行了解。
Python3 通过两个标准库 _thread 和 threading 提供对线程的支持,由于_thread只是为了兼容python2的thread模块,所以推荐使用threading模块实现多线程
。
1. threading的一些功能介绍
方法 | 说明 |
---|---|
threading.currentThread() | 返回当前的线程变量 |
threading.enumerate() | 返回一个包含正在运行的线程的list |
threading.activeCount() | 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果。 |
除了使用方法外,线程模块同样提供了Thread类来处理线程,Thread类提供了以下方法:
方法 | 说明 |
---|---|
run() | 用以表示线程活动的方法。 |
start() | 启动线程活动。 |
join([time]) | 等待至线程中止。中止的方式:正常退出或者抛出未处理的异常-或者是可选的超时发生 。 |
isAlive() | 返回线程是否活动的。 |
getName() | 返回线程名。第一个子线程名字为:Thread-1,主线程名字为:MainThread |
setName() | 设置线程名。 |
下列举例说明这些功能,如果看不明白,没关系,可以暂时先跳过,学习完所有内容之后再回头看。
import threading
import time
def thread_1(num):
for i in range(num):
#print(threading.currentThread()) #当前线程变量
time.sleep(0.5)
def thread_2(num):
for i in range(num):
#print(threading.currentThread()) #当前线程变量
time.sleep(0.5)
def main():
#args是函数对应的参数,以元组的形式存在
t_thread_1 = threading.Thread(target=thread_1, args=(5,))
t_thread_2 = threading.Thread(target=thread_2, args=(6,))
print("t_thread_1 isAlive(): ", t_thread_1.isAlive()) # 是否存活,False
t_thread_1.start()#启动
t_thread_2.start()#启动
print("threading.enumerate(): ", threading.enumerate()) #范围一个list
print("threading.activeCount(): ", threading.activeCount()) #当前3个,包括一个主线程
#打印某个线程的信息
print("t_thread_1 isAlive(): ",t_thread_1.isAlive())#是否存活,True
print("t_thread_1 getName(): ", t_thread_1.getName()) # 获取线程名字Thread-1
print("t_thread_2 getName(): ", t_thread_2.getName()) # 获取线程名字Thread-2
t_thread_1.join()
t_thread_2.join()
print("t_thread_1 isAlive(): ", t_thread_1.isAlive()) # 是否存活,False
print("主线程结束")
if __name__ == '__main__':
main()
2. 线程的实现——函数和类
(1)函数方式实现多线程
import threading
import time
def thread_1(num):
for i in range(num):
print("子线程1 :%d" % i)
time.sleep(0.5)
def thread_2(num):
for i in range(num):
print("子线程2 :%d" % i)
time.sleep(0.5)
def main():
#args是函数对应的参数,以元组的形式存在
t_thread_1 = threading.Thread(target=thread_1, args=(5,))
t_thread_2 = threading.Thread(target=thread_2, args=(6,))
t_thread_1.start()#启动
t_thread_2.start()#启动
if __name__ == '__main__':
main()
(2)类的方式实现多线程
步骤:
- 导入threading包
- 创建一个类继承
threading.Thread
类,重写run方法
- 创建类对象,调用start()方法启动线程
import threading
import time
class MyThread(threading.Thread):
def run(self): #重写
for i in range(3):
time.sleep(1)
#self.name:线程的名字从Thread-1开始
msg = "I'm "+self.name+' @ '+str(i)
print(msg)
def main():
for i in range(5):#创建5个线程
t = MyThread()
t.start()
if __name__ == '__main__':
main()
3. 守护线程与同步线程
(1)非守护线程——默认线程
非守护线程:当主线程执行完所有的程序后,这个时候主线程并不回退出,也就是不会销毁,直到所有的子线程完成
了各自的任务后才自动销毁。默认情况下,python产生的线程是非守护线程
。
当线程设置为setDaemon(False),则表示线程为非守护线程,默认为False,如果设置为True则为守护线程。
下列代码中,子线程设置了延迟,主线程会等待子线程结束。
import threading
import time
def my_thread():
time.sleep(2)#延迟2s
print('---子线程结束---')
def main():
t1 = threading.Thread(target=my_thread)
t1.start()
print('---主线程---结束')
if __name__ == '__main__':
main()
输出:
---主线程---结束
---子线程结束---
(2)守护线程
守护线程,也就是当主线程结束之后,子线程也会随之消亡
。建立守护线程只需要设置setDaemon(True)就可以。
以下列代码为例,因为子线程设置为守护线程,而且子线程中设置了延迟2s,所以主线程结束之后,不会运行子线程的print。
import threading
import time
def my_thread():
time.sleep(2)
print('---子线程结束---')
def main():
t1 = threading.Thread(target=my_thread)
t1.setDaemon(True)
t1.start()
print('---主线程---结束')
if __name__ == '__main__':
main()
输出:
---主线程---结束
(3)同步线程(join)
当主线程遇到同步线程的时候,即遇到join()的时候会等待(即主线程进入堵塞状态,无法继续前行)所有的同步线程执行完毕
,之后主线程才继续往下走。
import threading
import time
def my_thread_1():
for i in range(2):#运行2s
print('子线程1睡眠第秒'.format(i + 1))
time.sleep(1)
print('子线程1结束!!!')
def my_thread_2():
for i in range(5):#运行5s
print('子线程2睡眠第秒'.format(i + 1))
time.sleep(1)
print('子线程2结束!!!')
def main():
t1 = threading.Thread(target=my_thread_1)
t1.start()
t2 = threading.Thread(target=my_thread_2)
t2.start()
t1.join()#等待子线程1结束
t2.join()#等待子线程2结束
print('主线程结束!!!')
if __name__ == '__main__':
main()
输出:
子线程1睡眠第1秒
子线程2睡眠第1秒
子线程1睡眠第2秒
子线程2睡眠第2秒
子线程1结束!!!
子线程2睡眠第3秒
子线程2睡眠第4秒
子线程2睡眠第5秒
子线程2结束!!!
主线程结束!!!
(4)非守护线程 + 同步线程
一般情况
:当我们设置某个子线程为非守护线程并设置其为同步线程的时候,也就是上面所说的同步线程join()是一样的,主线程会等待所有的同步线程执行完毕再往下执行,因为默认情况下,创建的线程为非守护线程
。
第二种情况
:如果在join中添加了timeout参数后,主线程会等待子线程timeout的时间,然后继续往下执行
,主线程执行完后不会退出
,而是等待子线程完成任务后再退出。详情可以见下一节。
import threading
import time
def my_thread():
for i in range(2):
print('子线程1睡眠第秒'.format(i + 1))
time.sleep(1)
print('子线程结束!!!')
def main():
t1 = threading.Thread(target=my_thread)
t1.setDaemon(False)
t1.start()
t1.join()
#t1.join(timeout=1)#join设置timeout,单位秒
print('主线程结束!!!')
if __name__ == '__main__':
main()
输出:
子线程1睡眠第1秒
子线程1睡眠第2秒
子线程结束!!!
主线程结束!!!
(5)守护线程 + 同步线程join
一般情况
:如果是守护线程这种情况下,join不设置timeout的话,主线程会等待子线程完成任务再继续往下执行
。
第二种情况
:如果join设置了timeout参数的话,等待子线程的时间后,主线程继续往下进行
,主线程结束了自己的任务后,发现子线程还是个守护线程,但是这时候子线程还没完成自己的任务,而主线程也不会等待它,而是直接自动销毁
。
import threading
import time
def thread1():
for i in range(5):
print('子线程睡眠第秒'.format(i + 1))
time.sleep(1)
print('子线程结束!!!')
def main():
t1 = threading.Thread(target=thread1)
t1.setDaemon(True)
t1.start()
t1.join(timeout=1)#设置timeout,单位秒
print('主线程结束!!!')
if __name__ == '__main__':
main()
输出:
子线程睡眠第1秒
主线程结束!!!
4. 互斥锁
由于线程之间是进行随机调度
的,如果有多个线程同时操作一个对象,如果没有很好地保护该对象,会造成程序结果的不可预期,我们因此也称为“线程不安全”。为了防止上面情况的发生,就出现了互斥锁(Lock)。
使用 Thread 对象的 Lock 和 Rlock 可以实现简单的线程同步,这两个对象都有 acquire 方法和 release 方法,对于那些需要每次只允许一个线程操作的数据,可以将其操作放到 acquire 和 release 方法之间
。
import threading
import time
class myThread (threading.Thread):
def __init__(self, threadID, name, my_time):
threading.Thread.__init__(self)
self.threadID = threadID
self.name = name
self.my_time = my_time
def run(self):
print ("开启线程: " + self.name)
# 获取锁,用于线程同步
threadLock.acquire()
print_time(self.name, self.my_time, 5)#每个运行5次
# 释放锁,开启下一个线程
threadLock.release()
def print_time(threadName, my_time, counter):
while counter:
time.sleep(my_time)
print ("%s: %s" % (threadName, time.ctime(time.time())))
counter -= 1
threadLock = threading.Lock()
threads = []
# 创建新线程
thread1 = myThread(1, "Thread-1", 1)#1s打印一次
thread2 = myThread(2, "Thread-2", 2)#2s打印一次
# 开启新线程
thread1.start()
thread2.start()
# 添加线程到线程列表
threads.append(thread1)
threads.append(thread2)
# 等待所有线程完成
for t in threads:
t.join()
print ("退出主线程")
输出:
开启线程: Thread-1
开启线程: Thread-2
Thread-1: Wed Dec 29 14:41:14 2021
Thread-1: Wed Dec 29 14:41:15 2021
Thread-1: Wed Dec 29 14:41:16 2021
Thread-1: Wed Dec 29 14:41:17 2021
Thread-1: Wed Dec 29 14:41:18 2021
Thread-2: Wed Dec 29 14:41:20 2021
Thread-2: Wed Dec 29 14:41:22 2021
Thread-2: Wed Dec 29 14:41:24 2021
Thread-2: Wed Dec 29 14:41:26 2021
Thread-2: Wed Dec 29 14:41:28 2021
退出主线程
以上是关于python 多线程详解的主要内容,如果未能解决你的问题,请参考以下文章