线程:
1.线程简介
线程是cpu运行的最小单元。
共享内存之间可以并发执行多任务。
每一个线程都可以共享一个进程的资源。
每个进程中最少包含一个线程,如何调度进程和线程完全由操作系统来决定。
所有变量可以被线程共享。
线程之间共享数据最大的危险在于:如果有多个线程同时启动处理同一个变量,会导致数据紊乱。
2.创建线程
通过模块_thread(偏向底层的低级模块,操作起来比较难)
threading (对_thread模块进行封装 ,高级模块)
在任何一个进程启动的时候会默认启动一个线程,这个就是主线程。主线程中间可以启动子线程。
方式一:创建一个threading.Thread对象,在其初始化函数中将可调用对象作为参数传入:
import threading
def handle(sid):
print("Thread %d run"%sid)
#创建线程方法1
for i in range(1, 11):
t = threading.Thread(target=handle, args=(i,))
t.start()
print("main thread")
方式二:通过继承Thread类,重写它的run()方法
import threading
def handle(sid):
print("Thread %d run"%sid)
class MyThread(threading.Thread):
def __init__(self,sid):
threading.Thread.__init__(self)
self.sid = sid
def run(self):
handle(self.sid)
for i in range(1,11):
t = MyThread(i)
t.start()
print("main thread")
3.threading.Thread对象:
class threading.Thread(group=None, target=None, name=None, args=(), kwargs={}, *, daemon=None)
group :这个应该为空,是个保留位,是为了以后ThreadGroup(线程组)这个类实现而预留的
target:是个由run()方法执行调用的可调用对象,也就是线程要执行的函数,默认为空,意味着没有东西可以被调用
name: 这个参数表示线程的名称,默认情况下,是一个类似于Thread-N形式的名字,这里的N是小的十进制失踪
args: 这是个参数元组,是用来给target调用的,默认为一个空的元组
kwargs={} : 这是个关键字参数组成的字典,是用来给target调用的,默认为一个空的字典
daemon 这个bool值表明这个线程是否是守护进程,当为True时,就是个守护进程,否则为False,就不是守护进程
这个必须在start()之前被设置,否则就会抛出RuntimeError异常,它的初始值继承于创建它的线程,如果主进程不是守护进程,
那么由主进程创建的子进程默认都不是守护进程
一般用到的是 target=函数 name=“名字”,args=(元组,单个元素加逗号) args=(aa,)
4.start方法与run方法
启动线程是调用start方法,而不调用run方法,这里两者有什么区别呢?
首先线程的状态有五种:创建、就绪、运行、阻塞和死亡
当调用start函数,线程的状态就处于就绪状态,处于就绪状态后,还需要等待CPU分配时间片
如果分到时间片,就进入运行状态,执行run函数
start() --> 启动线程,这个函数只能在一个线程中调用一次,如果超过一次则会抛出RuntimeError异常,它安排run()方法在另一个线程中调用
run() --> 用以表示线程活动的方法,你可以在子线程中覆盖此方法
在自己构造线程类时候可以重写run方法,此时调用的是当前线程类的run
而不是父类身上的run
import threading
class MyThread(threading.Thread):
def __init__(self,sid):
threading.Thread.__init__(self)
self.sid = sid
def run(self):
currentTreadname = threading.currentThread()
print("running in", currentTreadname)
t = MyThread(1)
t.start()
t.run()
可以看到调用start方法是在创建的子线程中调用,而直接调用run方法则是在主线程中调用
5.线程方法
daemon 是否为守护线程 默认False 主线程根本不等你子线程
getName 获取线程名字
ident 线程的标识符(线程的id)
isAlive is_alive 是否为活动线程
isDaemon 是否为守护线程
name 线程名字
start 线程运行
join([timeout]) 阻塞主线程),timeout为最长等待时间
run 方法
6.锁 lock
使用公共资源的时候,可以上个锁,其他线程就不能使用了。解决所线程导致数据紊乱的问题。多线程同步时如果需要获得多个锁才能进入临界区的话,可能会发生死锁,在多线程编程时一定要注意并认真检查和避免这种情况。死锁可以理解为嵌套锁。
- acquire() 获得锁
- release() 释放锁
死锁
# lock.acquire() # 锁上了,底下的代码在等待它的释放;将lock 状态调为locked
# lock.acquire() # lock 阻塞等待 lock 被释放;发生死锁
# lock.release() # 这里根本释放不了
# lock.release() # 没有上锁,就不能释放。获取锁和释放锁要一一对应。RuntimeError
Rlock
重复锁,递归锁,可以锁很多次,但是解锁要一一对应
也是局部锁,在一个线程中可以实现重复获取锁,并且不需要释放锁,代码依旧执行(其他线程局部不调用此锁时),如果在多个线程中使用RLock,必须在线程局部获取锁和释放锁一一对应,否则其他线程会出现阻塞。
condition 条件锁
方法:
- wait():
wait方法会释放锁,并阻塞当前线程直到超时或其他线程针对同一个Condition对象调用了notify/notify_all方法,被唤醒之后当前线程会重新尝试获取锁并在成功获取锁之后结束wait方法,然后继续执行。 - wait_for(predicate=fun,timeout=None)方法
阻塞当前线程,直到超时或者制定条件得到满足
import time
print("主线程开始")
def fun():
time.sleep(2)
return True
con.wait_for(fun) # 阻塞,直到predicate 返回TRUE
print("主线程结束..")
- notify(n=1)
唤醒等待该Condition对象的一个或多个线程,该方法并不负责释放锁,n=1唤醒一个锁 - notify_all
会唤醒等待该Condition对象的所有线程
线程同步
信号量对象 semaphore(value)
Semaphore ,是一种带计数的线程同步机制,当调用release时,增加计数,当acquire时,减少计数,当计数为0的时候,自动阻塞,等待被释放。
sem = Semaphore(10) # 班级 信号量锁,指定锁的次数,只能锁到10次,保证只有10个线程在运行,其余的都在等待。
Event对象
Event是线程间通信最简单的机制之一:一个线程发送一个event信号,其他的线程则等待这个信号。用于主线程控制其他线程的执行。Event管理一个flag,这个flag可以使用set()设置成True或者使用clear()重置为False,wait()则用于阻塞,在flag为True之前。Event内置了一个初始化为False的标志(flag)。
# Event 直行红灯
from threading import Event,Thread
import time
event = Event()
event.clear() # 将Event 开关设置成False
"""
event.set() 将Event 开关设置成True
event.isSet() 查看开关是否为True
event.wait() 等待
"""
num = 4
class RGB(Thread):
def __init__(self):
super().__init__()
def run(self):
while True:
global num, flag
time.sleep(2)
num -= 1
print("倒计时:%s"%num)
if num == 0:
event.set()
time.sleep(2)
event.clear()
if num<=0:
num = 4
class Car(Thread):
def __init__(self):
super().__init__()
def run(self):
while True:
if event.isSet():
time.sleep(2)
print("%s:直行通过红绿灯"%self.name)
else:
event.wait()
t1 = RGB()
t1.start()
for i in range(5):
t = Car()
t.start()
- 方法
Event.wait(timeout) :堵塞线程,直到Event对象内部标识符被设为True或者超时
Event.set():将标识符设为Ture
Event.clear():将标识符设为False
Event.isSet():判断标识符是否为Ture
栅栏 Barrier
栅栏提供了一个简单的同步原语,用于应对固定数量的线程需要彼此相互等待的情况,线程调用wait()方法后将阻塞,直到所有线程都用了wait()方法,此时所有线程将被同时释放。
class threading.Barrier(parties,action=None,timeout=None)
parties 栅栏数
action=None 一个函数
timeout 超时数
方法
wait() 等待
parties 栅栏打、开所需线程的数量(属性)
n_waiting 当前正在栅栏中阻塞的线程
rest() 重置栅栏 # 避免死锁会用一下的
abort() 使栅栏破损,冲出栅栏所需要的线程数量
broken 一个布尔值,值为True表示栅栏受损
田径比赛
from threading import Thread,Barrier
import time,random
def fun():
print("<比赛开始>")
bar = Barrier(10,action=fun)
class Per(Thread): # 创建人,线程
def __init__(self):
super().__init__()
def run(self):
time.sleep(1)
print("%s已经准备好了"%self.name)
bar.wait()
time.sleep(random.randint(2,5))
print("%s跑完了全程"%self.name)
for i in range(10): # 19 前10个人已经跑完了,后边不够10个人,线程阻塞了
p = Per()
p.start()
with语法
Lock 、 RLock 、 Condition 、 Semaphore 和 BoundedSemaphore 对象可以用作 with 语句的上下文管理器。
进程
线程具体占用的时间是多少,优先级是多少,就得看进程了。
多线程适合IO密集型,每一个耗时比较长
跑起来的软件、程序 就是进程。
运算密集型用进程,CPU就是用来做运算的。
概述
进程是正在执行中的应用程序(编完编程就是系统里的一个进程)。一个进程是一个执行中的文件使用资源的总和,包括虚拟地址空间、代码、数据、对象句柄、环境变量和执行单元等。一个应用程序同时打开并执行多次,会创建多个进程。
python 标志库 multiprocessing 支持使用类似于threading 的用法来创建于管理进程,并且避免了GIL(Global Interpreter Lock)问题,可以有效利用CPU。
总结:对于操作系统而言,一个任务就是一个进程,是系统中程序执行和资源分配的基本单元。每个进程都有自己的数据段,堆栈段。
全局变量在多个进程中不能共享,子进程和主进程和兄弟进程操作的不是同一个变量。在操作的时候,每一个进程都会引用一个新的全局变量。或者说,在创建子进程时,自动备份了一个去全局变量。
父进程不能影响结束子进程。
可以用进程池来管理进程。
全局解释锁,解释器的锁。GIL(Global Interpreter Lock)
历史原因:
硬件厂商,CPU的速度技术上解决不了。做了一个多核,开始多核运算。核是处理线程的,4个核就可以运行4个线程。但是核多了,会有弊端,这4个核同时运行的期间,数据是不共享的。要解决数据共享的问题,就得上锁。
硬件厂商已经想好了数据进行共享的问题,而python也想解决这个问题,因此全局解释锁就是使python自己的线程可以进行数据共享。
每一版本python都在解决这个问题。
每一个核里都有GIL,只有一个能获取锁,只是占用了内存,没有起作用。
全局解释锁非常不高效,直接取消的话,之前很多包都不能用了。只能慢慢想对策去改,怎么样改就是开发的问题。
python是一个开源的,没有人维护,全部是靠赞助,效率不高。
一个解释器有一个GIL,开两个解释器就有两个锁。
采用多进程的方法解决
创建进程(两种类似线程)
一种是通过构造函数继承的方式,
一种直接通过mulitprocessing实例化一个进程对象。
僵尸进程
守护进程的时候,主进程不考虑子进程有没有完
子进程不考虑主进程
上下文和启动进程方法
- spawn
父进程启动一个新的python解释器进程。子进程将只继承运行流程对象run() 方法所需的资源。特别是,来自父进程的不必要的文件描述符合句柄不会被继承。与使用fork或forkserver相比,使用这种方法启动进程要慢得多。
可在Unix和Windows上使用,Windows上的默认值 - fork(复制)
父进程使用os.fork() 派生python解释器。子进程开始时实际上与父进程相同。父进程的所有资源都由子进程继承。注意,安全地分叉多线程进程是有问题的。
只能在Unix是上使用,Unix上的默认值。 - forkserver
当程序启动并选择forkserver start方法时,将启动一个服务器进程。从那时起,每当需要一个新进程时,父进程就连接到服务器,并请求它派生一个新进程,fork服务器进程是单线程的,因此使用os.fork() 是安全的,没有继承不必要的资源。 - 创建方法 在windows里只有spawn这种方式
multiprocessing
multiprocessing.active_children() 返回主进程中所有子进程
multiprocessing.cpu_count() 返回系统中的CPU数量
multiprocessing.current_process() 返回系统当前数量
multiprocessing.get_all_start_methods() 返回支持的start方法列表,第一个是默认方法
multiprocessing.get_context(method=None) 获得上下文 如果method=None,则返回默认的上下文 method = fork,spawn,forkserver
multiprocessing.get_start_method() 返回用于启动进程的start方法的名称
进程对象方法
run()
start() 运行的时候运行run文件
join()
name
is_alive()
daemon
pid 返回进程ID
authkey 进程的身份验证密钥
exitcode 进程终止码,没有终止则为None
terminate() 立刻终止进程运行,极易产生僵尸进程:主进程已经结束了,子进程还在运行。
kill() 使用SIGKILL信号相同
close() 关闭
数据交换
- Queue
- Pipe
进程同步
- Manager 对象
控制一个拥有list,dict,Lock,Semaphore,BoundedSemaphore,Queue,Event,Barrier,Value,Array。
进程池
利用pool对象进行数据并行处理
创建进程池
from multiprocessing import Pool
p = Pool(num) num为进程池中进进程的数量
常用方法
- apply(func[,args,kwds])
调用func函数,并传递参数args和kwds,同时阻塞当前进程直至函数返回 - apply_async(func,callback,error_callback)
返回AsyncResult对象
get()获得返回值
wait()等到结果可用
ready()返回调用是否完成
succful()返回结果是否在没有引发异常的情况下完成 - map(func,iterable)
内置函数map()模块的并行版本,但只能接受一个可迭代对象作为参数,该方法会阻塞当前进程直至结果可用。该方法会把迭代对象iterable切分成多个块再作为独立的任务提交给进程池,块的大小可通过参数chunksize(默认值为1)来设置。 - close()
不允许再次向进程池提交任务,当所有已提交的任务完成后工作进程会退出 - terminate()
立即结束工作进程,当进程池对象被回收时会自动调用该方法 - join()
等待工作进程退出,在此之前必须先调用close()或terminate()