目录
一、进程与线程的概念
1.进程
进程是系统中正在运行的一个应用程序;程序一旦运行就是进程。进程是资源分配的最小单位。(动态)
2.线程
它被包含在进程之中,是操作系统能够进行运算调度的最小单位。一个进程中可以并发多个线程,每条线程并行执行不同的任务,从而提高程序执行效率。
3.进程和线程的关系
一个进程可以有多个线程,但至少有一个线程; 一个线程只能属于一个进程。
线程是最小的执行单元; 进程是最小的资源管理单元。
4.任务执行方式
并行:同时运行,只有具备多cpu才能实现并行。
并发:是指资源有限的情况下,两者交替轮流使用资源,是伪并行,即看起来是同时运行。
多线程在单核cpu中并发执行,根据时间片切换线程。多线程在多核cpu中并行执行。
如:有n个任务(做饭、洗衣、整理衣物、拖地...)需要完成。
二、Python中的Threading模块
1.线程模块介绍
python3进程多线程编程的模块有_thread和threading:
①_thread 模块:只提供基本的线程和锁定支持,不能很好的控制线程的运行。(不建议使用)
②threading 模块:提供了更高级别、功能更全面的线程管理。threading是常用的模块。
2.Threading介绍
(1)方法和属性
current_thread() |
返回当前进程 |
active_count() |
返回当前活跃的线程数 |
get_ident() |
返回当前线程的线程标识符 |
enumerater() |
返图当前活动Thed对象列表 |
main_thread() |
返回主Tred对象 |
stack_size(size) | 返回线程栈大小;或为后续创建的线程设定栈大小为size |
返回线程栈大小;或为后续创建的线程设定 |
TIMEOUT_MAX |
Lock.acquire(), RLock.acquire(), Condition.wait()允许的最大超时时间 | |
(2)类方法
Timer |
用于在指定时间之后调用一个函数的情况 |
Event |
事件类,用于线程同步 |
Condition |
条件类,用于线程同步 |
Lock、RLock |
锁类,用于线程同步 |
Semaphore |
信号量类,用于线程同步 |
Barrier |
阻碍”类,用于线程同步 |
三、线程简单编写
1.流程
创建线程,启动线程,等待线程结束
t = threading.Thread(target=xxx,args=(,)) #创建
t.start() #启动
t.join #等待结束
2.创建线程
(1)方法一
直接实例化对象threading.Thread对象
t = threading.Thread(target=xxx,args=(,)) #创建
t.start() #启动
t.join #等待结束
(2)方法二
创建线程子类来实例化线程对象(必须重写run方法)
class MyThread(threading.Thread):
def _init_(self,arg):
threading.Thread._init_()
self.arg = arg
def run(self):
pass
如果不使用join()方法,主线程不会等待子线程执行完毕,而是继续往下执行它自己的代码。Python默认会等待最后一个线程执行完毕后才会退出
3.例子
用线程编写一个获取系统实时时间的程序
import threading
import time
def show_time():
while True:
print(f"\r{time.strftime('%Y-%m-%d %H:%M:%S')}",end='')
time.sleep(1)
def main()
t = threading.Thread(target=show_time())
t.start()
t.join
if _name_ == '_main_':
main()
四、守护线程
在脚本运行过程中有一个主线程,若在主线程中创建了子线程,子线程的结束方式:
主线程会等待子线程完成后再退出。(非守护线程)
主线程运行结束时子线程将随主线程一起结束,而不论是否运行完成。(守护线程)
方法一:t = thredding.Thread(target=xxx,args=(,)),deamon = True)
方法二:t.deamon = True
方法三:t.setDaemon(True)
设置守护线程必须在启动线程前完成
五、线程同步技术
1.线程锁
当多个线程对一个共享的变量进行读写操作时,为了保证运行结果的正确性,通常需要对线程之间进行同步。当然,同步会降低并发的程度。threading模块中定义了几种线程锁类,以实现线程同步:
类名 |
描述 |
Lock |
互斥锁 |
RLock |
可重入锁 |
Semaphore |
信号锁 |
Event |
事件锁 |
Condition |
条件锁 |
Barrier |
栅栏,也叫阻碍类 |
(1)互斥锁Lock
Lock互斥锁:为原始锁,在锁定时不属于任何一个线程。是一种独占锁,同一时刻只有一个线程可以访问共享的数据。
使用方法:
初始化锁对象(创建锁对象)
将锁当做参数传递给任务函数
在任务中加锁(acquire)
使用后释放锁(release)
(2)可重入锁RLock
可重入锁RLock:
在同一个线程中,RLock.acquire()可以被多次调用,最后也必须由获取它的线程来释放它。
acquire()和release() 对可以嵌套。利用该特性,可以解决部分死锁问题。
Lock与RLock的对比:
锁名 |
是否同一线程加锁或释放锁 |
是否可重复加锁 |
Lock |
否 |
否 |
RLock |
是 |
是 |
(3)信号锁Semaphore
信号锁Semaphore:
类名:BoundedSemaphore。
这种锁允许一定数量的线程同时更改数据,它不是互斥锁。比如地铁安检,排队人很多,工作人员只允许一定数量的人进入安检区,其它的人继续排队。
(4)事件锁Event
运行机制:
定义一个全局Flag,如果Flag为False,执行wait()方法时就会阻塞,如果Flag为True,线程不再阻塞。这种锁,类似交通红绿灯(默认是红灯),它属于在红灯的时候一次性阻挡所有线程,在绿灯的时候,一次性放行所有排队中的线程。
事件锁提供的方法:
方法名 |
说明 |
set() |
将Event对象内部的 信号标志设置 为真 |
clear() |
将Event对象内部的 信号标志设置为假 |
is_set() |
判断其内部信号标志的状态 |
wait() |
信号为假时阻塞线程,信号为真时或超时再唤醒 |
(5)条件锁Condition
条件锁的作用:
使用Condition对象可以在某些事件触发后才处理数据或执行特定的功能代码,可以用于不同线程之间的通信或通知,以实现更高级别的同步。
条件锁提供的方法:
方法名 |
说明 |
** ** |
acquire() |
加锁 |
|
release() |
解锁 |
|
wait([timeout]) |
调用wait()会释放****锁 , 被唤醒之后又重新获得锁 ,然后继续执行。 |
使用前,线程必须已获得锁 |
notify() |
从等待池挑选一个线程并通知,收到通知的线程将自动调用acquire()尝试获得锁定,其他线程仍然在等待池中。 |
调用该方法 不会释放锁 |
notifyAll() |
通知等待池中所有线程,这些线程都将进入锁定池尝试获得锁定。 |
(6)Barrier“阻碍”类
Barrier的作用:
多个线程运行到某个点以后每个线程都需要等着其他线程准备好以后再同时进行下一步工作,此时就可以用Barrier对象。
①主线程中创建阻碍锁
lock=threading.Barrier(parties,action=None,timeout=None)
parties:设置等待的线程数
action:如果设置,则表示当等待的线程达到了指定数量,在释放它们之前,将由其中一个线程调用action指定的操作。
timeout:设置所有线程默认的等待时间。
②子线程中用wait()等待被唤醒
lock.wait( timeout=None)
imeout——设置线程的等待时间
如果等待超时,会引发BrokenBarrierError错误。
返回值:返回一个介于0\~parties-1之间的整数。
线程调用该方法后会阻塞,当指定数量线程都调用了该方法后,先由其中一个线程执行action指定的操作(如果有),再同时释放这些线程并继续执行后面的代码。
(7)queue.Queue()对象
队列 Queue的作用:
一种先入先出的结构。queue对象适合需要在多个线程之间进行信息交换,实现了多线程编程所需要的所有锁语义。
①创建queue对象
q = queue.Queue(maxsize=0)
maxsize:设置队列大小
如果maxsize<= 0,队列大小是无限的。
②队列对象常用方法
方法 |
说明 |
put(item,block=True,timeout=None) |
在队列尾部追加元素 |
get(block=True,timeout=None) |
在队列头部取元素 |
Queue.qsize() |
返回队列的大小 |
Queue.empty() |
队列为空则返回True,否则返回False |
Queue.full() |
队列满则返回True,否则返回False |
Queue.join() |
队列为空时执行别的操作 |
(8)定时器Timer
定时器Timer类是threading模块中的一个小工具,用于指定n秒后执行某操作。
2.with语句使用线程锁
所有的线程锁都有一个加锁和释放锁的动作,非常类似文件的打开和关闭。通过with上下文管理器,可以确保锁被正常释放。其格式如下:
with some_ Lock:
#执行任务
六、多进程编程
1.全局解释器锁(GIL)
Python设计之初为了数据安全所做的决定。CPython中的某个线程想要执行,必须先拿到GIL,但在CPython进程中GIL只有一个。
2.全局解释器锁(GIL)导致的问题
无论CPU有多少核,由于只有一个GIL,导致同一时刻只能执行一个线程,此时多线程并不能大幅度的提供处理效率。Python中想要充分利用多核CPU,就用多进程。因为每个进程有各自独立的GIL。Python标准库multiprocessing支持使用类似于threading的用法来创建与管理进程。
建议:IO密集型任务中使用多线程,如socket,爬虫。计算密集型任务中使用多进程,如金融分析。
3.进程同步
与多线程一样,当多个进程对一个共享的变量进行读写操作时,为了保证结果的正确性,通常需要对进程之间进行同步。
4.进程间通信
进程间通常数据是独立的,不能共享数据。进程间通信据的方式: 共享内存、管道、信号、socket、外部存储等。
常用方式:
方式 |
类或函数 |
队列 |
multiprocessing.Queue() |
管道 |
multiprocessing.Pipe() |
七、并发编程方法比较
Python中执行并发任务有三种方式:
* 多进程
* 多线程
* 多协程
包含关系:一个进程中可启动多个线程,一个线程中可启动多个协程。
1.协程
协程:称为微线程,是比线程更轻量级。可在一个线程中启动多个协程进行函数执行切换来实现并发。
协程始终都能获得到GIL,所以不会受GIL影响。如果希望通过协程利用多核CPU的计算能力,那么可通过多进程与协程配合的模式来实现。
协程库:asyncio
2.多进程
优点:
* 可以利用多核CPU进行并行计算
* 子进程之间数据独立,安全性较好
缺点:
* 占用资源最多
* 进程间切换的开销也比较大
* 可以启动数目最少
使用场景:CPU密集型计算。
3.多线程
优点:
* 相比于进程更轻量级
* 占用资源更少
缺点:
* 受限于全局解释器锁GIL,在CPython中,多线程只能并发执行,无法充分利用多核处理器。
* 启动数目有限制,占用内存资源,有线程切换的开销。
适用场景:IO密集型计算
4.多协程
优点:
* 内存开销最小,可启动数目最多(可多达几万个)
缺点:
* 支持的库有限制(如requests库不支持,需用aiohttp); 代码实现复杂。
适用场景:IO密集型计算、需要启动超多任务、有现成的库支持。