多进程、多线程

多进程、多线程

线程和进程的区别

  • 线程共享内存空间;进程的内存是独立的
  • 同一个进程的线程之间可以直接交流;两个进程想通信,必须通过一个中间代理来实现
  • 创建新进程很简单;创建新进程需要对其父进程进行一个克隆
  • 一个线程可以控制和操作同一进程里的其他线程;但是进程只能操作子进程
  • 改变注线程(如优先权),可能会影响其他线程;改变父进程,不影响子进程

python GIL(Global Interpreter Lock)

python GIL 称为 python全局解释器锁,表示无论你启动多少个线程,你有多少个cpu,Python在执行的时候都只会在同一时刻只允许一个线程运行。

需要明确的一点是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

因此,这种伪多线程的情况在Cpython解释器中是存在的,但在其他解释器就可能不存在,如Jpython。因此:GIL并不是python的特性,Python完全可以不依赖于GIL
参考

线程

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

一个标准的线程有线程ID、当前指令指针(PC),寄存器集合和堆栈组成。另外,线程是进程中的一个实体,是被系统独立调度和分派的基本单元,线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其他线程共享进程所拥有的全部资源。一个线程可以创建和撤销另一个线程,同一进程中的多个线程之间可以并发执行。由于线程之间的相互制约,致使线程在运行中呈现处间断性。

线程也有就绪、阻塞和运行三种基本状态。就绪状态是指线程具备运行的所有条件,逻辑上可以运行,在等待处理机;运行状态是指线程占有处理机正在运行;阻塞状态是指线程在等待一个事件(如某个信号量),逻辑上不可执行。每一个程序都至少有一个线程,若程序只有一个线程,那就是程序本身。

线程是程序中一个单一的顺序控制流程。进程内一个相对独立的、可调度的执行单元,是系统独立调度和分派CPU的基本单元。在单一程序中同时运行多个想成完成不同的工作,称为多线程。

python的标准库提供了两个模块:threadthreading,thread是低级模块,threading是高级模块,对thread进行了封装。绝大多数情况下,我们只需要使用threading这个高级模块。

启动一个线程就是把一个函数传入并创建Thread实例,然后调用start()开始执行:

import time, threading

# 新线程执行的代码:
def loop():
    print 'thread %s is running...' % threading.current_thread().name
    n = 0
    while n < 5:
        n = n + 1
        print 'thread %s >>> %s' % (threading.current_thread().name, n)
        time.sleep(1)
    print 'thread %s ended.' % threading.current_thread().name

print 'thread %s is running...' % threading.current_thread().name
t = threading.Thread(target=loop, name='LoopThread')
t.start()
t.join()
print 'thread %s ended.' % threading.current_thread().name

执行结果如下:

thread MainThread is running...
thread LoopThread is running...
thread LoopThread >>> 1
thread LoopThread >>> 2
thread LoopThread >>> 3
thread LoopThread >>> 4
thread LoopThread >>> 5
thread LoopThread ended.
thread MainThread ended.

由于任何进程默认就会启动一个线程,我们把该线程称为主线程,主线程又可以启动新的线程,Python的threading模块有个current_thread()函数,它永远返回当前线程的实例。主线程实例的名字叫MainThread,子线程的名字在创建时指定,我们用LoopThread命名子线程。名字仅仅在打印时用来显示,完全没有其他意义,如果不起名字Python就自动给线程命名为Thread-1,Thread-2……

线程锁

多线程和多进程最大的不同在于,多进程中,同一个变量,各自有一份拷贝存在于每个进程中,互不影响,而多线程中,所有变量都由所有线程共享,所以,任何一个变量都可以被任何一个线程修改,因此,线程之间共享数据最大的危险在于多个线程同时改一个变量,把内容给改乱了。

来看看多个线程同时操作一个变量怎么把内容给改乱了:

import time, threading

# 假定这是你的银行存款:
balance = 0

def change_it(n):
    # 先存后取,结果应该为0:
    global balance
    balance = balance + n
    balance = balance - n

def run_thread(n):
    for i in range(100000):
        change_it(n)

t1 = threading.Thread(target=run_thread, args=(5,))
t2 = threading.Thread(target=run_thread, args=(8,))
t1.start()
t2.start()
t1.join()
t2.join()
print balance

我们定义了一个共享变量balance,初始值为0,并且启动两个线程,先存后取,理论上结果应该为0,但是,由于线程的调度是由操作系统决定的,当t1、t2交替执行时,只要循环次数足够多,balance的结果就不一定是0了。

如果我们要确保balance计算正确,就要给change_it()上一把锁,当某个线程开始执行change_it()时,我们说,该线程因为获得了锁,因此其他线程不能同时执行change_it(),只能等待,直到锁被释放后,获得该锁以后才能改。由于锁只有一个,无论多少线程,同一时刻最多只有一个线程持有该锁,所以,不会造成修改的冲突。创建一个锁就是通过threading.Lock()来实现:

balance = 0
lock = threading.Lock()

def run_thread(n):
    for i in range(100000):
        # 先要获取锁:
        lock.acquire()
        try:
            # 放心地改吧:
            change_it(n)
        finally:
            # 改完了一定要释放锁:
            lock.release()

当多个线程同时执行lock.acquire()时,只有一个线程能成功地获取锁,然后继续执行代码,其他线程就继续等待直到获得锁为止。

获得锁的线程用完后一定要释放锁,否则那些苦苦等待锁的线程将永远等待下去,成为死线程。所以我们用try...finally来确保锁一定会被释放。

锁的好处就是确保了某段关键代码只能由一个线程从头到尾完整地执行,坏处当然也很多,首先是阻止了多线程并发执行,包含锁的某段代码实际上只能以单线程模式执行,效率就大大地下降了。其次,由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对方持有的锁时,可能会造成死锁,导致多个线程全部挂起,既不能执行,也无法结束,只能靠操作系统强制终止。

进程

进程是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单元,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据机器组织形式的描述,进程是程序的实体。里面包含对各种资源的调用,内存的管理,网络接口的调用等。

Python实现多进程

import multiprocessing
 5 import time,threading
 6 
 7 def thread_id():
 8     """获得线程ID。"""
 9     print(" thread..")
10     print("thread_id:%s\n" % threading.get_ident())
11 
12 def hello(name):
13     time.sleep(2)
14     print("hello %s..." % name)
15     # 启一个线程
16     t = threading.Thread(target=thread_id,)
17     t.start()
18 
19 if __name__ == "__main__":            # windows环境下必须写这句,不写会报错
20     for i in range(10):
21         # 启一个进程和一个线程的语法都差不多
22         p = multiprocessing.Process(target=hello,args=("progress %s" % i,))
23         p.start()


Python多进程锁

当多个进程需要访问共享资源的时候,如对同一个文件进行写操作的时候,Lock可以用开避免访问的冲突。如多进程情况下,多个进程抢占屏幕导致输出信息混杂就是没有加锁的缘故。

示例:一个进程对一个值+1,一个进程对一个值+3

如果在没有加锁的情况下:

import multiprocessing
import time


def add(number, change_number, lock):
    # with lock:
    for i in range(5):
        number += change_number
        print "add {0} The number is {1}".format(change_number,number)
        time.sleep(1)       #如果不等待的话,将看不到效果
    print number

if __name__ == "__main__":
    init_number = 0
    process_lock = multiprocessing.Lock()
    p1 = multiprocessing.Process(target=add, args=(init_number, 1, process_lock))
    p2 = multiprocessing.Process(target=add, args=(init_number, 3, process_lock))
    p1.start()
    p2.start()
    # print "Execute finished!"

上面那个例子中,我们没有对循环加锁执行加法运算,两个进程的变量处于不同的命名空间中,相互不影响。进程p1的init_number 不会对进程p2的 init_number产生影响。

结果如下:

add 3 The number is 3
add 1 The number is 1
add 3 The number is 6
add 1 The number is 2
add 3 The number is 9
add 1 The number is 3
add 3 The number is 12
add 1 The number is 4
add 3 The number is 15
add 1 The number is 5
15
5

可以看到连个进程交叉执行运算,这儿只执行了5次,如果执行次数很多将会产生输出内容抢占屏幕的情况。

如果对上面的循环加锁的话:

import multiprocessing
import time


def add(number, change_number, lock):
    with lock:
        for i in range(5):
            number += change_number
            print "add {0} The number is {1}".format(change_number,number)
            time.sleep(1)
        print number

    #也可以这样写
    # lock.acquire()
    # for i in range(5):
    #     number += change_number
    #     print "add {0} The number is {1}".format(change_number,number)
    #     time.sleep(1)
    # print number
    # lock.release()

if __name__ == "__main__":
    init_number = 0
    process_lock = multiprocessing.Lock()
    p1 = multiprocessing.Process(target=add, args=(init_number, 1, process_lock))
    p2 = multiprocessing.Process(target=add, args=(init_number, 3, process_lock))
    p1.start()
    p2.start()
    # print "Execute finished!"

输出结果如下:

add 1 The number is 1
add 1 The number is 2
add 1 The number is 3
add 1 The number is 4
add 1 The number is 5
5
add 3 The number is 3
add 3 The number is 6
add 3 The number is 9
add 3 The number is 12
add 3 The number is 15
15

可以看到,执行开始变得有序了,先执行完p1进程,后执行p2进程。

Lock和join的区别

Lock和join都可以使进程阻塞,只让一个执行完后其他的执行。但是,不同的是,Lock是有一个抢占的过程的,哪一个进程抢占到这个锁,就可以执行,进程之间的执行顺序是不确定的。而join是人为的编排了执行顺序,必须等到join的那个进程执行完后才能执行其他的进程。有时候的确有这个需求,其他的进程需要前面进程的返回结果。

进程之间通信

Queue

多个子进程间的通信就要采用Queue,比如,一个子进程项队列中些数据,另外一个进程从队列中取数据。

from multiprocessing import Process, Queue
import time


# 写数据
def write(q):
    for value in range(10):
        print "put {0} to queue".format(value)
        q.put(value)
        time.sleep(1)

# 读数据
def read(q):
    while True:
        if not q.empty():
            value = q.get()
            print "get {0} from queue".format(value)
            time.sleep(2)
        else:
            break

if __name__ == '__main__':
    q = Queue()
    t1 = Process(target=write, args=(q,))
    t2 = Process(target=read, args=(q,))

    t1.start()
    t2.start()
    t1.join()
    t2.join()

执行结果:

put 0 to queue
get 0 from queue
put 1 to queue
put 2 to queue
get 1 from queue
put 3 to queue
put 4 to queue
get 2 from queue
put 5 to queue
put 6 to queue
get 3 from queue
put 7 to queue
put 8 to queue
get 4 from queue
put 9 to queue
get 5 from queue
get 6 from queue
get 7 from queue
get 8 from queue
get 9 from queue

Pipe

multiprocess.Pipe([duplex])
返回2个连接对象(conn1,conn2),代表管道的两端,默认是双向通信,如果duplex=False,conn1只能用来接受消息,conn2只能用开发送消息,不同与os.open之处在于os.pipe()返回2个文件描述符(r,w)表示可读的和可写的。

示例:

from multiprocessing import Process, Pipe
import time

def send(p):
    for value in range(10):
        p.send(value)
        print "send {0} to pipe".format(value)
        time.sleep(1)

def read(p):
    while True:
        data = p.recv()
        print "recv {0} from pipe".format(data)
        if data >= 9:
            break
        time.sleep(1)

if __name__ == '__main__':
    pi = Pipe(duplex=False)

    # pi[0]和pi[1]在duplex=False的时候顺序很重要,如果duplex=True的时候就表示无所谓,duplex=True表示双全工模式。两端都可以发送和接收数据。而duplex=False的时候就只能一端发,一端收。
    t1 = Process(target=send, args=(pi[1],))
    t2 = Process(target=read, args=(pi[0],))

    t1.start()
    t2.start()

    t2.join()

结果如下:

send 0 to pipe
recv 0 from pipe
send 1 to pipe
recv 1 from pipe
send 2 to pipe
recv 2 from pipe
send 3 to pipe
recv 3 from pipe
send 4 to pipe
recv 4 from pipe
send 5 to pipe
recv 5 from pipe
send 6 to pipe
recv 6 from pipe
send 7 to pipe
recv 7 from pipe
send 8 to pipe
recv 8 from pipe
send 9 to pipe
recv 9 from pipe

