Python中级 —— 03进程与线程

多任务的实现有3种方式:
多进程模式;
多线程模式;
多进程+多线程模式。

** 进程: **

不同任务,例如打开一个写字本,就是开启一个新进程。

多进程

Unix/Linux操作系统提供了一个fork()系统调用
fork()调用一次,返回两次,操作系统自动把当前进程(称为父进程)复制一份(称为子进程),分别在父进程和子进程内返回。
子进程永远返回0,父进程返回子进程的ID(好处在于一个父进程可以fork()调用很多个子进程,父进程要记住子进程ID,即 `getpid()`;子进程如果想要拿到父进程的ID则调用 `getppid()` 即可。)   

** Pyhon的 os模块封装了常见的系统调用,其中就包括 fork() **

Unix/Linux/Mac: os模块

import os

print('Process (%s) start...' % os.getpid())
# Only works on Unix/Linux/Mac:
pid = os.fork()
if pid == 0:
    print('I am child process (%s) and my parent is %s.' % (os.getpid(), os.getppid()))
else:
    print('I (%s) just created a child process (%s).' % (os.getpid(), pid))

结果:
Process (754) start...
I (754) just created a child process (755).
I am child process (755) and my parent is 754.

** 由于windows没有fork()调用,需要跨平台的库支持多线程。 **

跨平台: multiprocessing 模块

- Process类 进程

步骤:

1. Process(target=func, args=(pro_name,)) # 创建一个Process实例,(参数:执行函数,进程名)

2. start()方法启动进程

3. join()方法可以等待子进程结束后再继续往下运行,通常用于进程间的同步
from multiprocessing import Process
import os

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

if __name__=='__main__':
    print('Parent process %s.' % os.getppid())
    p = Process(target=run_proc, args=('p1',)) # 创建Process实例
    print('Child process will start.')
    p.start() # 启动
    p.join() # 结束
    print('Child process end.')


结果:
Parent process 4484.
Child process will start.
Run child process p1(7106)...
Child process end.

** 如果需要创建大量的子进程,则需要使用 进程池的方式批量创建子进程 **

- Pool类 进程池

步骤:

1. Pool(4) # 进程池里面有四个进程的限制,必须有进程销毁才能继续添加,默认是CPU数

2. apply_async(func, args=(pro_name,))方法,(参数:执行函数,进程名)

3. close() # 禁止继续添加新的Process,在join之前

4. join() # 等待所有进程执行完毕
from multiprocessing import Pool
import os, time, random

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

if __name__=='__main__':
    print('Parent process %s.' % os.getpid())
    p = Pool(4) # 进程池限制4个进程
    for i in range(1, 6):
        p.apply_async(long_time_task, args=('pro'+str(i),))
    print('Waiting for all subprocesses done...')
    p.close() # 禁止继续添加新的Process
    p.join() # 等待所有进程执行完毕
    print('All subprocesses done.')
结果:
Parent process 2868.
Waiting for all subprocesses done...
Run task pro1 (1280)...
Run task pro2 (3692)...
Run task pro3 (6316)...
Run task pro4 (6000)...
Task pro2 runs 1.15 seconds.
Run task pro5 (3692)...
Task pro3 runs 2.04 seconds.
Task pro4 runs 2.06 seconds.
Task pro1 runs 2.48 seconds.
Task pro5 runs 2.00 seconds.
All subprocesses done.

** 进程之间需要通信,用于交换数据等,Queue、Pipes 等多个类 **

-Queue类 进程间通信

都写两个进程共用同一个Queue队列
from multiprocessing import Process, Queue
import os, time, random

# 写数据进程执行的代码:
def write(q):
    print('Process to write: %s' % os.getpid())
    for value in ['A', 'B', 'C']:
        print('Put %s to queue...' % value)
        q.put(value)
        time.sleep(random.random())

# 读数据进程执行的代码:
def read(q):
    print('Process to read: %s' % os.getpid())
    while True:
        value = q.get(True)
        print('Get %s from queue.' % value)

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

* 线程: *

每个任务中的不同操作,例如word中的输入文字,插入图片等

多线程

