Python - 线程、进程和协程

 线程和进程

 

1. 线程

进程(英语:process),是计算机中已运行程序的实体。进程为曾经是分时系统的基本运作单位。在面向进程设计的系统(如早期的UNIX,Linux 2.4及更早的版本)中,进程是程序的基本执行实体;在面向线程设计的系统(如当代多数操作系统、Linux 2.6及更新的版本)中,进程本身不是基本运行单位,而是线程的容器。程序本身只是指令、数据及其组织形式的描述,进程才是程序(那些指令和数据)的真正运行实例。–维基百科

 

Threading用于提供线程相关的操作,线程是应用程序中工作的最小单元。

多任务可以由多进程完成,也可以由一个进程内的多线程完成,一个进程内的所有线程,共享同一块内存python中创建线程比较简单,导入threading模块,下面来看一下代码中如何创建多线程。

import threading
import time

def show(arg):
    time.sleep(1)
    print('thread'+str(arg))

for i in range(5): #创建5个线程
    t = threading.Thread(target=show, args=(i+1,))
    t.start()     # 主线程等待子线程完成,子线程并发执行

print('main thread stop')

 >>>

 main thread stop
 thread2
 thread3
 thread1
 thread4
 thread5

 

主线程从上到下执行,创建5个子线程,打印出'start',然后等待子线程执行完结束,如果想让线程要一个个依次执行完,而不是并发操作,那么就要使用.join()方法。

def f1(i):
    time.sleep(1)
    print(i)

if __name__ == '__main__':

    for i in range(5):
        t = threading.Thread(target=f1, args=(i,))

        t.start()
        t.join() #使用join()取消并发执行效果
    print('start')
>>>

  0
  1
  2
  3
  4
  start

 

上面的代码不适用join的话,主线程会默认等待子线程结束,才会结束,如果不想让主线程等待子线程的话,可以子线程启动之前设置将其设置为后台线程,如果是后台线程,主线程执行过程中,后台线程也在进行,主线程执行完毕后,后台线程不论成功与否,均停止,前台线程则相反,若果不加指定的话,默认为前台线程,下面从代码来看一下,如何设置为后台线程。例如下面的例子,主线程直接打印start,执行完后就结束,而不会去等待子线程,子线程中的数据也就不会打印出来 

def f1(i):
    time.sleep(1)
    print(i)

if __name__ == '__main__':

    for i in range(5):
        t = threading.Thread(target=f1, args=(i,))
        t.setDaemon(True) #不等待子线程执行完毕,执行主线程
        t.start()

    print('start')

>>>
    start

 

除此之外,自己还可以为线程自定义名字,通过 t = threading.Thread(target=f1, args=(i,), name='mythread{}'.format(i)) 中的name参数,除此之外,Thread还有一下一些方法 

 

    • start             线程准备就绪,等待CPU调度
    • setName        为线程设置名称
    • getName        获取线程名称
    • setDaemon    设置为后台线程或前台线程(默认False)
                          False 如果是后台线程,主线程执行过程中,后台线程也在进行,主线程执行完毕后,后台线程不论成功与否,均停止
                          True 如果是前台线程,主线程执行过程中,前台线程也在进行,主线程执行完毕后,等待前台线程也执行完成后,程序停止
    • join               逐个执行每个线程,执行完毕后继续往下执行,该方法使得多线程变得无意义
    • run               线程被cpu调度后自动执行线程对象的run方法
    • t.is_alive        判断线程是否为激活状态
    • t.isAlive         判断线程是否为激活状态
#自定义线程类

import threading
import time


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

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

if __name__ == '__main__':
    t1 = MyThread(1)
    t2 = MyThread(2)
    t1.start()
    t2.start()

 

2.线程锁

由于线程是共享同一份内存的,所以如果操作同一份数据,很容易造成冲突,这时候就可以为线程加上一个锁了,这里我们使用Rlock,而不使用Lock,因为Lock如果多次获取锁的时候会出错,而RLock允许在同一线程中被多次acquire,但是需要用n次的release才能真正释放所占用的琐,一个线程获取了锁在释放之前,其他线程只有等待。

 

import threading
import time

globals_sum = 0

lock = threading.RLock()#实例化线程锁

"""
A reentrant lock must be released by the thread that acquired it. Once a
thread has acquired a reentrant lock, the same thread may acquire it again
without blocking; the thread must release it once for each time it has
acquired it.
"""

def Func():
    lock.acquire()#获取锁,锁定线程
    global globals_sum
    globals_sum += 10
    time.sleep(1)
    print(globals_sum)
    lock.release()#释放锁,释放线程,让下一个线程进入,执行程序

for i in range(10):#创建10个线程
    t = threading.Thread(target=Func)
    t.start()

>>>
10
20
30
40
50
60
70
80
90
100

 

3. 线程间通信Event

Event是线程间通信最简单的机制,主要用于主线程控制其他线程的执行,主要用过wait()阻止线程,clear()设定线程旗False,set()设定线程旗True,这三个方法来实现。

