一,进程与线程
1.什么是线程
线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务
一个线程是一个执行上下文,这是一个CPU的所有信息需要执行一系列的指令。
假设你正在读一本书,你现在想休息一下,但是你希望能够回来,恢复从你停止的位置。实现这一点的方法之一是通过草草记下页码、行号和数量。所以你读一本书的执行上下文是这三个数字。
如果你有一个室友,她使用相同的技术,她可以把书当你不使用它,并继续阅读,她停了下来。然后你就可以把它拿回来,恢复你在哪里。
线程在相同的方式工作。CPU是给你的错觉同时做多个计算。它通过花一点时间在每个计算。它可以这样做,因为它有一个为每个计算执行上下文。就像你可以与你的朋友分享一本书,很多任务可以共享CPU。
更多的技术水平,一个执行上下文(因此一个线程)由CPU的寄存器的值。
最后:线程不同于流程。执行线程的上下文,而进程是一群资源与计算有关。一个过程可以有一个或多个线程。
澄清:与流程相关的资源包括内存页(一个进程中的所有线程都有相同的视图的内存),文件描述符(如。、打开的套接字)和安全凭据(如。,用户的ID开始这个过程)。
2.什么是进程
一个执行程序被称为过程的实例。
每个进程提供了所需的资源来执行一个程序。进程的虚拟地址空间,可执行代码,打开系统处理对象,一个安全上下文,一个独特的进程标识符,环境变量,优先类,最小和最大工作集大小和至少一个线程的执行。每个流程开始一个线程,通常被称为主要的线程,但从它的任何线程可以创建额外的线程。
3.进程与线程的区别
- 线程共享创建它的进程的地址空间,进程有自己的地址空间。
- 线程直接访问的数据段过程;过程有自己的复制父进程的数据段。
- 线程可以直接与其他线程的通信过程,过程必须使用进程间通信和同胞交流过程。
- 新创建的线程很容易;新工艺需要复制父进程。
- 线程可以锻炼相当大的控制线程相同的过程;流程只能控制子进程。
- 主线程变更(取消、优先级变化等)可能会影响进程的其他线程的行为;父进程的变化不会影响子进
4.Python GIL(Global Interpreter Lock)
全局解释器锁在CPython的,或GIL,是一个互斥锁,防止多个本地线程执行Python字节码。这把锁是必要的,主要是因为CPython的内存管理不是线程安全的。(然而,由于GIL存在,其他功能已经习惯于依赖保证执行)。
首先需要明确的一点是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
参考文档:http://www.dabeaz.com/python/UnderstandingGIL.pdf
二、多线程
多线程类似于同时执行多个不同程序,多线程运行有如下优点:
使用线程可以把占据长时间的程序中的任务放到后台去处理。
用户界面可以更加吸引人,这样比如用户点击了一个按钮去触发某些事件的处理,可以弹出一个进度条来显示处理的进度
程序的运行速度可能加快
在一些等待的任务实现上如用户输入、文件读写和网络收发数据等,线程就比较有用了。在这种情况下我们可以释放一些珍贵的资源如内存占用等等。
线程在执行过程中与进程还是有区别的。每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
每个线程都有他自己的一组CPU寄存器,称为线程的上下文,该上下文反映了线程上次运行该线程的CPU寄存器的状态。
指令指针和堆栈指针寄存器是线程上下文中两个最重要的寄存器,线程总是在进程得到上下文中运行的,这些地址都用于标志拥有线程的进程地址空间中的内存。
线程可以被抢占(中断)。
在其他线程正在运行时,线程可以暂时搁置(也称为睡眠) -- 这就是线程的退让。
1.threading模块
直接调用:
import threading
import time
def code(num): #定义每个线程要运行的函数
print("running on number:%s" %num)
time.sleep(3)
if __name__ == '__main__':
t1 = threading.Thread(target=code,args=(1,)) #生成一个线程实例
t2 = threading.Thread(target=code,args=(2,)) #生成另一个线程实例
t1.start() #启动线程
t2.start() #启动另一个线程
print(t1.getName()) #获取线程名
print(t2.getName())
或者:
#!/usr/bin/env python
#coding:utf-8
import threading
import time
class A(object):#定义每个线程要运行的函数
def __init__(self,num):
self.num = num
self.run()
def run(self):
print('线程',self.num)
time.sleep(1)
for i in range(10):
t = threading.Thread(target=A,args=(i,))#生成一个线程实例 target对应你要执行的函数名
t.start()#启动线程
继承类调用:
import threading
import time
class MyThread(threading.Thread):#继承threading.Thread
def __init__(self,num):
threading.Thread.__init__(self)
self.num = num
def run(self):#定义每个线程要运行的函数
print("我是第%s个程序" %self.num)
time.sleep(3)#执行结束后等待三秒
if __name__ == '__main__':
t1 = MyThread(1)
t2 = MyThread(2)
t1.start()
t2.start()
或者:
import threading
import time
class MyThread(threading.Thread):#继承threading.Thread
def __init__(self,num):
threading.Thread.__init__(self)
self.num = num
def run(self):#定义每个线程要运行的函数
print("我是第%s个程序" %self.num)
time.sleep(3)#执行结束后等待三秒
if __name__ == '__main__':
for i in range(10):
t = MyThread(i)
t.start()
上述代码创建了10个“前台”线程,然后控制器就交给了CPU,CPU根据指定算法进行调度,分片执行指令
规定与方法:
import threading
首先导入threading 模块,这是使用多线程的前提。
- start 线程准备就绪,等待CPU调度
- setName 为线程设置名称
- getName 获取线程名称
- setDaemon 设置为后台线程或前台线程(默认)
如果是后台线程,主线程执行过程中,后台线程也在进行,主线程执行完毕后,后台线程不论成功与否,均停止
如果是前台线程,主线程执行过程中,前台线程也在进行,主线程执行完毕后,等待前台线程也执行完成后,程序停止 - join 逐个执行每个线程,执行完毕后继续往下执行,该方法使得多线程变得无意义
- run 线程被cpu调度后执行Thread类对象的run方法
2.Join & Daemon
join
1).join方法的作用是阻塞主进程(挡住,无法执行join以后的语句),专注执行多线程。
2).多线程多join的情况下,依次执行各线程的join方法,前头一个结束了才能执行后面一个。
3).无参数,则等待到该线程结束,才开始执行下一个线程的join。
4.设置参数后,则等待该线程这么长时间就不管它了(而该线程并没有结束)。
不管的意思就是可以执行后面的主进程了。
例如:
如果不使用join
import time
import threading
def run(n):
print('正在运行[%s]\n' % n)
time.sleep(2)
print('运行结束--')
def main():
for i in range(5):
t = threading.Thread(target=run,args=[i,])
#time.sleep(1)
t.start()
t.join(1)
print('进行中的线程名', t.getName())
#第一个执行的
m = threading.Thread(target=main,args=[])
m.start()
print("---main thread done----")
print('继续往下执行')
结果如下:
---main thread done---- #线程还没结束就执行
正在运行[0]
继续往下执行 #线程还没结束就执行
进行中的线程名 Thread-2
正在运行[1]
运行结束--
进行中的线程名 Thread-3
正在运行[2]
运行结束--
进行中的线程名 Thread-4
正在运行[3]
运行结束--
进行中的线程名 Thread-5
正在运行[4]
运行结束--
进行中的线程名 Thread-6
运行结束--
如果使用join:
import time
import threading
def run(n):
print('正在运行[%s]\n' % n)
time.sleep(1)
print('运行结束--')
def main():
for i in range(5):
t = threading.Thread(target=run,args=[i,])
t.start()
t.join(1)
print('进行中的线程名', t.getName())
#第一个执行的
m = threading.Thread(target=main,args=[])
m.start()
m.join()#开启join
print("---main thread done----") #结果是线程执行完毕之后 才执行
print('继续往下执行') #结果是线程执行完毕之后 才执行
注:join(time)等time秒,如果time内未执行完就不等了,继续往下执行
如下:
import time
import threading
def run(n):
print('正在运行[%s]\n' % n)
time.sleep(1)
print('运行结束--')
def main():
for i in range(5):
t = threading.Thread(target=run,args=[i,])
#time.sleep(1)
t.start()
t.join(1)
print('进行中的线程名', t.getName())
#第一个执行的
m = threading.Thread(target=main,args=[])
m.start()
m.join(timeout=2) #设置时间
print("---main thread done----")
print('继续往下执行')
结果:
正在运行[0]
进行中的线程名 Thread-2
运行结束--
正在运行[1]
运行结束--
进行中的线程名 Thread-3
---main thread done---- #执行了
继续往下执行 #执行了
正在运行[2]
运行结束--
进行中的线程名 Thread-4
正在运行[3]
运行结束--
进行中的线程名 Thread-5
正在运行[4]
运行结束--
进行中的线程名 Thread-6
daemon
一些线程做后台任务,比如发送keepalive包,或执行垃圾收集周期,等等。这些只是有用的主程序运行时,它可以杀死他们一旦其他,非守护线程退出。
没有守护程序线程,你要跟踪他们,和告诉他们退出,您的程序可以完全退出。通过设置它们作为守护进程线程,你可以让他们运行和忘记他们,当程序退出时,任何守护程序线程自动被杀。
import time
import threading
def run(n):
print('正在运行[%s]\n' % n)
time.sleep(1)
print('运行结束--')
def main():
for i in range(5):
t = threading.Thread(target=run,args=[i,])
time.sleep(1)
t.start()
t.join(1)
print('进行中的线程名', t.getName())
#第一个执行的
m = threading.Thread(target=main,args=[])
m.setDaemon(True)#将主线程设置为Daemon线程,它退出时,其它子线程会同时退出,不管是否执行完任务
m.start()
print("---main thread done----")
print('继续往下执行')
注意:守护程序线程突然停在关闭。他们的资源(如打开的文件、数据库事务,等等)可能不会正常发布。如果你想让你的线程停止优雅,让他们non-daemonic和使用合适的信号机制等
线程锁
一个进程下可以启动多个线程,多个线程共享父进程的内存空间,也就意味着每个线程可以访问同一份数据,此时,如果2个线程同时要修改同一份数据那就会出现数据修改会被不是一个进程修改
由于线程之间是进行随机调度,并且每个线程可能只执行n条执行之后,CPU接着执行其他线程。所以,可能出现如下问题:
import time
import threading
def addNum(ip):
global num #在每个线程中都获取这个全局变量
print('--get num:',num,'线程数',ip )
time.sleep(1)
num +=1 #对此公共变量进行-1操作
num_list.append(num)
num = 0 #设定一个共享变量
thread_list = []
num_list =[]
for i in range(10):
t = threading.Thread(target=addNum,args=(i,))
t.start()
thread_list.append(t)
for t in thread_list: #等待所有线程执行完毕
t.join()
print('final num:', num )
print(num_list)
结果:
--get num: 0 线程数 0
--get num: 0 线程数 1
--get num: 0 线程数 2
--get num: 0 线程数 3
--get num: 0 线程数 4
--get num: 0 线程数 5
--get num: 0 线程数 6
--get num: 0 线程数 7
--get num: 0 线程数 8
--get num: 0 线程数 9
final num: 10
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
正常来讲,这个num结果应该是0, 但在python 2.7上多运行几次,会发现,最后打印出来的num结果不总是0,为什么每次运行的结果不一样呢? 哈,很简单,假设你有A,B两个线程,此时都 要对num 进行减1操作, 由于2个线程是并发同时运行的,所以2个线程很有可能同时拿走了num=100这个初始变量交给cpu去运算,当A线程去处完的结果是99,但此时B线程运算完的结果也是99,两个线程同时CPU运算的结果再赋值给num变量后,结果就都是99。那怎么办呢? 很简单,每个线程在要修改公共数据时,为了避免自己在还没改完的时候别人也来修改此数据,可以给这个数据加一把锁, 这样其它线程想修改此数据时就必须等待你修改完毕并把锁释放掉后才能再访问此数据。
*注:不要在3.x上运行,不知为什么,3.x上的结果总是正确的,可能是自动加了锁
加上锁之后
import time
import threading
def addNum():
global num #在每个线程中都获取这个全局变量
print('--get num:',num )
time.sleep(1)
lock.acquire() #修改数据前加锁
num -=1 #对此公共变量进行-1操作
lock.release() #修改后释放
num = 100 #设定一个共享变量
thread_list = []
lock = threading.Lock() #生成全局锁
for i in range(100):
t = threading.Thread(target=addNum)
t.start()
thread_list.append(t)
for t in thread_list: #等待所有线程执行完毕
t.join()
print('final num:', num )
RLock(递归锁)
说白了就是在一个大锁中还要再包含子锁
Semaphore(信号量)
互斥锁 同时只允许一个线程更改数据,而Semaphore是同时允许一定数量的线程更改数据 ,比如厕所有3个坑,那最多只允许3个人上厕所,后面的人只能等里面有人出来了才能再进去。
event
一个事件是一个简单的同步对象;
事件代表一个内部国旗,和线程
可以等待标志被设置,或者设置或清除标志本身。
事件= threading.Event()
、#客户端线程等待国旗可以设置
event.wait()#服务器线程可以设置或重置它
event.set()
event.clear()
如果设置了国旗,等方法不做任何事。
如果标志被清除,等待会阻塞,直到它再次成为集。
任意数量的线程可能等待相同的事件。
Python提供了Event对象用于线程间通信,它是由线程设置的信号标志,如果信号标志位真,则其他线程等待直到信号接触。
Event对象实现了简单的线程通信机制,它提供了设置信号,清楚信号,等待等用于实现线程间的通信。
1 设置信号
使用Event的set()方法可以设置Event对象内部的信号标志为真。Event对象提供了isSet()方法来判断其内部信号标志的状态。当使用event对象的set()方法后,isSet()方法返回真
2 清除信号
使用Event对象的clear()方法可以清除Event对象内部的信号标志,即将其设为假,当使用Event的clear方法后,isSet()方法返回假
3 等待
Event对象wait的方法只有在内部信号为真的时候才会很快的执行并完成返回。当Event对象的内部信号标志位假时,则wait方法一直等待到其为真时才返回。
事件处理的机制:全局定义了一个“Flag”,如果“Flag”值为 False,那么当程序执行 event.wait 方法时就会阻塞,如果“Flag”值为True,那么event.wait 方法时便不再阻塞。
clear:将“Flag”设置为False
set:将“Flag”设置为True
案例:
#!/usr/bin/env python
#codfing:utf-8
#__author__ = 'yaoyao'
import threading
def do(event):
print ('最先执行')
event.wait()
print ('最后执行')
event_obj = threading.Event()
for i in range(10):
t = threading.Thread(target=do, args=(event_obj,))
t.start()
print ('开始等待')
event_obj.clear()
inp = input('输入true:')
if inp == 'true':
event_obj.set()
queque队列:
队列是特别有用在线程编程时必须在多个线程之间交换安全信息。
class queue.Queue(maxsize=0) #先入先出
class queue.LifoQueue(maxsize=0) #last in fisrt out
class queue.PriorityQueue(maxsize=0) #存储数据时可设置优先级的队列
构造函数为一个优先队列。最大尺寸是整数集upperbound限制数量的物品可以放在队列中。插入块一旦达到这个尺寸,直到队列项。如果最大尺寸小于或等于零,队列大小是无限的。
生产者消费模型
二、多进程
案例:
#!/usr/bin/env python
#codfing:utf-8
from multiprocessing import Process
import threading
import time
def foo(i):
print ('开始',i)
if __name__ == "__main__":
for i in range(10):
p = Process(target=foo,args=(i,))
p.start()
print('我是华丽的分隔符')
注意:由于进程之间的数据需要各自持有一份,所以创建进程需要的非常大的开销。
进程数据共享
进程各自持有一份数据,默认无法共享数据
比如:
#!/usr/bin/env python
#codfing:utf-8
#__author__ = 'yaoyao'
from multiprocessing import Process
li = []
def foo(i):
li.append(i)
print ('进程里的列表是',li)
if __name__ == '__main__':
for i in range(10):
p = Process(target=foo,args=(i,))
p.start()
print ('打开列表 是空的',li)
显示如下:
打开列表 是空的 []
进程里的列表是 [0]
打开列表 是空的 []
进程里的列表是 [2]
打开列表 是空的 []
进程里的列表是 [3]
打开列表 是空的 []
进程里的列表是 [1]
打开列表 是空的 []
进程里的列表是 [5]
打开列表 是空的 []
进程里的列表是 [4]
打开列表 是空的 []
打开列表 是空的 []
进程里的列表是 [6]
打开列表 是空的 []
进程里的列表是 [7]
打开列表 是空的 []
进程里的列表是 [8]
打开列表 是空的 []
进程里的列表是 [9]
共享数据两种方法:
-
Array
!/usr/bin/env python
codfing:utf-8
author = 'yaoyao'
from multiprocessing import Process,Array
temp = Array('i', [11,22,33,44])
def Foo(i):
temp[i] = 100+i
for item in temp:
print (i,'----->',item)if name == "main":
for i in range(1):
p = Process(target=Foo,args=(i,))
p.start()
2.manage.dict()
协程
协程,又称微线程,纤程。英文名Coroutine。一句话说明什么是线程:协程是一种用户态的轻量级线程。
协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。因此:
协程能保留上一次调用时的状态(即所有局部状态的一个特定组合),每次过程重入时,就相当于进入上一次调用的状态,换种说法:进入上一次离开时所处逻辑流的位置。
协程的好处:
无需线程上下文切换的开销
无需原子操作锁定及同步的开销
方便切换控制流,简化编程模型
高并发+高扩展性+低成本:一个CPU支持上万的协程都不是问题。所以很适合用于高并发处理。
缺点:
无法利用多核资源:协程的本质是个单线程,它不能同时将 单个CPU 的多个核用上,协程需要和进程配合才能运行在多CPU上.当然我们日常所编写的绝大部分应用都没有这个必要,除非是cpu密集型应用。
进行阻塞(Blocking)操作(如IO时)会阻塞掉整个程序
使用yield实现协程操作例子
import time
import queue
def consumer(name):
print("--->starting eating baozi...")
while True:
new_baozi = yield
print("[%s] is eating baozi %s" % (name,new_baozi))
#time.sleep(1)
def producer():
r = con.__next__()
r = con2.__next__()
n = 0
while n < 5:
n +=1
con.send(n)
con2.send(n)
print("\033[32;1m[producer]\033[0m is making baozi %s" %n )
if name == 'main':
con = consumer("c1")
con2 = consumer("c2")
p = producer()
Greenlet
!/usr/bin/env python
-- coding:utf-8 --
from greenlet import greenlet
def test1():
print 12
gr2.switch()
print 34
gr2.switch()
def test2():
print 56
gr1.switch()
print 78
gr1 = greenlet(test1)
gr2 = greenlet(test2)
gr1.switch()
Gevent
Gevent 是一个第三方库,可以轻松通过gevent实现并发同步或异步编程,在gevent中用到的主要模式是Greenlet, 它是以C扩展模块形式接入Python的轻量级协程。 Greenlet全部运行在主程序操作系统进程的内部,但它们被协作式地调度。
import gevent
def foo():
print('Running in foo')
gevent.sleep(0)
print('Explicit context switch to foo again')
def bar():
print('Explicit context to bar')
gevent.sleep(0)
print('Implicit context switch back to bar')
gevent.joinall([
gevent.spawn(foo),
gevent.spawn(bar),
])
输出:
Running in foo
Explicit context to bar
Explicit context switch to foo again
Implicit context switch back to bar