多进程:
多任务:
简单的说就是操作系统(os)可以同时运行多个任务。我们的操作系统就是多任务。
单核cpu:
1、时间片轮换 :每个进程被分配一个时间 段,称作它的时间片,即该进程允许运行的时间
2、优先级别调度:优先级别越高就约会被优先调用
多核cpu:
1、并发 :
并发就是只有一个CPU资源,程序(或线程)之间要竞争得到执行机会。第一个阶段,在A执行的过程中B,C不会执 行,因为这段时间内这个CPU资源被A竞争到了,同理,第二个阶段只有B在执行,第三个阶段只有C在执行。其实,并发过程 中,A, B,C并不是同时在进行的(微观角度)。但又是同时进行的(宏观角度)。
微观角度:微观角度:所有的并发处理都有排队等候,唤醒,执行等这样的步骤,在微观上他们都是序列被处理的,如果是同一时 刻到达的请求(或线程)也会根据优先级的不同,而先后进入队列排队等候执行。
宏观角度:多个几乎同时到达的请求(或线程)在宏观上看就像是同时在被处理
2、并行 :
指两个或两个以上事件(或线程)在同一时刻发生,是真正意义上的不同事件或线程在同一时刻,在不同CPU资源呢上 (多 核),同时执行。
简易理解:
你在敲代码,烟瘾犯了,你一直到敲完了以后才去抽烟,这就说明你不支持并发也不支持并行。
你敲代码敲到一半,烟瘾犯了,你停了下来抽空了根烟,抽完继续敲代码,这说明你支持并发。
你敲代码敲到一半,烟瘾犯了,你一边敲一边抽烟,这说明你支持并行。
Fork子进程
程序:
编写完的代码,在没有运行的情况下,称之为程序。
进程:
正在运行的程序称为进程。 进程,除了包含代码外,还需要运行环境等,所以和程序是存在区别的。
Fork:
python在os模块中封装了常用的系统调用,其中就有fork。注意:fork只能在linux和mac中运行不能在windows中运行。
Python中的fork() 函数可以获得系统中进程的PID ( Process ID ),返回0则为子进程,否则就是父进程,然后可以据此对运行中的进程进行操作。
运行结果如下图:
getpid、getppid:
os.getpid:获得子进程编号
getppid:获得父进程编号
运行结果如下图所示(会无限循环下去因为用的是while True死循环):
多进程修改全局变量
其运行结果如下图:
正常情况下后一个输出结果应该位1+1=2的,但是结果中显示都是位1,说明了进程间是无法共享数据的。
注意:多个进程间,每个进程的所有数据(包括全局变量)都是各自拥有一份的,互不影响。
多个fork问题
同时进行多个fork
其结果如下图:
主线程和子线程各执行了一次,但是第三个和第四个都执行了两次出现上图结果的原因入下图所示:
windows下实现多进程:multiprocessing
其运行结果如下图:
从上面的运行结果中可以看出 主程序比子程序运行结束的要快,那么就需要用t1.join来进行约束(表示这个子进程运行完成才能进行主进程)
在上面的程序中是带参数的,如果方法中不带参数 则可以直接将参数去掉。
类实现方式:
进程类的实现非常的简单,只要继承了Process类就ok了,重写该类的run方法,run方法里面的代码,就是我们需要的子进程代码。
代码如下图:
运行结果如下
在进程类的实现中如果想要初始化一些前面我们提到过的参数,如进程名称等,可以使用__init__借助父类来完成。(注意第一个属性名称具有特殊意义,如果不用super().__init__将会无法进行修改)如下图所示:
结果如下图:
这时候的函数名字就是graves
阻塞/非阻塞、同步/异步:
在进行网络变成时,长用到同步(Sync)/异步(Async),阻塞(Block)/非阻塞(Unblock)四种调用方式:
同步:
所谓同步,就是在c端发出一个功能调用时,在没有得到结果之前,该调用就不返回。也就是必须一件一件事做,等前一件做完了才能做下一件事。
异步:
当c端一个异步过程调用通过发出后,调用者不能立刻得到结果。实际处理这个调用的不见在完成后,通过状态、通知和回调来通知调用者。
阻塞:
阻塞调用时值调用结果返回之前,当前线程会被挂起(线程进入非可执行状态,在这个状态下,cpu不会给线程分配时间片,即线程暂停运行)。函数只有在得到结果之后才会返回。
非阻塞:
非阻塞和阻塞的概念相对应,指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回。
进程池:
当我们需要的进程数量不多的时候,我们可以使用multiprocessing的Process类来创建进程。但是如果我们需要的进程特别多的时候,手动创建工作量太大了,所以Python也为我们提供了Pool(池)的方式来创建大量进程。
其代码入下图所示:
运行结果如下图所示:
注意:一般我们使用apply_async这个方法,表示非阻塞的运行,一旦使用了apply方法表示阻塞式执行任务,此时就是单任务执行了(一般不会使用,特殊场景才会使用)
本地进程间的通信方式:
队列(Queue)、管道(Pipe)、管理者(Manager)
队列(queue)常用方法:
Queue.qsize(): 返回queue中item的数量,注意这个数量并不准确
Queue.empty(): 如果队列为空则返回True
Queue.full():如果队列充满则返回True
Queue.put(item[, block[, timeout]]): block表示是否阻塞,默认为True即阻塞,如果设定了timeout,则阻塞timeout时长,如果仍然没有空余的slot,则raise Queue.full exception。如果block=False,那么就不阻塞,当queue full时,直接报Queue.full exception。写入队列对象
Queue.get([block[, timeout]]) 获得队列对象
具体用法入下图所示:
其运行结果为:
管道(Pipe):
管道是一种两个进程间进行单向通信的机制。因为管道传递数据的单向性,管道又称为半双工管道。管道的这一特点决定了器使用的局限性。管道是Linux支持的最初Unix IPC形式之一,具有以下特点:
数据只能由一个进程流向另一个进程(其中一个读管道,一个写管道);如果要进行双工通信,需要建 立两个管道。
管道只能用于父子进程或者兄弟进程间通信。,也就是说管道只能用于具有亲缘关系的进程间通信。
用法与queue相似:
其运行结果为:
Manager:
Manager对象类似于服务器与客户之间的通信,做到了进程之间的数据共享。Managers支持的数据类型有list、dict、Namespace、Lock、rlock、Queue、Value、Arrayand so on。
结果如下:
从图中可以看出 manager做到了进程之间的数据共享
线程:
进程:
能够完成多个任务,一般而言,一个进程就是一个独立的软件,如我们在电脑上运行了多个QQ。进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。
线程:
能够完成多个任务,一般而言,一个进程至少存在一个线程或者多个线程,如打开网页,启动多个页面选项卡。线程,有时被称为轻量级 进程(Lightweight Process,LWP),是程序执行流的最小单元。
注意:进程是程序的基本单位,线程是程序的最小单位。类似于人们币的基本单位是元,最小单位是分
两者区别:
1、一个程序中至少有一个进程,一个进程中至少有一个线程;
2、线程的划分尺度小于进程(占有资源),使得多线程程序的并发性高;
3、进程运行过程中拥有独立的内存空间,而线程之间共享内存,从而极大的提高了程序的运行效率
4、线程不能独立运行,必须存在于进程中
5、多线程和多进程的写法很像,其次就是多线程的运行速度很快如果没有join或者设置为守护线程,主线程会直接执行后面的代码,之后挂起,等待所有的子线程运行完成后结束,才能结束。
线程优缺点:线程开销小,但是不利于资源的管理和保护
进程与之相反:进程开销大,但是利于资源的管理和保护,安全比较有保障
多线程------threading:
Python3中取消了thread模块,只有threading模块,所以我们使用threading模块来学习多线程编程。
其运行结果入下图:
重写run方法:
结果如下:
传递参数版:
1、 线程同样会有名称
2、 当run运行完成,该线程就会自动结束
3、 我们无法控制线程,但是可以通过一些别的方式来影响线程调度
4、 下图是线程的几种状态
===================================================================================================
Python的GIL(Global Interpreter Lock):
官方定义:
In CPython, the global interpreter lock, or GIL, is a mutex that prevents multiple native threads from executing Python bytecodes at once. This lock is necessary mainly because CPython’s memory management is not thread-safe. (However, since the GIL exists, other features have grown to depend on the guarantees that it enforces.)
定义说,GIL是多线程间的一把互斥锁,并且是一把全局锁,它保证了Cpython在内存管理上面是线程安全的。 这里要注意一点的是,官方定义开头就给了限制范围,是在CPython这个解释器下。我们知道,python存在各种各样的解释器,也就是说,在某些解释器下,其实并不存在GIL这个东西,经过查阅资料,像JPython中就不存在GIL。
结论:在Cpython解释器中,同一个进程下开启的多线程,同一时刻只能有一个线程执行,无法利用多核优势。GIL并不是Python的特性,Python完全可以不依赖于GIL。
多线程-共享全局变量 :
结果
在图中可以看到两个子线程是可以共享主线程的数据的。此时的数据较小,但是如果数据很大的时候就会可能出现别的问题了:
假设两个线程t1和t2都要对No=0进⾏增1运算,t1和t2都各对No修改100万次次,No的最终的结果应该为2000000。但是由于是多线程访问,有可能出现下⾯情况:在No=0时,t1取得No=0。此时系统把t1调度为”sleeping”状态,把t2转换为”running”状态,t2也获得No=0。然后t2对得到的值进⾏加1并赋给No,使得No=1。然后系统⼜把t2调度为”sleeping”,把t1转为”running”。线程t1⼜把它之前得到的0加1后赋值给No。这样,明明t1和t2都完成了1次加1⼯作,但结果仍然是No=1。如图:
其结果为:
如果我们将代码中的time.sleep(3)注释去掉
问题产⽣的原因就是没有控制多个线程对同⼀资源的访问,对数据造成破坏,使得线程运⾏的结果不可预期。这种现象称为“非线程安全”。那么该怎么解决着这个问题呢?
系统调⽤t1,然后获取到No的值为0,此时上⼀把锁,即不允许其他现在操作No对No的值进⾏+1解锁,此时No的值为1,其他的线程就可以使⽤No了,⽽且是No的值不是0⽽是1同理其他线程在对No进⾏修改时,都要先上锁,处理完后再解锁,在上锁的整个过程中不允许其他线程访问,就保证了数据的正确性。通俗点就是:A去上厕所,进去后把厕所门锁上了,B就不能进去,要等到A出来将锁打开了才能进去。所以上述问题就得需要一把“锁”来解决----互斥锁
互斥锁:
当多个线程⼏乎同时修改某⼀个共享数据的时候,需要进⾏同步控制线程同步能够保证多个线程安全访问竞争资源,最简单的同步机制是引⼊互斥锁。
某个线程要更改共享数据时,先将其锁定,此时资源的状态为“锁定”,其他线程不能更改;直到该线程释放资源,将资源的状态变成“⾮锁定”,其他的线程才能再次锁定该资源。互斥锁保证了每次只有⼀个线程进⾏写⼊操作,从⽽保证了多线程情况下数据的正确性。
threading模块中定义了Lock类,常用方法有:
#创建锁:mutex = threading.Lock()
#锁定:mutex.acquire([blocking])
#释放:mutex.release()
锁定⽅法acquire可以有⼀个blocking参数。如果设定blocking为True,则当前线程会堵塞,直到获取到这个锁为⽌(如果没有指定,那么默认为True),如果设定blocking为False,则当前线程不会堵塞。
锁的好处:
确保了某段关键代码只能由⼀个线程从头到尾完整地执⾏
锁的坏处:
阻⽌了多线程并发执⾏,包含锁的某段代码实际上只能以单线程模式执⾏,效 率就⼤⼤地下降了。由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对⽅持有的锁时,可能会造成死锁。
具体做法看图说话:
结果如下:
⾮共享数据:
结果如下:
非共享数据无需加锁,因为局部变量不是共享的,不能彼此影响。
在多线程开发中,全局变量是多个线程都共享的数据,⽽局部变量等是各⾃线程的,是⾮共享的。所以不存在非线程安全问题。
网友评论