记录:
并发:在一个时间段,处理多个任务,单核也可以并发(CPU分时间片);
并行:在同一个时刻,处理多个任务,必须多核才能并行;
一、Python实现并发的手段:
1、操作系统提供:进程、线程;
2、编程语言提供:协程:用户空间的调度(py3);
题外话:
现在的操作系统,进程和线程的区别越来越小,因为进程越来越轻了;实际上,Linux的线程是通过进程实现的;
二、Python的进程和线程的区别:
Python每个进程都会启动一个解释器;
Python每个线程(一个进程下面的)共享一个解释器;
ps:Python没有提供主动停止线程的方法的;只能等线程处理完毕,或者主线程结束;所以在线程逻辑里面一定要写退出逻辑;
三、logging库:
通常用logging代替print,因为logging是线程安全的,在多线程下,输出表现正常,而print是非线程安全的,在多线程下,输出表现会出现异常,例如,T1没输出完毕,T2又进行输出了;
四、Python 线程的 daemon 属性:
默认是false;
daemon:守护、守护进程;
守护进程:Daemon()程序是一直运行的服务端程序,又称为守护进程。通常在系统后台运行,没有控制终端,不与前台交互,Daemon程序一般作为系统服务使用。Daemon是长时间运行的进程,通常在系统启动后就运行,在系统关闭时才结束。
知识点一:
当一个进程启动之后,会默认产生一个主线程,因为线程是程序执行流的最小单元,当设置多线程时,主线程会创建多个子线程,在python中,默认情况下(其实就是setDaemon(False)),主线程执行完自己的任务以后,就退出了,此时子线程会继续执行自己的任务,直到自己的任务结束;
知识点二:
当我们使用setDaemon(True)方法,设置子线程为守护线程时,主线程一旦执行结束,则全部线程全部被终止执行,可能出现的情况就是,子线程的任务还没有完全执行结束,就被迫停止;
知识点三:
join所完成的工作就是线程同步,即主线程任务结束之后,进入阻塞(等待)状态,一直等待其他的子线程执行结束之后,主线程在终止;
知识点四:
join有一个timeout参数:
当设置守护线程时,含义是主线程对于子线程等待timeout的时间将会杀死该子线程,最后退出程序。所以说,如果有10个子线程,全部的等待时间就是每个timeout的累加和。简单的来说,就是给每个子线程一个timeout的时间,让他去执行,时间一到,不管任务有没有完成,直接杀死。
没有设置守护线程时,主线程将会等待timeout的累加和这样的一段时间,时间一到,主线程结束,但是并没有杀死子线程,子线程依然可以继续执行,直到子线程全部结束,程序退出。
t1.join(),子线程设置这个就是让主线程阻塞(等待),那么在哪里设置了 t1.join(),主线程就在这里等待t1执行完毕,再继续往下执行;因为两个线程顺序完成,看起来象一个线程,所以称为线程的合并,其实也是让线程同步了;
注意(看到的结果是这样,解释可能不太正确):
(1)看图:
t1设置执行2秒;t2设置执行3秒;目的是为了t1先于t2执行完毕;
t1没有设置daemon线程,所以当主线程结束后,t1仍然会继续执行完毕,所以查看结果:当输出 main end ... 后,仍然有 t1 out ... 输出;
t2设置为daemon线程,所以当主线程结束后,t2也被主线程杀死了(t1已结执行完毕),跟着结束了,程序(进程)也结束了,所以没有 t2 out... 输出;
注意,这里一定要保证t1(非daemon子线程)先于t2执行完毕;因为如果t1要比t2耗时长的话,因为 t1是非daemon子线程,即是主线程结束了,t1仍然要继续执行的,所以整个程序(进程)并不会结束,也就没有杀死t2,导致t2也会执行完毕;看下面:
可以看到:即是t2是daemon子线程,但是仍然执行完毕了,并没有因为主线程的退出而被杀死,因为t1的执行时间比t2长;
ps:如果不是以继承的方式创建线程,那么run方法和start方法只能执行其中一个;通常也不建议使用继承的方式;start方法是子线程执行的,启动子线程;
要注意:主线程、父线程、子线程的关系;
五、Python中的ThreadLocal变量
注意,并不是子线程要执行的目标函数里面的局部变量,虽然每个线程即使是执行同一个函数,都会有自己的内存的,互不干扰,但是函数里面的局部变量,就是属于函数的,到了另外一个函数不会认识此局部变量;
而Python的threadlocal变量,是属于线程的,此线程调用的所有函数都认识threadlocal变量,eg.:
global_data = threading.local()
def show():
print (threading.current_thread().getName(), global_data.num)
def thread_cal():
global_data.num = 0
for _ in xrange(1000):
global_data.num += 1
show()
每个线程都可以通过 global_data.num 获得自己独有的数据,并且每个线程读取到的 global_data 都不同,真正做到线程之间的隔离。其他线程是不认识global_data.num的;
六、线程同步方式(前三个方式需要在具体了解)
线程的同步,有一层意思就是:线程之间的通讯,线程总是有某种关联,才需要同步的,而同步就意味着阻塞(等待);
1、event
两个线程之间事件的通知,我发现了一个事件,set,你就可以查看。
2、lock (rlock?)
保护共享资源;
3、condition
生产者消费者模式;生产者唤醒消费者;
4、Barrier(栅栏)
就是线程们在等待,直到线程数满足设定的数量之和,才继续执行;
可以使用此方法开发并发测试工具;
一些关键语句:
barrier = threading.Barrier(3) #等齐3个线程
worker_id = barrier.wait() #线程执行到这一句,就等着,满足数量之后,再往下走
barrier.n_waiting #现在在等待的线程数量
barrier.abort() #通知已经在等待的线程,不必再等了,我执行不到wait()了(任何一个线程执行都可以),当abort()方法被执行,wait()方法就会抛出异常
5、semaphore (信号量)
s = threading.Semaphore(2)
s.acquire() # 获得,在线程中,也是锁住某个变量,自己此刻独占;
输出:True # 成功锁住
s.acquire(False) # 再来一次
输出:True # 成功锁住
s.acquire(False) # 再来一次
输出:False # 没有锁
锁是信号量的特例:为1的信号量;
信号量也是对共享资源的保护,但是和锁不一样,锁限制只有一个线程可以访问共享资源,而信号量限制指定个线程可以访问共享资源;所以信号量可以用于做连接池的功能;
七、异步编程
1、概念
同步、异步:
发生在函数调用的时候,是否得到直接最终结果;
得到直接最终结果的是:同步调用;
不得到直接最终结果的是:异步调用;
阻塞、非阻塞:
发生在函数调用的时候,是否立刻返回;
立刻返回:非阻塞调用;
不立刻返回:阻塞调用;
ps:同步、异步 与 阻塞、非阻塞 在概念上是不相关的;
同步、异步:关注的是结果;
阻塞、非阻塞:关注的是是否等待;
ps:异步非阻塞是最好的性能咯;
同步IO、异步IO、IO多路复用: