您的位置:首页 > 编程语言 > Python开发

Python开发【第九章】:线程、进程和协程

2016-09-18 14:14 211 查看

一、线程

线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务

注:cpu内一个核数只能同时运行一个线程,所以多核cpu同时可以运行多个线程;但是在Python中,即使是多核cpu,同时运行的线程也只有一个,Python语言设计之初就不支持多核,所以在Python程序中,启用越多的线程,程序不一定运行的就很快,因为cpu要进行大量的上下文切换,反而消耗时间;GIL全局解释锁保障线程的上下文关系,保障当前只有一个线程在运行,与lock数据加锁无关

1、threading模块

线程创建有2种方式:如下

直接调用

import  threading,time

def run(n):
print("test...",n)
time.sleep(2)

if __name__ == '__main__':

t1 = threading.Thread(target=run,args=("t1",))
t2 = threading.Thread(target=run,args=("t2",))

# 两个同时执行,然后等待两秒程序结束
t1.start()
t2.start()

# 程序输出
# test... t1
# test... t2


继承式调用

import threading,time

class MyThread(threading.Thread):
def __init__(self,num):
# threading.Thread.__init__(self)
super(MyThread,self).__init__()
self.num =num

def run(self):#定义每个线程要运行的函数
print("running on number:%s" %self.num)
time.sleep(2)

if __name__ == '__main__':
# 两个同时执行,然后等待两秒程序结束
t1 = MyThread(1)
t2 = MyThread(2)
t1.start()
t2.start()

# 程序输出
# running on number:1
# running on number:2


2、join

join等待线程执行完后,其他线程再继续执行(串行)

import  threading,time

def run(n,sleep_time):
print("test...",n)
time.sleep(sleep_time)
print("test...done", n)
if __name__ == '__main__':

t1 = threading.Thread(target=run,args=("t1",2))
t2 = threading.Thread(target=run,args=("t2",3))

# 两个同时执行,然后等待t1执行完成后,主线程和子线程再开始执行
t1.start()
t2.start()
t1.join()   # 等待t1

print("main thread")

# 程序输出
# test... t1
# test... t2
# test...done t1
# main thread
# test...done t2


3、Daemon

Daemon守护进程,主程序执行完毕时,守护线程会同时退出,不管是否执行完任务

import threading,time

def run(n):
print('[%s]------running----\n' % n)
time.sleep(2)
print('--done--')

def main():
for i in range(5):
t = threading.Thread(target=run, args=[i, ])
t.start()
t.join(1)
print('starting thread', t.getName())

m = threading.Thread(target=main, args=[])
m.setDaemon(True)  # 将main线程设置为Daemon线程,它做为程序主线程的守护线程,当主线程退出时,
# m线程也会退出,由m启动的其它子线程会同时退出,不管是否执行完任务
m.start()
m.join(timeout=2)
print("---main thread done----")

# 程序输出
# [0]------running----
# starting thread Thread-2
# [1]------running----
# --done--
# ---main thread done----


 

4、Mutex 线程锁(互斥锁) 

一个进程下可以启动多个线程,多个线程共享父进程的内存空间,也就意味着每个线程可以访问同一份数据,此时,如果2个线程同时要修改同一份数据,会出现什么状况?

import time
import threading

def addNum():
global num  # 在每个线程中都获取这个全局变量
print('--get num:', num)
time.sleep(1)
num -= 1  # 对此公共变量进行-1操作

num = 100  # 设定一个共享变量
thread_list = []
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)


正常来讲,这个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)


机智的同学可能会问到这个问题,就是既然你之前说过了,Python已经有一个GIL来保证同一时间只能有一个线程来执行了,为什么这里还需要lock? 注意啦,这里的lock是用户级的lock,跟那个GIL没关系 ,具体我们通过下图来看一下

import socket

HOST = 'localhost'    # The remote host
PORT = 8001           # The same port as used by the server
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))
while True:
msg = bytes(input(">>:"),encoding="utf8")
s.sendall(msg)
data = s.recv(1024)
#print(data)

print('Received', repr(data))
s.close()


socket_client
我们在这里留一个问题,现在已经明白了异步的优点,当遇到I/0操作时会进行切换操作,那么程序是如何知道之前的I/O执行完毕再切换回来的呢?!

四、论事件驱动与异步IO

通常,我们写服务器处理模型的程序时,有以下几种模型:

(1)每收到一个请求,创建一个新的进程,来处理该请求;

(2)每收到一个请求,创建一个新的线程,来处理该请求;

(3)每收到一个请求,放入一个事件列表,让主进程通过非阻塞I/O方式来处理请求

上面的几种方式,各有千秋,

第(1)中方法,由于创建新的进程的开销比较大,所以,会导致服务器性能比较差,但实现比较简单。

第(2)种方式,由于要涉及到线程的同步,有可能会面临死锁等问题。