进程之间数据共享

Pipe、Queue都有一定数据共享的功能,但是他们会阻塞进程,这里介绍两种数据共享方式都不会阻塞进程,而且都死多进程安全的。

共享内存

共享内存(Shared Memory)是最简单的进程间通信方式,它允许多个进程访问相同的内存,一个进程改变其中的数据后,其他的进程都可以看到数据的变化。即进程间可以相互通信。

共享内存有两个结构,一个是Value,一个是Arrary,这两个结构内部都实现了锁机制,因此是多进程安全的。用法如下:

# 对进程共享内存实现

from multiprocessing import Process, Value, Array

def func(a):
    # n.value = 50
    for i in range(len(a)):
        a[i] += 10
    print a[:]

def func1(n, a):
    # t_list = []
    t_list = n[:] + a[:]
    print t_list

if __name__ == "__main__":
    num = Array('i', range(10, 20))
    ints = Array('i', range(10))

    p1 = Process(target=func, args=(ints,))
    p2 = Process(target=func1, args=(num, ints))
    p1.start()
    p1.join()   #先让p1执行完,获取执行完后的ints,之后将新的ints放到p2中执行
    p2.start()

结果如下:

# p1的结果
[10, 11, 12, 13, 14, 15, 16, 17, 18, 19]

# p2的结果
[10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]

服务进程Manager

上面的共享内存支持两种结构Value和Array,这些值在主进程中管理,很分散。Python中还有一统天下,无所不能的Server process,专门用来做数据共享。其支持的类型非常多,比如list,dict,Namespace,Lock,RLock,Semaphore,BoundedSemaphore,Condition,Event,Queue,Value和Array。

用法如下:

from multiprocessing import Process, Manager

def func(dct, lst):
    dct[1] = 2
    lst.reverse()

if __name__ == "__main__":
    manager = Manager()
    dct = manager.dict()
    lst = manager.list(range(1, 10))

    p = Process(target=func, args=(dct, lst))
    p.start()
    p.join()

    print dct, lst

一个Manager对象是一个服务进程,推荐多进程程序中,数据共享就用一个 manager 管理。

进程池

如果有50个任务要执行,但是CPU只有四核,你可以创建50个进程来做这个事情?大可不必。如果你只想创建4个进程,让他们轮流替你完成任务,不用自己去管理具体的进程的差un关键销毁,那就可以使用进程池Pool。

Pool 是进程池,进程池能够管理一定的进程,当有空闲进程时,则利用空闲进程完成任务,知道所有任务完成为止,用法如下:

from multiprocessing import Process,Pool
def func(x):
    return x*x
    
pool = Pool(processes=4)

print pool.map(func,range(8))

Pool进程池创建4个进程,不管有没有任务,都一直在进程池中等待,等到有数据的时候就开始执行。

Pool 的API列表如下

  • apply(func[,args[,kwds]])
  • apply_async(func[,args[,kwds[,callback]]])
  • map(func, iterable[, chunksize])
  • map_async(func, iterable[, chunksize[, callback]])
  • imap(func, iterable[, chunksize])
  • imap_unordered(func, iterable[, chunksize])
  • close()
  • terminate()
  • join()

异步执行

apply_async 和 map_async 执行之后立即返回,然后异步返回结果。使用方法如下:

from multiprocessing import Process, Pool

def func(x):
    return x*x

def callback(x):
    print x , "in callback"

if __name__ == "__main__":
    pool = Pool(processes=4)
    result = pool.map_async(func, range(8), 8, callback)
    print result.get(), "in main"

callback 是在结果返回之前,调用的一个函数,这个函数必须只有一个参数,它会首先接收到结果。callback不能有耗时操作,因为它会阻塞主线程。

AsyncResult是获取结果的对象,其API如下:

  • get([timeout])
  • wait([timeout])
  • ready()
  • successful()

如果设置了timeout时间,超时会抛出 multiprocessing.TimeoutError 异常。wait是等待执行完成。ready测试是否已经完成,successful实在确定已经ready的情况下,如果执行中没有抛出异常,则成功,如果没有ready就调用该函数,会得到一个AssertionError异常。

