1、基本概念
1.1、线程
线程:是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。一个线程是一个cpu执行时所需要的一串指令。
线程的工作方式:CPU会给你一个在同一时间能够做多个运算的幻觉,实际上它在每个运算上只花了极少的时间,本质上CPU同一时刻只干了一件事,但是多个运算共同使用一块CPU,因为CPU可以存储执行每个运算的执行内容,再次执行时可以继续。
1.2、进程
进程:一个程序的执行实例就是一个进程。每一个进程提供执行程序所需的所有资源。(进程本质上是资源的集合)。
一个进程有一个虚拟的地址空间、可执行的代码、操作系统的接口、安全的上下文(记录启动该进程的用户和权限等等)、唯一的进程ID、环境变量、优先级类、最小和最大的工作空间(内存空间),还要有至少一个线程。每一个进程启动时都会最先产生一个线程,即主线程。然后主线程会再创建其他的子线程。
进程资源有:内存页(同一个进程中的所有线程共享同一个内存空间),文件描述符(e.g. open sockets),安全凭证(e.g.启动该进程的用户ID)。
1.3、线程和进程的区别
1.同一个进程中的线程共享同一内存空间,但是进程之间是独立的。
2.同一个进程中的所有线程的数据是共享的(进程通讯),进程之间的数据是独立的。
3.对主线程的修改可能会影响其他线程的行为,但是父进程的修改(除了删除以外)不会影响其他子进程。
4.线程是一个上下文的执行指令,而进程则是与运算相关的一簇资源。
5.同一个进程的线程之间可以直接通信,但是进程之间的交流需要借助中间代理来实现。
6.创建新的线程很容易,但是创建新的进程需要对父进程做一次复制。
7.一个线程可以操作同一进程的其他线程,但是进程只能操作其子进程。
8.线程启动速度快,进程启动速度慢(但是两者运行速度没有可比性)。
2、python多线程(threading类)
2.1、GIL
在非python环境中,单核情况下,同时只能有一个任务执行。多核时可以支持多个线程同时执行。但是在python中,无论有多少核,同时只能执行一个线程。究其原因,这就是由于GIL的存在导致的。
GIL:全称是Global Interpreter Lock(全局解释器锁),某个线程想要执行,必须先拿到GIL,并且在一个python进程中,GIL只有一个。拿不到通行证的线程,就不允许进入CPU执行。GIL只在cpython中才有,因为cpython调用的是c语言的原生线程,所以他不能直接操作cpu,只能利用GIL保证同一时间只能有一个线程拿到数据。而在pypy和jpython中是没有GIL的。
2.1、Python多线程的工作过程
拿到公共数据,申请GIL,python解释器调用系统原生线程,系统操作cpu执行运算,当该线程执行时间到后,无论运算是否已经执行完,系统都被要求释放,进而由其他线程重复上面的过程,等其他线程执行完后,又会切换到之前的线程(从他记录的上下文继续执行),整个过程是每个线程执行自己的运算,当执行时间到就进行切换(context switch)。
注意:python想要充分利用多核CPU,就用多进程。因为每个进程有各自独立的GIL,互不干扰,这样就可以真正意义上的并行执行,在python中,多进程的执行效率优于多线程(仅仅针对多核CPU而言)。
2.2、线程锁
线程锁:防止出现脏数据,线程锁,即同一时刻允许一个线程执行操作。线程锁用于锁定资源,你可以定义多个锁, 当你需要独占某一资源时,任何一个锁都可以锁这个资源,就好比你用不同的锁都可以把相同的一个门锁住是一个道理。
互斥锁(Lock):互斥锁同时只允许一个线程更改数据。
递归锁(RLcok):RLcok类的用法和Lock类一模一样,但它支持嵌套,,在多个锁没有释放的时候一般会使用使用RLcok类。
信号量(BoundedSemaphore类):,Semaphore是同时允许一定数量的线程更改数据 。
2.3、事件,条件,定时器
事件(Event类):python线程的事件用于主线程控制其他线程的执行。
条件(Condition类):使得线程等待,只有满足某条件时,才释放n个线程。
定时器(Timer类):定时器,指定n秒后执行某操作。
3、python多进程(multiprocessing类)
多进程:在linux中,每个进程都是由父进程提供的。每启动一个子进程就从父进程克隆一份数据,但是进程之间的数据本身是不能共享的。
3.1进程通信
由于进程之间数据是不共享的,所以不会出现多线程GIL带来的问题。多进程之间的通信通过Queue()或Pipe()来实现。
3.1.1 Queue()
进程加入队列
3.1.2Pipe()
Pipe的本质是进程之间的数据传递,而不是数据共享,这和socket有点像。pipe()返回两个连接对象分别表示管道的两端,每端都有send()和recv()方法。如果两个进程试图在同一时间的同一端进行读取和写入那么,这可能会损坏管道中的数据。
3.2、Manager
通过Manager可实现进程间数据的共享。Manager()返回的manager对象会通过一个服务进程,来使其他进程通过代理的方式操作python对象。
3.3、进程锁(进程同步)
数据输出的时候保证不同进程的输出内容在同一块屏幕正常显示,防止数据乱序的情况。
3.4、进程池
由于进程启动的开销比较大,使用多进程的时候会导致大量内存空间被消耗。为了防止这种情况发生可以使用进程池,(由于启动线程的开销比较小,所以不需要线程池这种概念,多线程只会频繁得切换cpu导致系统变慢,并不会占用过多的内存空间)
进程池中常用方法:apply() 同步执行(串行),apply_async() 异步执行(并行),terminate() 立刻关闭进程池,join() 主进程等待所有子进程执行完毕。必须在close或terminate()之后,close() 等待所有进程结束后,才关闭进程池。
4、协程(greenlet)
线程和进程的操作是由程序触发系统接口,最后的执行者是系统,它本质上是操作系统提供的功能。
协程:而协程的操作则是程序员指定的,在python中通过yield,人为的实现并发处理。
协程存在的意义:对于多线程应用,CPU通过切片的方式来切换线程间的执行,线程切换时需要耗时。协程,则只使用一个线程,分解一个线程成为多个“微线程”,在一个线程中规定某个代码块的执行顺序。
协程的适用场景:当程序中存在大量不需要CPU的操作时(IO)。常用第三方模块gevent和greenlet。(本质上,gevent是对greenlet的高级封装,因此一般用它就行,这是一个相当高效的模块。)
实际上,greenlet就是通过switch方法在不同的任务之间进行切换。
import multiprocessing as mp
def worker_1():
algorithm1.run(9200)
solutions1 = algorithm1.result
return solutions1
def worker_2():
algorithm2.run(10000)
solutions2 = algorithm2.result
return solutions2
def worker_3():
algorithm3.run(10000)
solutions3 = algorithm3.result
return solutions3
fun_list = [worker_1,worker_2,worker_3]
def multicore():
pool=mp.Pool(processes=3)#定义一个Pool,默认为CPU核心数,可以自己定义多少个定义容量为3的进程池,最大三个子进程
multi_res = []
for fun in fun_list:
res=pool.apply_async(fun) # apply_async向进程池提交目标请求
multi_res.append(res)#多个结果
results_t = [res.get()for res in multi_res]
solutions1 = results_t[0]
solutions2 = results_t[1]
solutions3 = results_t[2]
return solutions1,solutions2,solutions3
if __name__=='__main__':
solutions1, solutions2, solutions3 = multicore()