import threading
"""
Events manage a flag that can be set to true with the set() method and reset
to false with the clear() method. The wait() method blocks until the flag is
true.  The flag is initially false.
"""

def do(event):
    print('start')
    event.wait()#event.wait()会阻止所有线程执行flag = False
    print('execute')


obj_event = threading.Event() #创建event

for i in range(5):
    t = threading.Thread(target=do, args=(obj_event, ))
    t.start()

obj_event.clear() #Reset the internal flag to false.
inp = input("input: ")
if inp == 'True':
    obj_event.set() #Set the internal flag to true,打开event.wait()阻止的线程

>>>
start
start
start
start
start
input: True
execute
execute
execute
execute
execute

 

4.队列  

可以简单的理解为一种先进先出的数据结构,比如用于生产者消费者模型,或者用于写线程池,以及前面写select的时候,读写分离时候可用队列存储数据等等,以后用到队列的地方很多,因此对于队列的用法要熟练掌握。下面首先来看一下队列提供了哪些用法

q = queue.Queue(maxsize=0)  # 构造一个先进显出队列,maxsize指定队列长度,为0时,表示队列长度无限制。

q.join()        # 等到队列为kong的时候,在执行别的操作
q.qsize()       # 返回队列的大小 (不可靠)
q.empty()       # 当队列为空的时候,返回True 否则返回False (不可靠)
q.full()        # 当队列满的时候,返回True,否则返回False (不可靠)
q.put(item, block=True, timeout=None)   # 将item放入Queue尾部,item必须存在,参数block默认为True,表示当队列满时,会等待                         # 为False时为非阻塞,此时如果队列已满,会引发queue.Full 异常。 可选参数timeout,表示会阻塞设置的时间,                         # 如果在阻塞时间里 队列还是无法放入,则引发 queue.Full 异常

q.get(block=True, timeout=None)      # 移除并返回队列头部的一个值,可选参数block默认为True,表示获取值的时候,如果队列为空,则阻塞                        # 阻塞的话若此时队列为空,则引发queue.Empty异常。 可选参数timeout,表示会阻塞设置的时间.

q.get_nowait()                # 等效于 get(item,block=False) 

 

消费者生成者模型

import queue
import random
import time
import threading

message = queue.Queue(10) #Create a queue object with a given maximum size.

def product(num):
    for i in range(num):
        message.put(i) #Put an item into the queue.
        print('将{}添加到队列中'.format(i))
        time.sleep(random.randrange(0, 1))

def consume(num):
    count = 0
    while count < num:
        i = message.get() #Remove and return an item from the queue.
        print('将{}从队列取出'.format(i))
        time.sleep(random.randrange(1, 2))
        count += 1

t1 = threading.Thread(target=product, args=(10,))
t1.start()

t2 = threading.Thread(target=consume, args=(10,))
t2.start()

 

2. 进程

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

mutiprocessing.Process

from multiprocessing import Process
import threading
import time

def foo(i):
    print 'say hi',i

for i in range(10):
    p = Process(target=foo,args=(i,))#创建进程
    p.start()

注意:由于进程之间的数据需要各自持有一份,所以创建进程需要的非常大的开销。

 

进程数据共享

进程各自持有一份数据,默认无法共享数据

from multiprocessing import Process
from multiprocessing import Manager
 
import time
 
li = []
 
def foo(i):
    li.append(i)
    print ('say hi',li)
  
if __name__ == "__main__":
    for i in range(10):
        p = Process(target=foo, args=(i,))
        p.start()

    print('ending', li)
进程间默认无法数据共享

mutiprocessing.Manager.dict( )实现数据共享

当创建进程时(非使用时),共享数据会被拿到子进程中,当进程中执行完毕后,再赋值给原值。

from multiprocessing import Process, Manager

def Foo(i, dic):
    dic[i] = 100 + i            #i = 0        i = 1
    for k, v in dic.items():
        print(k, v)             #{0:100}      {1:101}

if __name__ == "__main__":
    manage = Manager()
    # dic = manage.dict()
    dic = {}
    for i in range(2): # i=0    i =1
        p = Process(target=Foo, args=(i, dic, ))
        p.start()
        p.join()
>>>
0 100
1 101
dic={}
from multiprocessing import Manager
from multiprocessing import Process

#进程不共享内存,每个进程都创建一个dic,range(0), range(1)分别分配到一个进程里
#输出两个不同的字典

def Foo(i, dic):
                                #i = 0              i = 1
    dic[i] = 100+i              #dic[0]=100+0       dic[0]=100+0
    for k, v in dic.items():   #                   dic[1]=100+1
        print(k, v)
    print(len(dic))#输出字典的长度

if __name__ == "__main__":
    manage = Manager()
    dic = manage.dict()         #0 100               #0 100
                                                     #1 101

    for i in range(2):     #i =0        #i =1
        p = Process(target=Foo, args=(i, dic))#每个进程传进i, dic
        p.start()
        p.join()
