python:多进程和多线程(提高cpu效率)
一、进程
1.1-1.听说过多任务吗
在现实社会,我们经常需要一种场景,就是同时有多个事情需要执行,如在浏览网页的同时需要听音乐。比如说在跳舞的时候要唱歌。
同样的,在程序中我们也可能需要这种场景。如下面我们以同时听音乐和浏览网页为例。
def network(): |
执行代码,我们发现代码并没有按照我们的想法执行,而是,一直执行上网的代码,这样就无法完成我们想要的效果。
如果你想要同时听音乐和浏览网页两件事,就需要一个新的方式来完成,这个方式就是----多任务。
1-2.多任务是咋回事呢?
什么叫做多任务呢?简单的说,就是操作系统(OS)可以同时运行多个任务。如你可以一边浏览网页,一边听着歌,同时还可以使用画板画着画,这个就是多任务。其实我们的操作系统就是多任务,只是我们都没有注意罢了。
单核CPU中:
1、 时间片轮换
2、 优先级别调度
多核CPU中:
1、 并发
2、 并行
1-3.1.Fork子进程
一些概念
编写完的代码,在没有运行的情况下,称之为程序。
正在运行的代码,就成为了进程。
进程,除了包含代码外,还需要运行环境等,所以和程序是存在区别的。
Fork
Python在os模块上封装了常用的系统调用,其中就包括了fork,我们可以很轻松地在Python代码中创建进程。
# 注意,下面代码只能在Linux、Mac系统上运行,window下不支持运行!!! Print(“这个代码会执行两次”) |
Python中的fork() 函数可以获得系统中进程的PID ( Process ID ),返回0则为子进程,否则就是父进程,然后可以据此对运行中的进程进行操作;
但是强大的 fork() 函数在Windows版的Python中是无法使用的。。。只能在Linux系统中使用,比如 Ubuntu 15.04,Windows中获取父进程ID可以用 getpid()。
1-3-2.getpid、getppid
我们可以使用os模块的getpid来获取当前进程的进程编号,同样也可以使用os.getppid()方法案来获取当前进程的父进程编号。
import os |
使用os模块中的getpid()方法,可以得到当前线程的编号,使用getppid()方法可以得到该线程的父级编号。
1-3-3.多进程修改全局变量
import os |
输出结果如下:
父进程~~~~~~ 1 子进程~~~~~~ 1 |
由此可见,进程间是无法共享数据的。
注意:多个进程间,每个进程的所有数据(包括全局变量)都是各自拥有一份的,互不影响。
完成最开始的任务
现在我们已经有了多进程的基础知识了,那么就可以完成之前,上网的同时在唱歌:
import os |
1-3-4.多个fork问题(我想要哦~)
上面的所有程序中,我们使用了fork函数,生成了两个进程(一个主进程、一个子进程),但是如果我们在程序中需要多个进程呢?如两次调用fork函数,会生成三个进程吗?
import os,time |
结果如下:
我们发现,主线程和子线程各执行了一次,但是第三个和第四个都执行了两次,为什么了,看下面的图。
1-3-5. 主进程和子进程的执行顺序
其实通过上面的代码,我们已经发现了,主进程和子进程的执行顺序是没有规律的,这个不受程序员的控制,有操作系统的调度算法来控制。
1-4.multiprocessing
前面我们使用os.fork()方法实现了多进程,但是这种方案只能在Linux下运行,window环境下是无法运行的,那么有没有可以实现在任何平台都能运行的多进程了,有!Python为大家提供了multiprocessing模块用来实现多进程。
1、函数实现方式
Demo1(无参数版):
from multiprocessing import Process |
运行结果如下:
我们发现主进程结束后,子进程才结束,说明我们确实开辟了一个独立的进程。
Demo2(有参数版):
from multiprocessing import Process |
args为调用的子进程的函数的参数,注意类型是一个元组。
Demo3(多个子进程版):
from multiprocessing import Process |
多个进程启动还是一样,执行的顺序同样不可控。
Demo4(主进程等待子进程版):
join方法表示只有子进程执行完成后,主进程才能结束。主进程会等待子进程完成后,自身才会执行完成。
from multiprocessing import Process |
Demo5(常用方法版):
from multiprocessing import Process |
Demo5(多个进程使用不同的方法版):
from multiprocessing import Process |
2.类实现方式
在Python中,很多的方案都提供了函数和类两种实现方式,如:装饰器、自定义元类。同样多进程也有两种实现,前面我们已经看了使用函数实现的方式,下面我们使用类来实现一下呗。
进程类的实现非常的简单,只要继承了Process类就ok了,重写该类的run方法,run方法里面的代码,就是我们需要的子进程代码。
from multiprocessing import Process
|
在进程类的实现中如果想要初始化一些前面我们提到过的参数,如进程名称等,可以使用__init__借助父类来完成。
from multiprocessing import Process |
1-5.进程池Pool
当我们需要的进程数量不多的时候,我们可以使用multiprocessing的Process类来创建进程。但是如果我们需要的进程特别多的时候,手动创建工作量太大了,所以Python也为我们提供了Pool(池)的方式来创建大量进程。
from multiprocessing import Pool |
注意:一般我们使用apply_async这个方法,表示非阻塞的运行(异步)一旦使用了apply方法表示阻塞式(同步)执行任务,此时就是单任务执行了(一般不会使用,特殊场景才会使用)
请看下面的例子:
from multiprocessing import Process, Pool # Pool进程池,可以提前创建很多个进程对象放置于进程池中, def eat(): print("在吃饭") return 1 # def run(): # print("在跑步") # return 2 def cb(arg): # 回调函数,每次子进程执行完毕后回调函数,若子进程函数中有返回值,则此返回值被当做参数传递给回调函数 print("回调,原函数返回值为:", arg) if __name__ == '__main__': P = Pool(10) # 可以规定在进程池中创建多少个进程对象 print("+++++++++++++ main start +++++++++++++") for x in range(10): num1 = 0 num2 = 0 P.apply_async(func=eat, callback=cb) # P.apply_async(func=run, callback=cb) P.close() P.join() print("============= main end ================")执行结果如下:
+++++++++++++ main start +++++++++++++
在吃饭
回调,原函数返回值为: 1
在吃饭
回调,原函数返回值为: 1
在吃饭
回调,原函数返回值为: 1
在吃饭
回调,原函数返回值为: 1
在吃饭
回调,原函数返回值为: 1
在吃饭
回调,原函数返回值为: 1
在吃饭
回调,原函数返回值为: 1
在吃饭
回调,原函数返回值为: 1
在吃饭
回调,原函数返回值为: 1
在吃饭
回调,原函数返回值为: 1
============= main end ================
分析:利用pool进程池创建的子进程对象,主进程不会等待子进程执行完毕才结束,而是主进程先结束,这会导致子进程无法执行。想要避免这种情况就要用到join()方法,迫使主进程等待子进程执行结束后才结束,使得子进程得以运行,但是一定要先关闭进程池,在调用join()方法。
1-6.进程间的通信方式—多进程queue(掌握)
我们通过学习前面的知识知道,进程间是无法随意通信的,但是有时候我们也需要多个进程间能够通信,那么操作系统也为我们提供了几种机制来实现进程间的通信。
1-6-1.Queue(消息队列)
我们可以使用multiprocessing模块中的Queue(消息队列)来完成多进程间的数据传递
我们可以通过queue获取队列对象,队列对象的常用方法如下:
q.put() q.get() q.get(item,block=True,timeout=None) q.put(item,block=True,timeout=None) q.empty() q.full() q.qsize() q.put_nowait() q.get_nowait() |
来!上代码:
from multiprocessing import Process,Queue |
请看下面的例子:
from multiprocessing import Process, Pool, Queue # Queue队列的特征:先进先出,注意queue是线程的队列,不同于Queue import time def sendMsg(q,msg): while True: print("发送数据:%s>>>>>>>>>>>>>>>>>>>>>>" % msg) q.put(msg) time.sleep(1) def accept(q): while True: print("<<<<<<<<<<<<<<<<<<<<<接收到的数据为:%s"%q.get()) time.sleep(1) if __name__ == '__main__': q = Queue(5) new_time = time.time() print("++++++++++++++++++ main start +++++++++++++++++++++") t1 = Process(target=sendMsg, args=(q,"你好吗?"), name="发送数据进程") t1.start() t2 = Process(target=accept,args=(q,), name="接收数据进程") t2.start() print("================== main end ======================", time.time() - new_time)
执行结果为:
++++++++++++++++++ main start +++++++++++++++++++++
================== main end ====================== 0.031022071838378906
发送数据:你好吗?>>>>>>>>>>>>>>>>>>>>>>
<<<<<<<<<<<<<<<<<<<<<接收到的数据为:你好吗?
发送数据:你好吗?>>>>>>>>>>>>>>>>>>>>>>
<<<<<<<<<<<<<<<<<<<<<接收到的数据为:你好吗?
发送数据:你好吗?>>>>>>>>>>>>>>>>>>>>>>
<<<<<<<<<<<<<<<<<<<<<接收到的数据为:你好吗?
发送数据:你好吗?>>>>>>>>>>>>>>>>>>>>>>
<<<<<<<<<<<<<<<<<<<<<接收到的数据为:你好吗?
发送数据:你好吗?>>>>>>>>>>>>>>>>>>>>>>
<<<<<<<<<<<<<<<<<<<<<接收到的数据为:你好吗?
发送数据:你好吗?>>>>>>>>>>>>>>>>>>>>>>
<<<<<<<<<<<<<<<<<<<<<接收到的数据为:你好吗?
发送数据:你好吗?>>>>>>>>>>>>>>>>>>>>>>
<<<<<<<<<<<<<<<<<<<<<接收到的数据为:你好吗?
.........................
分析:利用multiprocessing中的Queue模块(消息队列)可以实现进程之间的通信,Queue为队列,先进先出,使得一个进程中推进数据,另一个进程可以拿出数据。
1-6-2.进程间的通信方式—pipe(了解)
进程间的通信除了我们上面学习的队列外,还有管道的方式。
from multiprocessing import Process,Pipe |
例如下面的例子:
from multiprocessing import Process,Pipe import time def sendMsg(p,msg): print("发送数据:%s>>>>>>>>>>>>>>>>>>>>>>" % msg) p.send(msg)p.close()time.sleep(1)if __name__ == '__main__': p1,p2 = Pipe() # Pipe管道返回值有两个,一个是父进程,一个是子进程 print(p1,p2) new_time = time.time() print("++++++++++++++++++ main start +++++++++++++++++++++") t1 = Process(target=sendMsg, args=(p2,"你好吗?"), name="发送数据进程") t1.start() print("<<<<<<<<<<<<<<<接收到的数据为:%s"%p1.recv()) print("================== main end ======================", time.time() - new_time)
运行结果为:
<multiprocessing.connection.PipeConnection object at 0x0000023A96337400> ,<multiprocessing.connection.PipeConnection object at 0x0000023A96337438>
++++++++++++++++++ main start +++++++++++++++++++++
发送数据:你好吗?>>>>>>>>>>>>>>>>>>>>>>
<<<<<<<<<<<<<<<接收到的数据为:你好吗?
================== main end ====================== 0.27419519424438477
1-6-3.进程间的数据共享—manager(掌握)
之前我们学习进程间的数据通信仅仅只是数据的传递,而不是数据的共享,那么如果我们想实现进程间的数据共享应该怎么做呢?使用Manager来实现。
Managers支持的数据类型有list、dict、Namespace、Lock、Rlock、Queue、Value、Array and so on。
看案例:
from multiprocessing import Process,Manager |
from multiprocessing import Process, Manager def saveDatas(lists, dicts): lists.append("武松") lists.append("潘金莲") lists.append("武大郎") dicts["name"] = "姜子牙" dicts["age"] = 80 dicts["mp"] = 50 print(lists) print(dicts) if __name__ == '__main__': print("++++++++++++++ main start +++++++++++++++++++") # manager = Manager() # 获取一个Manager对象 with Manager() as manager: # <==>manager = Manager() lists = manager.list() # 普通的list无法跨越多进程,只有manager中的list可以 dicts = manager.dict() ls = [] # 启动线程 for x in range(5): task = Process(target=saveDatas, args=(lists, dicts)) task.start() ls.append(task) # 如果join()方法写在这里,则是一个进程结束后才开始第二个进程,进程之间数据同步,类似于单进程 # for i in ls: i.join() # manager 也要借助于join()方法来迫使主进程等待子进程的运行 print("================ main end ================") ''' 运行结果:++++++++++++++ main start +++++++++++++++++++ ['武松', '潘金莲', '武大郎'] {'age': 80, 'mp': 50, 'name': '姜子牙'} ['武松', '潘金莲', '武大郎', '武松', '潘金莲', '武大郎'] {'age': 80, 'mp': 50, 'name': '姜子牙'} ['武松', '潘金莲', '武大郎', '武松', '潘金莲', '武大郎', '武松', '潘金莲', '武大郎'] {'age': 80, 'mp': 50, 'name': '姜子牙'} ['武松', '潘金莲', '武大郎', '武松', '潘金莲', '武大郎', '武松', '潘金莲', '武大郎', '武松', '潘金莲', '武大郎'] {'age': 80, 'mp': 50, 'name': '姜子牙'} ['武松', '潘金莲', '武大郎', '武松', '潘金莲', '武大郎', '武松', '潘金莲', '武大郎', '武松', '潘金莲', '武大郎', '武松', '潘金莲', '武大郎'] {'age': 80, 'mp': 50, 'name': '姜子牙'} ================ main end ================ '''
二、线程
进程:能够完成多个任务,一般而言,一个进程就是一个独立的软件,如我们在电脑上运行了多个QQ
线程:能够完成多个任务,一般而言,一个进程至少存在一个线程或者多个线程,如打开网页,启动多个页面选项卡。
定义:
进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。
线程,有时被称为轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元。一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组成。另外,线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行。由于线程之间的相互制约,致使线程在运行中呈现出间断性。线程也有就绪、阻塞和运行三种基本状态。就绪状态是指线程具备运行的所有条件,逻辑上可以运行,在等待处理机;运行状态是指线程占有处理机正在运行;阻塞状态是指线程在等待一个事件(如某个信号量),逻辑上不可执行。每一个程序都至少有一个线程,若程序只有一个线程,那就是程序本身。
区别:
1、 一个程序中至少有一个进程,一个进程中至少有一个线程;
2、 线程的划分尺度小于进程(占有资源),使得多线程程序的并发性高;
3、 进程运行过程中拥有独立的内存空间,而线程之间共享内存,从而极大的提高了程序的运行效率
4、 线程不能独立运行,必须存在于进程中
优缺点:
线程开销小,但是不利于资源的管理和保护,而进程反之。
1.多线程---threading
Python2中支持多线程编程的模块有两个thread、threading,但是官方已经不建议使用thread模块
Python3中取消了thread模块,只有threading模块,所以我们使用threading模块来学习多线程编程。
1-1.函数式编程方案:
单线程的案例:
import time |
多线程的案例
import time |
1、 多线程和多进程的写法很像
2、 其次就是多线程的运行速度很快
3、 如果没有join或者设置为守护线程,主线程会直接执行后面的代码,之后挂起,等待所有的子线程运行完成后结束,才能结束。
多线程中的常用方法:
import threading,time |
多线程和多进程的效率对比,以下是多线程:
from threading import Thread,current_thread # 多线程 current_thread 当前进程 import time def run(msg): print("这个是独立线程运行的代码",msg) time.sleep(1) if __name__ == '__main__': new_time = time.time() print("++++++++++ main start +++++++++++") for x in range(10): t1 = Thread(target=run,args=("这个是参数%s"%x,)) t1.start() print("========== main end ==========", current_thread().getName(),time.time()-new_time) # current_thread().getName()可以得到当前主线程的名称 ''' 运行结果: ++++++++++ main start +++++++++++ 这个是独立线程运行的代码 这个是参数0 这个是独立线程运行的代码 这个是参数1 这个是独立线程运行的代码 这个是参数2 这个是独立线程运行的代码 这个是参数3 这个是独立线程运行的代码 这个是参数4 这个是独立线程运行的代码 这个是参数5 这个是独立线程运行的代码 这个是参数6 这个是独立线程运行的代码 这个是参数7 这个是独立线程运行的代码 这个是参数8 这个是独立线程运行的代码 这个是参数9 ========== main end ========== MainThread,0.0010006427764892578 '''我们再看看多进程:
from multiprocessing import Process # 多进程 import time def run(msg): print("这个是独立线程运行的代码",msg) time.sleep(1) if __name__ == '__main__': new_time = time.time() print("++++++++++ main start +++++++++++") for x in range(10): t1 = Process(target=run,args=("这个是参数%s"%x,)) t1.start() print("========== main end ==========",time.time()-new_time) ''' 运行结果: ++++++++++ main start +++++++++++ ========== main end ========== 0.2971487045288086 这个是独立线程运行的代码 这个是参数0 这个是独立线程运行的代码 这个是参数2 这个是独立线程运行的代码 这个是参数1 这个是独立线程运行的代码 这个是参数9 这个是独立线程运行的代码 这个是参数3 这个是独立线程运行的代码 这个是参数5 这个是独立线程运行的代码 这个是参数8 这个是独立线程运行的代码 这个是参数4 这个是独立线程运行的代码 这个是参数7 这个是独立线程运行的代码 这个是参数6 '''
1-2.面向对象编程方案—线程类:
和进程类似,线程也支持创建线程类来完成多线程的编程,同样是继承Thead类,重写run方法:
import time,threading |
传递参数版:
import time,threading |
from threading import Thread class Team(Thread): def __init__(self, name): super().__init__(name=name) # 继承自Thread def run(self): for i in range(10): print("这是类中的子进程") if __name__ == '__main__': print("+++++++++++ main start ++++++++++++") t = Team("丽丽") t.start() print("========== main end ============") ''' 运行结果: +++++++++++ main start ++++++++++++ 这是类中的子进程 ========== main end ============ 这是类中的子进程 这是类中的子进程 这是类中的子进程 这是类中的子进程 这是类中的子进程 这是类中的子进程 这是类中的子进程 这是类中的子进程 这是类中的子进程 '''
1、 线程同样会有名称 未命令默认为 Thread-n
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.)
结论:在Cpython解释器中,同一个进程下开启的多线程,同一时刻只能有一个线程执行,无法利用多核优势。
首先需要明确的一点是GIL并不是Python的特性,它是在实现Python解析器(CPython)时所引入的一个概念。就好比C++是一套语言(语法)标准,但是可以用不同的编译器来编译成可执行代码。有名的编译器例如GCC,INTEL C++,Visual C++等。Python也一样,同样一段代码可以通过CPython,PyPy,Psyco等不同的Python执行环境来执行。像其中的JPython就没有GIL。然而因为CPython是大部分环境下默认的Python执行环境。所以在很多人的概念里CPython就是Python,也就想当然的把GIL归结为Python语言的缺陷。所以这里要先明确一点:GIL并不是Python的特性,Python完全可以不依赖于GIL。
2.多线程-共享全局变量
我们前面已经说过了,多进程之间的数据时独立的,各自都有一份,即便是全局变量也不共享,那么一个进程中的多线程之间的全局变量呢?注意:多线程之间的全局数据数是共享的,因为多线程是在一个进程中,数据是互相可以访问的,案例如下:
import threading |
由此可知,一个进程间的多线程(下面开始统一叫多线程,因为我们一直说的多线程就是一个进程下的多线程)的全局数据是共享的。
但是数据较为庞大时就不会出现这么理想的情况了。
from threading import * num = 0 def run1( ): global num for x in range(1000000): num += 1 print(num) def run2( ): global num for x in range(1000000): num += 1 print(num) if __name__ == '__main__': t1 = Thread(target=run1,args=(lock,)) t1.start() t2 = Thread(target=run2,args=(lock,)) t2.start()运行结果:
1257760
1282077
为什么第二个数据不是2000000呢?这是因为num += 1实际上分为两步,a=num+1,num=a,这中间出现了中间变量,也就导致了可能未执行完就会被另一个进程抢先。最终导致部分数据并没有增加。
3.同步(协同步调)
多线程开发可能遇到的问题
假设两个线程t1和t2都要对num=0进⾏增1运算,t1和t2都各对num修改10
次,num的最终的结果应该为20。
但是由于是多线程访问,有可能出现下⾯情况:
在num=0时,t1取得num=0。此时系统把t1调度为”sleeping”状态,把t2转换
为”running”状态,t2也获得num=0。然后t2对得到的值进⾏加1并赋给num,
使得num=1。然后系统⼜把t2调度为”sleeping”,把t1转为”running”。线程t1
⼜把它之前得到的0加1后赋值给num。这样,明明t1和t2都完成了1次加1⼯
作,但结果仍然是num=1。
import threading # time.sleep(3) |
运行结果如下:
如果我们去掉主线程的注释time.sleep(3),再次运行代码:
问题产⽣的原因就是没有控制多个线程对同⼀资源的访问,对数据造成破坏,使得线程运⾏的结果不可预期。这种现象称为“非线程安全”。
3-1.什么是同步
同步就是协同步调,按预定的先后次序进⾏运⾏。如:你说完,我再说。
"同"字从字⾯上容易理解为⼀起动作,其实不是,"同"字应是指协同、协助、互相配合。
如进程、线程同步,可理解为进程或线程A和B⼀块配合,A执⾏到⼀定程度时要依靠B的某个结果,于是停下来,示意B运⾏。B依⾔执⾏,再将结果给A,A再继续操作。
3-2.解决问题的思路
提出的那个计算错误的问题,可以通过线程同步来进⾏解决思路,如下:
系统调⽤t1,然后获取到num的值为0,此时上⼀把锁,即不允许其他现在操作num对num的值进⾏+1解锁,此时num的值为1,其他的线程就可以使⽤num了,⽽且是num的值不是0⽽是1同理其他线程在对num进⾏修改时,都要先上锁,处理完后再解锁,在上锁的整个过程中不允许其他线程访问,就保证了数据的正确性。
4.互斥锁
当多个线程⼏乎同时修改某⼀个共享数据的时候,需要进⾏同步控制线程同步能够保证多个线程安全访问竞争资源,最简单的同步机制是引⼊互斥锁。
互斥锁为资源引⼊⼀个状态:锁定/⾮锁定。
某个线程要更改共享数据时,先将其锁定,此时资源的状态为“锁定”,其他线程不能更改;直到该线程释放资源,将资源的状态变成“⾮锁定”,其他的线程才能再次锁定该资源。互斥锁保证了每次只有⼀个线程进⾏写⼊操作,从⽽保证了多线程情况下数据的正确性。
threading模块中定义了Lock类,可以⽅便的处理锁定:
#创建锁 mutex=threading.Lock() #锁定 mutex.acquire([blocking]) #释放 mutex.release() |
其中,锁定⽅法acquire可以有⼀个blocking参数。
如果设定blocking为True,则当前线程会堵塞,直到获取到这个锁为⽌(如果没有指定,那么默认为True),如果设定blocking为False,则当前线程不会堵塞。
import threading |
from threading import * num = 0 def run1(lock): global num for x in range(1000000): lock.acquire() # 将程序锁定 num += 1 lock.release() # 解锁 print(num) def run2(lock): global num for x in range(1000000): lock.acquire() # 将程序锁定 num += 1 lock.release() # 解锁 print(num) if __name__ == '__main__': lock = Lock() # 创建互斥锁,用于解决非线程安全问题 t1 = Thread(target=run1,args=(lock,)) t1.start() t2 = Thread(target=run2,args=(lock,)) t2.start() # 运行结果:1993755 # 2000000
锁的好处:
确保了某段关键代码只能由⼀个线程从头到尾完整地执⾏
锁的坏处:
阻⽌了多线程并发执⾏,包含锁的某段代码实际上只能以单线程模式执⾏,效率就⼤⼤地下降了。由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对⽅持有的锁时,可能会造成死锁。加锁之后一定要解锁,并且加锁的范围越小越好。
5.多线程-⾮共享数据
对于全局变量,在多线程中要格外⼩⼼,否则容易造成数据错乱的情况发⽣,那么⾮全局变量是否要加锁呢?答案如下面的案例:
import threading |
在多线程开发中,全局变量是多个线程都共享的数据,⽽局部变量等是各⾃线程的,是⾮共享的。所以不存在非线程安全问题。
6.死锁
在线程间共享多个资源的时候,如果两个线程分别占有⼀部分资源并且同时等待对⽅的资源,就会造成死锁。尽管死锁很少发⽣,但⼀旦发⽣就会造成应⽤的停⽌响应。
import threading |
from threading import Thread,Lock import time # 申请两个全局锁 myLock1= Lock() myLock2= Lock() def run1(): print("第一个子线程运行 ") if myLock1.acquire(timeout=2): # 等待2s,也就是添加超时时间 print("lock1已经加锁了") time.sleep(1) # 人为停顿后代码进入run2() if myLock2.acquire(): # 第一次返回布尔值,表示加锁成功,第二次返回布尔值,则会卡死 print("第一个执行了吗?") myLock2.release() myLock1.release() def run2(): print("第二个子线程运行了") if myLock2.acquire(): print("lock2已经加锁了") time.sleep(1) # 停顿后代码重新进入run1() if myLock1.acquire(): print("第二个执行了吗?") myLock1.release() myLock2.release() if __name__ == '__main__': t1 = Thread(target=run1) t1.start() t2 = Thread(target=run2) t2.start() # 运行结果: # 第一个子线程运行 # lock1已经加锁了 # 第二个子线程运行了 # lock2已经加锁了 # 可以用递归加锁的方式解决死锁现象:rlock()
我们在设计代码的时候,一定要注意避免死锁的出现。可以使用如下的方案:
Ø 程序设计时要尽量避免(银⾏家算法)
Ø 添加超时时间等(不建议使用,加锁部分可能不会执行)
7.同步应⽤
多个线程有序执⾏的一个案例:
from threading import Thread, Lock |
from threading import Thread,Lock import time myLock1 = Lock() myLock2 = Lock() myLock3 = Lock() # 通过加锁来实现一个同步案例,同步即多线程中的协同步调 def run1(): while True: if myLock1.acquire(): # 锁1已经加锁 print("run1") # 运行打印run1 myLock2.release() # 将锁2解锁,停顿1s后进入run2(),不能进入run3(),因为此时锁3是锁定状态 time.sleep(1) def run2(): while True: if myLock2.acquire(): # 将 锁2加锁 print("run2") # 运行打印run2 myLock3.release() # 将 锁3解锁,停顿1s后,进入run3() time.sleep(1) def run3(): while True: if myLock3.acquire(): # 将锁3加锁 print("run3") # 运行打印run3 myLock1.release() # 将锁1解锁,停顿1s后进入run1() time.sleep(1) if __name__ == '__main__': myLock2.acquire() # 先将锁2锁住 myLock3.acquire() # 先将锁3锁住 t1 = Thread(target=run1) t1.start() t3 = Thread(target=run3) t3.start() t2 = Thread(target=run2) t2.start() # 运行结果: # run1 # run2 # run3 # run1 # run2 # run3 # run1 # run2 # run3总结:通过添加3个全局锁,依次控制进程运行的顺序,使得出现同步现象。
8.⽣产者与消费者模式
在实际的软件开发过程中,经常会碰到如下场景:某个模块负责产生数据,这些数据由另一个模块来负责处理(此处的模块是广义的,可以是类、函数、线程、进程等)。产⽣数据的模块,就形象地称为生产者;⽽而处理数据的模块,就称为消费者。
单抽象出生产者和消费者,还够不上是生产者/消费者模式。该模还需要有一个缓冲区处于生产者和消费者之间,作为一个中介。生产者把数据放入缓冲区,而消费者从缓冲区取出数据。
Python的Queue模块中提供了同步的、线程安全的队列类,包括FIFO(先⼊先出)队列Queue,LIFO(后⼊先出)队列LifoQueue,和优先级队列PriorityQueue。这些队列都实现了锁原语(可以理解为原⼦操作,即要么不做,要么就做完),能够在多线程中直接使⽤。可以使⽤队列来实现线程间的同步。
from threading import Thread |
⽣产者消费者模式的说明:
在线程世界⾥,⽣产者就是⽣产数据的线程,消费者就是消费数据的线程。在多线程开发当中,如果⽣产者处理速度很快,⽽消费者处理速度很慢,那么⽣产者就必须等待消费者处理完,才能继续⽣产数据。同样的道理,如果消费者的处理能⼒⼤于⽣产者,那么消费者就必须等待⽣产者。为了解决这个问题于是引⼊了⽣产者和消费者模式。
⽣产者消费者模式是通过⼀个容器来解决⽣产者和消费者的强耦合问题。⽣产者和消费者彼此之间不直接通讯,⽽通过阻塞队列来进⾏通讯,所以⽣产者⽣产完数据之后不⽤等待消费者处理,直接扔给阻塞队列,消费者不找⽣产者要数据,⽽是直接从阻塞队列⾥取,阻塞队列就相当于⼀个缓冲区,平衡了⽣产者和消费者的处理能⼒。
这个阻塞队列就是⽤来给⽣产者和消费者解耦的。纵观⼤多数设计模式,都会找⼀个第三者出来进⾏解耦
python队列Queue
Queue
Queue是python标准库中的线程安全的队列(FIFO)实现,提供了一个适用于多线程编程的先进先出的数据结构,即队列,用来在生产者和消费者线程之间的信息传递
基本FIFO队列
class Queue.Queue(maxsize=0)
FIFO即First in First Out,先进先出。Queue提供了一个基本的FIFO容器,使用方法很简单,maxsize是个整数,指明了队列中能存放的数据个数的上限。一旦达到上限,插入会导致阻塞,直到队列中的数据被消费掉。如果maxsize小于或者等于0,队列大小没有限制。
举个例子:
import Queue
q = Queue.Queue()
for i in range(5):
q.put(i)
while not q.empty():
print q.get()
输出:
0
1
2
3
4
LIFO队列
class Queue.LifoQueue(maxsize=0)
LIFO即Last in First Out,后进先出。与栈的类似,使用也很简单,maxsize用法同上
再举个栗子:
import Queue
q = Queue.LifoQueue()
for i in range(5):
q.put(i)
while not q.empty():
print q.get()
输出:
4
3
2
1
0
可以看到仅仅是将Queue.Quenu类
替换为Queue.LifiQueue类
优先级队列
class Queue.PriorityQueue(maxsize=0)
构造一个优先队列。maxsize用法同上。
import Queue
import threading
class Job(object):
def __init__(self, priority, description):
self.priority = priority
self.description = description
print 'Job:',description
return
def __cmp__(self, other):
return cmp(self.priority, other.priority)
q = Queue.PriorityQueue()
q.put(Job(3, 'level 3 job'))
q.put(Job(10, 'level 10 job'))
q.put(Job(1, 'level 1 job'))
def process_job(q):
while True:
next_job = q.get()
print 'for:', next_job.description
q.task_done()
workers = [threading.Thread(target=process_job, args=(q,)),
threading.Thread(target=process_job, args=(q,))
]
for w in workers:
w.setDaemon(True)
w.start()
q.join()
结果
Job: level 3 job
Job: level 10 job
Job: level 1 job
for: level 1 job
for: level 3 job
for: job: level 10 job
8-1.FifoQueue队列(先进先出,first in first out)
8-2.LifoQueue队列(前进后出,后进先出,last in first out)
8-3.PriorityQueue队列(优先级)
9.ThreadLocal(本地线程)
我们知道多线程环境下,每一个线程均可以使用所属进程的全局变量。如果一个线程对全局变量进行了修改,将会影响到其他所有的线程。为了避免多个线程同时对变量进行修改,引入了线程同步机制,通过互斥锁,条件变量或者读写锁来控制对全局变量的访问。
只用全局变量并不能满足多线程环境的需求,很多时候线程还需要拥有自己的私有数据,这些数据对于其他线程来说不可见。因此线程中也可以使用局部变量,局部变量只有线程自身可以访问,同一个进程下的其他线程不可访问。
有时候使用局部变量不太方便,因此 python 还提供了 ThreadLocal 变量,它本身是一个全局变量,但是每个线程却可以利用它来保存属于自己的私有数据,这些私有数据对其他线程也是不可见的。在多线程环境下,每个线程都有⾃⼰的数据。⼀个线程使⽤⾃⼰的局部变量⽐使⽤全局变量好,因为局部变量只有线程⾃⼰能看见,不会影响其他线程,⽽全局变量的修改必须加锁。
from threading import Thread,local |
from threading import Thread, local
mylocal = local() # 先new一个local对象
def printMsg():
print(mylocal.name, mylocal.age, mylocal.gender)
def speak():
print("%s说了一句话" % mylocal.name)
def run1():
# 自身函数中需要的参数绑定到本地线程Threadlocal上
mylocal.name = "张三"
mylocal.age = 18
mylocal.gender = "女"
printMsg()
speak()
def run2():
mylocal.name = "李四"
mylocal.age = 25
mylocal.gender = "男"
printMsg()
speak()
if __name__ == '__main__':
t1 = Thread(target=run1)
t2 = Thread(target=run2)
t1.start()
t2.start()
# 运行结果:张三 18 女
# 张三说了一句话
# 李四 25 男
# 李四说了一句话
10.异步
同步调⽤就是:你喊你朋友吃饭,你朋友在忙,你就⼀直在那等,等你朋友忙完了 ,你们⼀起去
异步调⽤就是:你喊你朋友吃饭,你朋友说知道了,待会忙完去找你,你就去做别的了。
附录-银⾏家算法
[背景知识]
⼀个银⾏家如何将⼀定数⽬的资⾦安全地借给若⼲个客户,使这些客户既能借到钱完成要⼲的事,同时银⾏家⼜能收回全部资⾦⽽不⾄于破产,这就是银⾏家问题。这个问题同操作系统中资源分配问题⼗分相似:银⾏家就像⼀个操作系统,客户就像运⾏的进程,银⾏家的资⾦就是系统的资源。
[问题的描述]
⼀个银⾏家拥有⼀定数量的资⾦,有若⼲个客户要贷款。每个客户须在⼀开始就声明他所需贷款的总额。若该客户贷款总额不超过银⾏家的资⾦总数,银⾏家可以接收客户的要求。客户贷款是以每次⼀个资⾦单位(如1万RMB等)的⽅式进⾏的,客户在借满所需的全部单位款额之前可能会等待,但银⾏家须保证这种等待是有限的,可完成的。例如:有三个客户C1,C2,C3,向银⾏家借款,该银⾏家的资⾦总额为10个资⾦单位,其中C1客户要借9各资⾦单位,C2客户要借3个资⾦单位,C3客户要借8个资⾦单位,总计20个资⾦单位。某⼀时刻的状态如图所示。
对于a图的状态,按照安全序列的要求,我们选的第⼀个客户应满⾜该客户所需的贷款⼩于等于银⾏家当前所剩余的钱款,可以看出只有C2客户能被满⾜:C2客户需1个资⾦单位,⼩银⾏家⼿中的2个资⾦单位,于是银⾏家把1个资⾦单位借给C2客户,使之完成⼯作并归还所借的3个资⾦单位的钱,进⼊b图。同理,银⾏家把4个资⾦单位借给C3客户,使其完成⼯作,在c图中,只剩⼀个客户C1,它需7个资⾦单位,这时银⾏家有8个资⾦单位,所以C1也能顺利借到钱并完成⼯作。最后(⻅图d)银⾏家收回全部10个资⾦单
位,保证不赔本。那麽客户序列{C1,C2,C3}就是个安全序列,按照这个序列贷款,银⾏家才是安全的。否则的话,若在图b状态时,银⾏家把⼿中的4个资⾦单位借给了C1,则出现不安全状态:这时C1,C3均不能完成⼯作,⽽银⾏家⼿中⼜没有钱了,系统陷⼊僵持局⾯,银⾏家也不能收回投资。综上所述,银⾏家算法是从当前状态出发,逐个按安全序列检查各客户谁能完成其⼯作,然后假定其完成⼯作且归还全部贷款,再进⽽检查下⼀个能完
成⼯作的客户,......。如果所有客户都能完成⼯作,则找到⼀个安全序列,银⾏家才是安全的。