线程在执行过程中与进程还是有区别的。每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。
但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
每个线程都有他自己的一组CPU寄存器,称为线程的上下文,该上下文反映了线程上次运行该线程的CPU寄存器的状态。
指令指针和堆栈指针寄存器是线程上下文中两个最重要的寄存器,线程总是在进程得到上下文中运行的,这些地址都用于标志拥有线程的进程地址空间中的内存。  
  • 线程可以被抢占(中断)
  • 在其他线程正在运行时,线程可以暂时搁置(也称为睡眠) -- 这就是线程的退让

线程可以分为:

  • 内核线程:由操作系统内核创建和撤销。
  • 用户线程:不需要内核支持而在用户程序中实现的线程。

Python3 线程中常用的两个模块为:

  • _thread
  • threading(推荐使用)
thread 模块已被废弃。用户可以使用 threading 模块代替。所以,在 Python3 中不能再使用"thread" 模块。为了兼容性,Python3 将 thread 重命名为 "_thread"。   

两种使用方式: * 函数*或者用 *类*来包装线程对象。

1. _thread

_thread.start_new_thread ( function, args[, kwargs] )

参数说明:

  • function -- 线程函数。
  • args -- 传递给线程函数的参数,他必须是个tuple类型。
  • kwargs -- 可选参数。

2. threading 线程模块

步骤:

threading.Thread(target=func, name='Threadname')
start()
join()

threading 模块除了包含 _thread 模块中的所有方法外,还提供的其他方法:

  • threading.currentThread(): 返回当前的线程变量。
  • threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。
  • threading.activeCount(): 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果。

除了使用方法外,线程模块同样提供了Thread类来处理线程,Thread类提供了以下方法:

  • run(): 用以表示线程活动的方法。
  • start():启动线程活动。
  • join([time]): 等待至线程中止。这阻塞调用线程直至线程的join() 方法被调用中止-正常退出或者抛出未处理的异常-或者是可选的超时发生。
  • isAlive(): 返回线程是否活动的。
  • getName(): 返回线程名。
  • setName(): 设置线程名。

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

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.

- 线程同步: Lock() 锁

1. lock = threading.Lock()
2. lock.acquire() # 获取锁
3. lock.release() # 释放锁

注意:

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

优点:

确保了某段关键代码只能由一个线程从头到尾完整地执行

缺点:

阻止了多线程并发执行,包含锁的某段代码实际上只能以单线程模式执行,效率大大下降。
由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对方持有的锁时,可能会造成死锁,导致多个线程全部挂起,既不能执行,也无法结束,只能靠操作系统强制终止。
import threading
import time

class myThread (threading.Thread):
    def __init__(self, threadID, name, counter):
        threading.Thread.__init__(self)
        self.threadID = threadID
        self.name = name
        self.counter = counter
    def run(self):
        print ("开启线程: " + self.name)
        # 获取锁,用于线程同步
        threadLock.acquire()
        print_time(self.name, self.counter, 3)
        # 释放锁,开启下一个线程
        threadLock.release()

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

threadLock = threading.Lock()
threads = []

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

# 开启新线程
thread1.start()
thread2.start()

# 添加线程到线程列表
threads.append(thread1)
threads.append(thread2)

# 等待所有线程完成
for t in threads:
    t.join()
print ("退出主线程")

执行结果:

开启线程: Thread-1
开启线程: Thread-2
Thread-1: Thu Jan 25 13:52:57 2016
Thread-1: Thu Jan 25 13:52:58 2016
Thread-1: Thu Jan 25 13:52:59 2016
Thread-2: Thu Jan 25 13:53:01 2016
Thread-2: Thu Jan 25 13:53:03 2016
Thread-2: Thu Jan 25 13:53:05 2016
退出主线程

- 线程优先级队列( Queue)

Python 的 Queue 模块中提供了同步的、线程安全的队列类,包括FIFO(先入先出)队列Queue,LIFO(后入先出)队列LifoQueue,和优先级队列 PriorityQueue。