Pool管理

Pool的执行流程,有三个阶段:
1、一个进程池接受很多任务,然后分开执行任务
2、当进程池中没有进程可以使用,则任务排队
3、所有进程执行完成,关闭连接池,完成进程

这就是上面的方法,close停止接受新的任务,如果还有任务来,就会抛出异常。join是等待所有任务完成。join必须要在close之后调用,否则会抛出异常。terminate非正常终止,内存不够用是,垃圾回收器调用的就是这个方法。

为什么在Python里推荐使用多进程而不是多线程

参考地址

最近在看Python的多线程,经常我们会听到老手说:“Python下多线程是鸡肋,推荐使用多进程!”,但是为什么这么说呢?

要知其然,更要知其所以然。所以有了下面的深入研究:

首先强调背景:

  • GIL是什么

GIL的全称为Global Interpreter Lock(全局解释器锁),来源是Python设计之初的考虑,为了数据安全所做的决定。

  • 每个CPU在同一时间只能执行一个线程

在单核CPU下的多线程其实都只是并发,不是并行,并发和并行从宏观上来讲都是同时处理多路请求的概念。但并发和并行又有区别,并行是指两个或者多个时间在同一时刻发生,二并发是指两个或多个事件在同一时间间隔内发生。

在Python多线程下,每个线程的执行方式:

  • 获取GIL
  • 执行代码知道sleep或者是python虚拟机将其挂起
  • 释放GIL

可见,某个线程想要执行,必须先拿到GIL,我们可以把GIL看作是“通行证”,并且在一个Python进程中,GIL只有一个。拿不到通行证的线程,就不允许进入CPU执行。

在python2.x里,GIL的释放逻辑是当前线程遇见IO操作或者ticks计数达到100(ticks可以看作是python自身的一个计数器,专门作用于GIL,每次释放后归零,这个计数可以通过sys.setcheckinterval来调整),进行释放

而每次释放GIL锁,线程进行锁竞争、切换线程,会消耗资源。并且由于GIL锁存在,python里一个进程永远只能同时执行一个线程(拿到GIL的线程才能执行),这就是为什么在多核CPU上,python的多线程效率并不高。

那么是不是python的多线程就完全没用了呢?

1、CPU密集型代码(各种循环处理,计数等等),在这种情况下,由于计算工作多,ticks计数很快就会达到阀值,然后触发GIL的释放与在竞争(多个线程来回切换当然是需要消耗资源的),所以python下的多线程对CPU密集型代码并不友好。

2、IO密集型代码(文件处理、网络爬虫),多线程能够有效提升效率(单线程下有IO操作会进行IO等待,造成不必要的时间浪费,而开启多线程能在线程A等待是,自动切换到线程B,可以不浪费CPU的资源,从而能提升程序执行效率)。所以python的多线程对IO秘籍型代码比较友好。

而在python3.x中,GIL不实用ticks奇数,改为使用计时器(执行时间达到阀值后,当前线程释放GIL),这样相对python2.x而言,对CPU密集型程序更加友好。单依然没有解决GIL导致的同一时间只能执行一个线程的问题。所以效率依然不尽如人意。

请注意:
多核多线程比单核多线程更差,原因是单核下的多线程,每次释放GIL,唤醒的那个线程都能获取到GIL锁,所以能够无缝执行,但多核下,CPU0释放GIL后,其他CPU上的线程都会进行竞争,但GIL可能会马上又被CPU0拿到,导致其他几个CPU上被唤醒后的线程会醒着等待到切换时间后又进入待调度状态,这样会造成线程颠簸(thrashing),导致效率更低。

回到最开始的问题:

原因是:每个进程有各自独立的GIL,互不干扰,这样就可以真正意义上的并发执行,所以在python中,多进程的执行效率由于多线程(仅仅针对多核CPU而言)

所以在这里说结论:多核下,想做并行提升效率,比较通用的方法是使用多进程,能够有效提高执行效率。

参考

posted @ 2017-12-18 23:38  PING1  阅读(2213)  评论(0编辑  收藏  举报