标题没有使用Java常用的名词“多线程”,是因为Python的并发分为多进程和多线程,进程在multiprocessing模块,线程在threading模块(线程虽然还有_thread模块,但是threading是对_thread的高级封装,使用起来更顺手所以这里只介绍threading)

 

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

 

 

先来看Python的多进程代码:

from multiprocessing import Process
import os

# 子进程要执行的代码
def run_proc(name) :
    print('Run child process %s (%s)...' % (name, os.getpid()))

if __name__=='__main__':
    print('Parent process %s.' % os.getpid())
    #入参要是元祖,进程内不允许修改
    p1 = Process(target=run_proc, args=('test-1',))
    p2 = Process(target=run_proc, args=('test-2',))
    print('Child process will start.')
    p1.start()
    p2.start()
    p1.join()
    p2.join()
    print('Child process end.')

运行结果:

Parent process 4956.
Child process will start.
Run child process test-1 (11108)...
Run child process test-2 (11172)...
Child process end.
共有3个进程,主进程和2个子进程,参要是元祖,进程内不允许修改,Java多线程中也会将入参设置成final类型。

还可以用进程池来管理:

from multiprocessing import Pool
import os, time, random

def myTask(name):
    print('Run task %s (%s)...' % (name, os.getpid()))
    start = time.time()
    time.sleep(random.random())
    end = time.time()
    print('Task %s runs %0.2f seconds.' % (name, (end - start)))

if __name__=='__main__':
    print('Parent process %s.' % os.getpid())
    p = Pool(2)
    #进程数故意比池多
    for i in range(5):
        p.apply_async(myTask, args=(i,))
    print('Waiting for all subprocesses done...')
    #join之前必须closs,禁止进程池再接收新任务
    p.close()
    #等待所有进程执行完毕
    p.join()
    print('All subprocesses done.')




运行结果:


Parent process 15160.
Waiting for all subprocesses done...
Run task 0 (12588)...
Run task 1 (7948)...
Task 1 runs 0.37 seconds.
Run task 2 (7948)...
Task 0 runs 0.73 seconds.
Run task 3 (12588)...
Task 2 runs 0.48 seconds.
Run task 4 (7948)...
Task 3 runs 0.58 seconds.
Task 4 runs 0.54 seconds.
All subprocesses done.

进程池的方式可以控制运行中的进程数量,通过观察运行结果中进程号看得出进程池中只有两个进程。

 

进程间如何通信?使用Queue!现在有3个进程,2个负责写1个负责读:

from multiprocessing import Process, Queue
import os, time, random

# 写数据进程执行的代码:
def write(q):
    print('Process to write: %s' % os.getpid())
    for element in ['A', 'B', 'C','D','E','F','G','H']:
        print('Write %s' %element)
        q.put(element)
        time.sleep(random.random())

# 读数据进程执行的代码:
def read(q):
    while True:
        #queue是阻塞队列,省去很多麻烦
        element = q.get(True)
        print('Read %s' %element)

if __name__=='__main__':
    # 父进程创建Queue,并传给各个子进程:
    q = Queue()
    pw1 = Process(target=write, args=(q,))
    pw2 = Process(target=write, args=(q,))
    pr = Process(target=read, args=(q,))
    # 启动子进程pw,写入:
    pw1.start()
    pw2.start()
    # 启动子进程pr,读取:
    pr.start()
    # 等待pw结束:
    pw1.join()
    pw2.join()
    # pr进程里是死循环,无法等待其结束,只能强行终止:
    pr.terminate()



 

看完多进程我们再来看python多线程的开发,回到了Java人员熟悉的多线程算是如鱼得水,编码思路跟多进程基本一样,不同点是考虑到线程安全问题需要引入锁和ThreadLocal的概念。

Python锁的两个核心方法是:加锁lock.acquire()和释放锁lock.release(),为了保证锁一定要被释放我们往往将release()放在finally里


例子:

import threading
import time

lock = threading.Lock()

class myThread(threading.Thread):
    def __init__(self, threadID, name, counter):
        threading.Thread.__init__(self)
        self.threadID = threadID
         = name
        self.counter = counter

    def run(self):
        # 获得锁,成功获得锁定后返回True
        # 可选的timeout参数不填时将一直阻塞直到获得锁定
        # 否则超时后将返回False
        while self.counter>0 :
            lock.acquire()
            #打印机打印3次
            print_time(, 3)
            # 释放锁
            lock.release()
            self.counter-=1

