Bootstrap

python多线程是如何工作

一、进程、线程、协程的相关概念
1、进程、线程、协程定义
(1)进程是系统进行资源分配和调度的独立单位
(2)线程是进程的实体,是CPU调度和分派的基本单位
(3)协程也是线程,称微线程,自带CPU上下文,是比线程更小的执行单元
2、进程和线程的区别
一个程序至少有一个进程,一个进程至少有一个线程。线程的划分尺度小于进程(资源比进程少),使得多线程程序的并发性高。进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率;线程不能够独立执行,必须依存在进程中。
3、进程和线程的优缺点
线程和进程在使用上各有优缺点:线程执行开销小,但不利于资源的管理和保护;而进程正相反。
4、协程相关
将把一个进程比作生活中的饭店,则保证饭店正常运行的服务员就是线程,服务好每桌客人就是线程要完成的任务。
  当用多线程完成任务时,采取以下模式:每来一桌的客人,该桌子就安排一个服务员,即有多少桌客人就得对应多少个服务员。
  而当我们用协程来完成任务时,模式如下:就安排一个服务员,来吃饭得有一个点餐和等菜的过程,当A在点菜,就去B服务,B叫了菜在等待,我就去C,当C也在等菜并且A点菜点完了,赶紧到A来服务… …依次类推
  从上面的例子可以看出,想要使用协程,那么我们的任务必须有等待。当我们要完成的任务有耗时任务,属于IO密集型任务时,我们使用协程来执行任务会节省很多的资源(一个服务员和多个服务员的区别), 并且可以极大的利用到系统的资源。
二、多进程例子
1、Unix/Linux操作系统->fork()系统调用
fork()非常特殊。普通的函数调用,调用一次,返回一次。但是fork()调用一次,返回两次,因为操作系统自动把当前进程(称为父进程)复制了一份(称为子进程),然后,分别在父进程和子进程内返回。子进程永远返回0,而父进程返回子进程的ID。这样做的理由是,一个父进程可以fork出很多子进程,所以,父进程要记下每个子进程的ID,而子进程只需要调用getppid()就可以拿到父进程的ID。
Python的os模块封装了常见的系统调用,其中就包括fork,可以在Python程序中轻松创建子进程

import os
print('Process (%s) start...' % os.getpid())
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 (163320) start...
I (163320) just created a child process (163337).
I am child process (163337) and my parent is 163320.

2、multiprocessing模块
由于fork()只支持Unix/Linux系统,无法在Windows系统下运行。所以fork()不支持跨平台操作。由于python是跨平台的,提供了multiprocessing模块,该模块可以支持跨平台的多进程操作。multiprocessing模块提供了一个Process类来代表一个进程对象。

import os
from multiprocessing import Process
# 子进程要执行的代码
def run_proc(name):
    print('Run child process %s (%s)...' % (name, os.getpid()))
if __name__=='__main__':
	print('Parent process %s.' % os.getpid())
    p = Process(target=run_proc, args=('test',))
    print('Child process will start.')
    p.start() # 启动子进程
    p.join() #等待子进程结束
    print('Child process end.')

输出为:

Parent process 163928.
Child process will start.
Run child process test (163945)...
Child process end.

创建子进程时,只需要传入一个执行函数和函数的参数,创建一个Process实例,用start()方法启动,这样创建进程比fork()还要简单。join()方法可以等待子进程结束后再继续往下运行,通常用于进程间的同步。
3、Pool模块
如果需要启动大量的子进程,可以用进程池的方式批量创建子进程,multiprocessing中的Pool提供了创建进程池的方法。

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)
    for i in range(5):
        p.apply_async(long_time_task, args=(i,))
    print('Waiting for all subprocesses done...')
    p.close()
    p.join()
    print('All subprocesses done.')

输出为:

Parent process 165248.
Waiting for all subprocesses done...
Run task 0 (165265)...
Run task 1 (165266)...
Run task 2 (165267)...
Run task 3 (165268)...
Task 3 runs 0.01 seconds.
Run task 4 (165268)...
Task 2 runs 0.50 seconds.
Task 0 runs 1.19 seconds.
Task 4 runs 1.20 seconds.
Task 1 runs 2.50 seconds.
All subprocesses done.