manage.dict()
#类型对应表
'c': ctypes.c_char,  'u': ctypes.c_wchar,
'b': ctypes.c_byte,  'B': ctypes.c_ubyte,
'h': ctypes.c_short, 'H': ctypes.c_ushort,
'i': ctypes.c_int,   'I': ctypes.c_uint,
'l': ctypes.c_long,  'L': ctypes.c_ulong,
'f': ctypes.c_float, 'd': ctypes.c_double

 

进程锁

.Rlock()

#!/usr/bin/env python
# -*- coding:utf-8 -*-

from multiprocessing import Process, Array, RLock

def Foo(lock,temp,i):
    """
    将第0个数加100
    """
    lock.acquire()
    temp[0] = 100+i
    for item in temp:
        print i,'----->',item
    lock.release()

lock = RLock()
temp = Array('i', [11, 22, 33, 44])

for i in range(20):
    p = Process(target=Foo,args=(lock,temp,i,))
    p.start()
进程锁

 

进程池

进程池内部维护一个进程序列,当使用时,则去进程池中获取一个进程,如果进程池序列中没有可供使用的进进程,那么程序就会等待,直到进程池中有可用进程为止。

进程池中有两个方法:

  • apply 每一个任务都是排队进行; 进程.join()
  • apply_async 每一个任务都是并发进行,可以设置回调函数callback,返回调用函数的返回值,无进程.join(),进程daemon=True

from multiprocessing import Pool
import time

def f1(a):
    time .sleep(1)
    print(a)


if __name__ == "__main__":
    pool = Pool(5)
    for i in range(40):
        # 每个任务排队进行
        ret = pool.apply(func=f1, args=(i,))
    pool.close()
    pool.join()
apply
from multiprocessing import Pool
import time

def f1(a):
    time .sleep(1)
    print(a)
    return("finished")

def f2(args):
    print(args)

if __name__ == "__main__":
    pool = Pool(5)
    for i in range(40):
        # 每个任务并发进行,可设置回调函数callback,返回func函数的返回值
        ret = pool.apply_async(func=f1, args=(i,), callback=f2)
        print("ready")
    pool.close()
    pool.join()
apply_async

 

3. 协程

协程,又称微线程,纤程。英文名Coroutine。

协程可以用来做什么?

描述逻辑:我主要把协程用来描述逻辑。一个流程可能需要调用多个接口,其中很多接口是异步的。这样描述起来会困难一点。用线程是可以解决部分问题,但是复杂度提升。

提高并发:主要应用在IO密集型应用中。gevent就是在greenlet基础之上的一个处理并发的框架,和上面的区别是,这里的事件及接口是IO接口。

协程存在的意义:对于多线程应用,CPU通过切片的方式来切换线程间的执行,线程切换时需要耗时(保存状态,下次继续)。协程,则只使用一个线程,在一个线程中规定某个代码块执行顺序。

协程的适用场景:当程序中存在大量不需要CPU的操作时(IO),适用于协程;

 

greenlet

greenlet这个库能随时记录代码运行现场,并随时终止,以及恢复,跟yield很相似。但greenlet能更好提供从一个greenlet对象切换到另一个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()

解析器读取文件,gr1.switch()跳到test1,输出12,跳到test2, 输出56, 跳到test1,输出34; 然后test1执行完, gr1执行完后,gr1.switch()调回返回值,所以不会输出78。

 

gevent

gevent就是利用greenlet实现的基于协程的python的网络library.

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),
])

 

遇到IO操作自动切换:

from gevent import monkey; monkey.patch_all()
import gevent
import urllib2

def f(url):
    print('GET: %s' % url)
    resp = urllib2.urlopen(url)
    data = resp.read()
    print('%d bytes received from %s.' % (len(data), url))

gevent.joinall([
        gevent.spawn(f, 'https://www.python.org/'),
        gevent.spawn(f, 'https://www.yahoo.com/'),
        gevent.spawn(f, 'https://github.com/'),
])

 

总结一下:
1)多进程能够利用多核优势,但是进程间通信比较麻烦,另外,进程数目的增加会使性能下降,进程切换的成本较高。程序流程复杂度相对I/O多路复用要低。
2)I/O多路复用是在一个进程内部处理多个逻辑流程,不用进行进程切换,性能较高,另外流程间共享信息简单。但是无法利用多核优势,另外,程序流程被事件处理切割成一个个小块,程序比较复杂,难于理解。
3)线程运行在一个进程内部,由操作系统调度,切换成本较低,另外,他们共享进程的虚拟地址空间,线程间共享信息简单。但是线程安全问题导致线程学习曲线陡峭,而且易出错。
4)协程有编程语言提供,由程序员控制进行切换,所以没有线程安全问题,可以用来处理状态机,并发请求等。但是无法利用多核优势。
上面的四种方案可以配合使用,我比较看好的是进程+协程的模式。

 

posted @ 2016-07-17 11:40  sam_r  阅读(56)  评论(0)    收藏  举报