def print_time(threadName, counter):
    while counter:
        time.sleep(1)
        print("%s: %s" % (threadName, time.ctime(time.time())))
        counter -= 1

# 创建新线程
thread1 = myThread(1, "Thread-1", 3)
thread2 = myThread(2, "Thread-2", 3)

# 开启新线程
thread1.start()
thread2.start()
#等待线程执行完毕
thread1.join()
thread2.join()

执行结果:

Thread-1: Thu Dec 21 15:34:02 2017
Thread-1: Thu Dec 21 15:34:03 2017
Thread-1: Thu Dec 21 15:34:04 2017
Thread-1: Thu Dec 21 15:34:05 2017
Thread-1: Thu Dec 21 15:34:06 2017
Thread-1: Thu Dec 21 15:34:07 2017
Thread-2: Thu Dec 21 15:34:08 2017
Thread-2: Thu Dec 21 15:34:09 2017
Thread-2: Thu Dec 21 15:34:10 2017
Thread-1: Thu Dec 21 15:34:12 2017
Thread-1: Thu Dec 21 15:34:13 2017
Thread-1: Thu Dec 21 15:34:14 2017
Thread-2: Thu Dec 21 15:34:15 2017
Thread-2: Thu Dec 21 15:34:16 2017
Thread-2: Thu Dec 21 15:34:17 2017
Thread-2: Thu Dec 21 15:34:18 2017
Thread-2: Thu Dec 21 15:34:19 2017
Thread-2: Thu Dec 21 15:34:20 2017

 

锁虽然能解决线程间资源共享的问题,但是必然会带来竞争,而且处理不好容易产生死锁。

为了线程安全 Python 也有 Threadlocal 的解决思路,跟前面介绍的一期 Java 中原理一样<ThreadLocal-单例模式下高并发线程安全>。


代码:

import threading,time,random

# 创建全局ThreadLocal对象:
local = threading.local()

class myThread(threading.Thread):
    def __init__(self, threadID, name, counter):
        threading.Thread.__init__(self)
        self.threadID = threadID
         = name
        self.counter = counter

    def run(self):
        # 获得锁,成功获得锁定后返回True
        # 可选的timeout参数不填时将一直阻塞直到获得锁定
        # 否则超时后将返回False
        while self.counter>0 :
            local.name = 
            local.count = 3
            #打印机打印3次
            print_time()
            self.counter-=1

def print_time():
    threadName = local.name
    counter = local.count
    while counter:
        time.sleep(random.random())
        print("%s: %s " % (threadName, time.ctime(time.time())))
        counter -= 1
# 创建新线程
thread1 = myThread(1, "Thread-1", 3)
thread2 = myThread(2, "Thread-2", 3)

# 开启新线程
thread1.start()
thread2.start()
#等待线程执行完毕
thread1.join()
thread2.join()




 

那么问题来了,多进程好还是多线程好?

多进程和多线程的任务调度都是Master-Worker模式的,Workder真正负责运算,Master负责监督Workder和收集运算结果。在多进程中,主进程是Master,子进程是Worker;在多线程中主线程是Master,子线程是Worker。

稳定性方面多进程比多线程要好,因为进程是独立分开的,子进程出现问题不会影响到其它进程;而多线程是共享进程内存的,子线程出现问题可能会导致该进程内整所有子线程一起完蛋!

在开销上多进程要比多线程要大,多线程要轻量很多,但是无论是多线程还是多进程同时运行的数量都不能太多,CPU调度会出现竞争,用于任务切换的消耗将远远大于用于worker任务的消耗。

数据共享方面,进程使用各自独立的存储,数据只能同步和交互无法共享;多线程间共用进程的同一个存储,可以共享同一个资源对象(也就是说非线程安全带来的好处)。

 

对于Java语言我们没得选,并发只能用多线程。但是对于Python语言当我们有的选的时候,先看规模,规模都很大的情况下如果不需要考虑数据共享,尽量用多进程,因为在分布式、微服务流行的年代,通过添加PC的方式进程数的限制不再成为瓶颈,为何不用更稳定的方式呢;如果要考虑数据共享先分析通过数据同步方式能否低消耗的解决,能解决还是用多进程,代价很大就用多线程。当然了运算规模很小,需要快餐式消费,还是多线程开销更小。