对Pool对象调用join()方法会等待所有子进程执行完毕,调用join()之前必须先调用close(),调用close()之后就不能继续添加新的Process了。由于Pool的默认大小是CPU的核数,至少提交大于核数的子进程才能看到上面的等待效果。
4、子进程
子进程往往并不是自身,而是一个外部进程。在创建子进程后,还需要控制子进程的输入和输出。subprocess模块可以非常方便地启动一个子进程,然后控制起输入和输出。

import subprocess
print('$ nslookup www.python.org')
r = subprocess.call(['nslookup', 'www.python.org'])
print('Exit code:', r)

输出结果为:

$ nslookup www.python.org
Server:		127.0.0.53
Address:	127.0.0.53#53

Non-authoritative answer:
www.python.org	canonical name = dualstack.python.map.fastly.net.
Name:	dualstack.python.map.fastly.net
Address: 146.75.112.223
Name:	dualstack.python.map.fastly.net
Address: 2a04:4e42:1a::223

exit code: 0

如果子进程还需要输入,则可以通过communicate()方法输入。
5、进程间通信
Process之间肯定是需要通信的,操作系统提供了很多机制来实现进程间的通信。Python的multiprocessing模块包装了底层的机制,提供了Queue、Pipes等多种方式来交换数据。以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__" :
 q = Queue()
 pw = Process(target=write,args=(q,))
 pr = Process(target=read,args=(q,))
 pw.start()
 pr.start()
 pw.join()
 pr.join()
 pr.terminate()

三、多线程例子
1、threading模块
当我们面临多任务的时候,可以使用多个进程完成,但同时也可以在一个进程内由多个线程完成。线程是操作系统直接支持的执行单元。Python中的线程是真正的Posix Thread,而不是模拟出来的线程
Python的标准库中提供了两个模块:_thread和threading,_thread是低级模块,threading是对_thread封装的高级模块。因此我们在使用时只需用threading模块。
启动一个线程和启动一个进程的过程基本一样,均是将函数传入进程(Process)模块或者线程(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)
if __name__ == "__main__" :
  	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.

从上方代码可以看出,任何进程都会默认启动一个线程,将该线程成为主线程,主线程中又可启动新的线程。threading.current_thread().name永远返回当前线程的实例。
2、Lock
多线程和多进程最大的区别就是多线程共享一个进程的内存,所有变量由所有线程共享,任何一个变量都有可能被任何一个线程修改,因此在多进程编程中,往往要注意多个线程对同一个变量修改的问题(原因为线程之间是交替运行,一个线程运行过程中可能中断,但在另一个线程中却在修改)。而多进程中,即使执行同一个代码,同一个变量也会拷贝在各自进程中的内存,互不影响。
为了解决线程之间可能会修改同一变量的问题,确保多线程程序可以正常运行,应该给线程中运行的函数加锁(Lock),即在运行加锁的线程时,其他的进程不可执行进程中的函数,只有当锁被释放之后,获得该锁的线程才能修改。无论有多少线程,锁只有一个,且同一时刻只有一个线程持有该锁,所以不会造成修改的冲突。python中创建锁是通过threading.Lock()实现。以下为代码示例:

import threading
balance = 0
lock = threading.Lock()
def change_it(n):
    global balance
    balance = balance + n
    balance = balance - n

def run_thread(n):

    for i in range(2000000):
        lock.acquire()
        try:
            change_it(n)
        finally:
            lock.release()
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)

lock.acquire()为线程获得锁,当线程用完之后一定要释放锁,通过lock.release()释放,否则线程将是死线程。可以用try…finally来确保锁一定会被释放。在多线程环境下,每个线程都有自己的数据。一个线程使用自己的局部变量比使用全局变量好,因为局部变量只有线程自己能看见,不会影响其他线程,而全局变量的修改必须加锁。
3、

;