这些队列都实现了锁原语,能够在多线程中直接使用,可以使用队列来实现线程间的同步。

  • Queue 模块中的常用方法:

  • Queue.qsize() 返回队列的大小

  • Queue.empty() 如果队列为空,返回True,反之False

  • Queue.full() 如果队列满了,返回True,反之False

  • Queue.full 与 maxsize 大小对应

  • Queue.get([block[, timeout]])获取队列,timeout等待时间

  • Queue.get_nowait() 相当Queue.get(False)

  • Queue.put(item) 写入队列,timeout等待时间

  • Queue.put_nowait(item) 相当Queue.put(item, False)

  • Queue.task_done() 在完成一项工作之后,Queue.task_done()函数向任务已经完成的队列发送一个信号

  • Queue.join() 实际上意味着等到队列为空,再执行别的操作

import queue
import threading
import time

exitFlag = 0

class myThread (threading.Thread):
    def __init__(self, threadID, name, q):
        threading.Thread.__init__(self)
        self.threadID = threadID
        self.name = name
        self.q = q
    def run(self):
        print ("开启线程:" + self.name)
        process_data(self.name, self.q)
        print ("退出线程:" + self.name)

def process_data(threadName, q):
    while not exitFlag:
        queueLock.acquire()
        if not workQueue.empty():
            data = q.get()
            queueLock.release()
            print ("%s processing %s" % (threadName, data))
        else:
            queueLock.release()
        time.sleep(1)

threadList = ["Thread-1", "Thread-2", "Thread-3"]
nameList = ["One", "Two", "Three", "Four", "Five"]
queueLock = threading.Lock()
workQueue = queue.Queue(10)
threads = []
threadID = 1

# 创建新线程
for tName in threadList:
    thread = myThread(threadID, tName, workQueue)
    thread.start()
    threads.append(thread)
    threadID += 1

# 填充队列
queueLock.acquire()
for word in nameList:
    workQueue.put(word)
queueLock.release()

# 等待队列清空
while not workQueue.empty():
    pass

# 通知线程是时候退出
exitFlag = 1

# 等待所有线程完成
for t in threads:
    t.join()
print ("退出主线程")

执行结果:

开启线程:Thread-1
开启线程:Thread-2
开启线程:Thread-3
Thread-3 processing One
Thread-1 processing Two
Thread-2 processing Three
Thread-3 processing Four
Thread-1 processing Five
退出线程:Thread-3
退出线程:Thread-2
退出线程:Thread-1
退出主线程

- 多核CPU

如果你不幸拥有一个多核CPU,你肯定在想,多核应该可以同时执行多个线程。

如果写一个死循环的话,会出现什么情况呢?

打开Mac OS X的Activity Monitor,或者Windows的Task Manager,都可以监控某个进程的CPU使用率。

我们可以监控到一个死循环线程会100%占用一个CPU。

如果有两个死循环线程,在多核CPU中,可以监控到会占用200%的CPU,也就是占用两个CPU核心。

要想把N核CPU的核心全部跑满,就必须启动N个死循环线程。

试试用Python写个死循环:

import threading, multiprocessing

def loop():
    x = 0
    while True:
        x = x ^ 1

for i in range(multiprocessing.cpu_count()):
    t = threading.Thread(target=loop)
    t.start()

启动与CPU核心数量相同的N个线程,在4核CPU上可以监控到CPU占用率仅有102%,也就是仅使用了一核。

但是用C、C++或Java来改写相同的死循环,直接可以把全部核心跑满,4核就跑到400%,8核就跑到800%,为什么Python不行呢?

因为Python的线程虽然是真正的线程,但解释器执行代码时,有一个GIL锁:Global Interpreter Lock,任何Python线程执行前,必须先获得GIL锁,然后,每执行100条字节码,解释器就自动释放GIL锁,让别的线程有机会执行。这个GIL全局锁实际上把所有线程的执行代码都给上了锁,所以,多线程在Python中只能交替执行,即使100个线程跑在100核CPU上,也只能用到1个核。

GIL是Python解释器设计的历史遗留问题,通常我们用的解释器是官方实现的CPython,要真正利用多核,除非重写一个不带GIL的解释器。

所以,在Python中,可以使用多线程,但不要指望能有效利用多核。如果一定要通过多线程利用多核,那只能通过C扩展来实现,不过这样就失去了Python简单易用的特点。

不过,也不用过于担心,Python虽然不能利用多线程实现多核任务,但可以通过多进程实现多核任务。多个Python进程有各自独立的GIL锁,互不影响。

posted @ 2018-01-25 13:09  DarkSoul  阅读(250)  评论(0编辑  收藏  举报