第(3)种方式,在写应用程序代码时,逻辑比前面两种都复杂。

综合考虑各方面因素,一般普遍认为第(3)种方式是大多数网络服务器采用的方式

看图说话讲事件驱动模型

在UI编程中,常常要对鼠标点击进行相应,首先如何获得鼠标点击呢?

方式一:创建一个线程,该线程一直循环检测是否有鼠标点击,那么这个方式有以下几个缺点:
1. CPU资源浪费,可能鼠标点击的频率非常小,但是扫描线程还是会一直循环检测,这会造成很多的CPU资源浪费;如果扫描鼠标点击的接口是阻塞的呢?
2. 如果是堵塞的,又会出现下面这样的问题,如果我们不但要扫描鼠标点击,还要扫描键盘是否按下,由于扫描鼠标时被堵塞了,那么可能永远不会去扫描键盘;
3. 如果一个循环需要扫描的设备非常多,这又会引来响应时间的问题;
所以,该方式是非常不好的

方式二:就是事件驱动模型
目前大部分的UI编程都是事件驱动模型,如很多UI平台都会提供onClick()事件,这个事件就代表鼠标按下事件。事件驱动模型大体思路如下:
1. 有一个事件(消息)队列;
2. 鼠标按下时,往这个队列中增加一个点击事件(消息);
3. 有个循环,不断从队列取出事件,根据不同的事件,调用不同的函数,如onClick()、onKeyDown()等;
4. 事件(消息)一般都各自保存各自的处理函数指针,这样,每个消息都有独立的处理函数;


  

事件驱动编程是一种编程范式,这里程序的执行流由外部事件来决定。它的特点是包含一个事件循环,当外部事件发生时使用回调机制来触发相应的处理。另外两种常见的编程范式是(单线程)同步以及多线程编程。

让我们用例子来比较和对比一下单线程、多线程以及事件驱动编程模型。下图展示了随着时间的推移,这三种模式下程序所做的工作。这个程序有3个任务需要完成,每个任务都在等待I/O操作时阻塞自身。阻塞在I/O操作上所花费的时间已经用灰色框标示出来了



在单线程同步模型中,任务按照顺序执行。如果某个任务因为I/O而阻塞,其他所有的任务都必须等待,直到它完成之后它们才能依次执行。这种明确的执行顺序和串行化处理的行为是很容易推断得出的。如果任务之间并没有互相依赖的关系,但仍然需要互相等待的话这就使得程序不必要的降低了运行速度。

在多线程版本中,这3个任务分别在独立的线程中执行。这些线程由操作系统来管理,在多处理器系统上可以并行处理,或者在单处理器系统上交错执行。这使得当某个线程阻塞在某个资源的同时其他线程得以继续执行。与完成类似功能的同步程序相比,这种方式更有效率,但程序员必须写代码来保护共享资源,防止其被多个线程同时访问。多线程程序更加难以推断,因为这类程序不得不通过线程同步机制如锁、可重入函数、线程局部存储或者其他机制来处理线程安全问题,如果实现不当就会导致出现微妙且令人痛不欲生的bug。

在事件驱动版本的程序中,3个任务交错执行,但仍然在一个单独的线程控制中。当处理I/O或者其他昂贵的操作时,注册一个回调到事件循环中,然后当I/O操作完成时继续执行。回调描述了该如何处理某个事件。事件循环轮询所有的事件,当事件到来时将它们分配给等待处理事件的回调函数。这种方式让程序尽可能的得以执行而不需要用到额外的线程。事件驱动型程序比多线程程序更容易推断出行为,因为程序员不需要关心线程安全问题。

当我们面对如下的环境时,事件驱动模型通常是一个好的选择:

程序中有许多任务,而且…

任务之间高度独立(因此它们不需要互相通信,或者等待彼此)而且…

在等待事件到来时,某些任务会阻塞。

当应用程序需要在任务间共享可变的数据时,这也是一个不错的选择,因为这里不需要采用同步处理。

网络应用程序通常都有上述这些特点,这使得它们能够很好的契合事件驱动编程模型。

总结:异步IO涉及到了事件驱动模型,进程中维护一个消息队列,当客户端又请求时,就会把请求添加到消息队列中,线程从消息队列中轮询取要处理的请求,遇到I/O阻塞时(操作系统处理调用I/O接口处理,与程序无关),则进行上下文切换,处理其他请求,当I/O操作完成时,调用回调函数,告诉线程处理完成,然后再切换回来,处理完成后返回给客户端 Nginx能处理高并发就是用的这个原理

Select\Poll\Epoll异步IO

Select\Poll\Epoll详解: http://www.cnblogs.com/lianzhilei/p/5843277.html

IO多路复用、异步IO详解: http://www.cnblogs.com/lianzhilei/p/5955526.html

  

进程与线程详细对比-》 点击
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: