Python既支持多进程,又支持多线程。线程是最小的执行单元,而进程由至少一个线程组成。如何调度进程和线程,完全由操作系统决定,程序自己不能决定什么时候执行,执行多长时间。
进程
一个程序的执行实例就是一个进程。每一个进程提供执行程序所需的所有资源。(进程本质上是资源的集合)每一个进程启动时都会最先产生一个线程,即主线程。然后主线程会再创建其他的子线程。
线程
操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。一个线程是一个execution context(执行上下文),即一个cpu执行时所需要的一串指令。
进程:打开word
线程:在word里打字、拼写、打印
进程与线程区别
1.同一个进程中的线程共享同一内存空间,但是进程之间是独立的。
2.同一个进程中的所有线程的数据是共享的(进程通讯),进程之间的数据是独立的。
3.对主线程的修改可能会影响其他线程的行为,但是父进程的修改(除了删除以外)不会影响其他子进程。
4.线程是一个上下文的执行指令,而进程则是与运算相关的一簇资源。
5.同一个进程的线程之间可以直接通信,但是进程之间的交流需要借助中间代理来实现。
6.创建新的线程很容易,但是创建新的进程需要对父进程做一次复制。
7.一个线程可以操作同一进程的其他线程,但是进程只能操作其子进程。
8.线程启动速度快,进程启动速度慢(但是两者运行速度没有可比性)。
多任务的实现有3种方式:
- 多进程模式;
- 多线程模式;
- 多进程+多线程模式。
(一)多进程
Python程序实现多进程(multiprocessing),先了解操作系统的相关知识:
Unix/Linux
操作系统提供了一个fork()
系统调用,它非常特殊。普通的函数调用,调用一次,返回一次,但是fork()
调用一次,返回两次,因为操作系统自动把当前进程(称为父进程)复制了一份(称为子进程),然后,分别在父进程和子进程内返回。子进程永远返回0
,而父进程返回子进程的ID
。这样做的理由是,一个父进程可以fork
出很多子进程,所以,父进程要记下每个子进程的ID
,而子进程只需要调用getppid()
就可以拿到父进程的ID
。
Python的os
模块封装了常见的系统调用,其中就包括fork
,可以在Python程序中轻松创建子进程:
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 (876) start...
I (876) just created a child process (877).
I am child process (877) and my parent is 876.
Windows没有fork
调用,上面的代码在Windows上无法运行。有了fork
调用,一个进程在接到新任务时就可以复制出一个子进程来处理新任务,常见的Apache服务器就是由父进程监听端口,每当有新的http请求时,就fork
出子进程来处理新的http请求。
1 、multiprocessing
multiprocessing
模块就是跨平台版本的多进程模块,提供了一个Process
类来代表一个进程对象:
from multiprocessing import Process
import os
# 子进程需要执行的代码
def run_proc(name):
print('Run子进程 %s(%s)...' % (name, os.getpid()))
if __name__ == '__main__':
print('父进程 %s.' % os.getpid())
p = Process(target=run_proc, args=('test',))
print('子进程将启动.')
p.start()
p.join()
print('子进程结束.')
创建子进程时,只需要传入一个执行函数和函数的参数,创建一个Process
实例,用start()
方法启动,这样创建进程比fork()
还要简单。join()
方法可以等待子进程结束后再继续往下运行,通常用于进程间的同步。
2、Pool
如果要启动大量的子进程,可以用进程池的方式批量创建子进程:
from multiprocessing import Pool
import os, time, random
def long_time_task(name):
print('任务启动%s(%s)...' % (name, os.getpid()))
start = time.time()
time.sleep(random.random() * 3)
end = time.time()
print('任务 %s 启动 %0.2f 秒.' % (name, (end - start)))
if __name__ == '__main__':
print('父进程%s.' % os.getpid())
p = Pool(4)
for i in range(5):
p.apply_async(long_time_task, args=(i,))
print('等待所有进程执行完毕...')
p.close() # 调用close()之后就不能继续添加新的Process
p.join() # 调用join()方法会等待所有子进程执行完毕 调用join()之前必须先调用close()
print('所有进程执行完毕.')
父进程7248.
等待所有进程执行完毕...
任务启动0(6012)...
任务启动1(5324)...
任务启动2(8176)...
任务启动3(6556)...
任务 1 启动 0.50 秒.
任务启动4(5324)...
任务 3 启动 1.67 秒.
任务 0 启动 2.43 秒.
任务 4 启动 1.99 秒.
任务 2 启动 2.57 秒.
所有进程执行完毕.
任务 0、1、2、3立刻执行,而任务4等前四个任务完成后才执行,任务 4要等待前面某个任务完成后才执行,Pool(4)
默认是4,最多执行4个进程,Pool
的默认大小是CPU的核数。
3 、子进程
很多时候,子进程并不是自身,而是一个外部进程。创建了子进程后,还需控制子进程的输入和输出。subprocess
模块可以让我们非常方便地启动一个子进程,然后控制其输入和输出。
import subprocess
print('$ nslookup www.python.org')
r = subprocess.call(['nslookup', 'www.python.org'])
print('Exit code:', r)
$ nslookup www.python.org
Server: 192.168.19.4
Address: 192.168.19.4#53
Non-authoritative answer:
www.python.org canonical name = python.map.fastly.net.
Name: python.map.fastly.net
Address: 199.27.79.223
Exit code: 0
如果子进程还需要输入,则可以通过communicate()
方法输入:
import subprocess
print('$ nslookup')
p = subprocess.Popen(['nslookup'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
output, err = p.communicate(b'set q=mx\npython.org\nexit\n')
print(output.decode('utf-8'))
print('Exit code:', p.returncode)
$ nslookup
Server: 192.168.19.4
Address: 192.168.19.4#53
Non-authoritative answer:g/h'
g
python.org mail exchanger = 50 mail.python.org.
Authoritative answers can be found from:
mail.python.org internet address = 82.94.164.166
mail.python.org has AAAA address 2001:888:2000:d::a6
Exit code: 0
4 、进程间通信
进程之间肯定是需要通信的,操作系统提供了很多机制来实现进程间的通信。Python的multiprocessing
模块包装了底层的机制,提供了Queue
、Pipes
等多种方式来交换数据。
以Queue
为例,在父进程中创建两个子进程,一个往Queue
里写数据,一个从Queue
里读数据:
from multiprocessing import Process, Queue
import os, time, random
# 写数据进程执行代码
def write(q):
print('进程 写:%s' % os.getpid())
for value in ['a', 'b', 'c']:
print('添加 %s 到 Queue...' % value)
q.put(value)
time.sleep(random.random())
# 读数据进程执行的代码
def read(q):
print('进程 读: %s' % os.getpid())
while True:
value = q.get(True)
print('读取 %s 从 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.join()
# pr进程死循环,强制终止
pr.terminate()
进程 写:5576
添加 a 到 Queue...
进程 读: 7232
读取 a 从 Queue...
添加 b 到 Queue...
读取 b 从 Queue...
添加 c 到 Queue...
读取 c 从 Queue...
在Unix/Linux
下,multiprocessing
模块封装了fork()
调用,不需要关注fork()
的细节。Windows没有fork
调用,因此
multiprocessing
需要模拟出fork
的效果,父进程所有Python对象都必须通过pickle
序列化再传到子进程去。
- Unix/Linux下,可以使用
fork()
调用实现多进程。 - 实现跨平台的多进程,使用
multiprocessing
模块。 - 进程间通信是通过
Queue
、Pipes
等实现的。
(二)多线程
多任务可以由多进程完成,也可以由一个进程内的多线程完成。线程是操作系统直接支持的执行单元,因此,高级语言通常都内置多线程的支持,Python也不例外,并且Python的线程是真正的Posix Thread
,而不是模拟出来的线程。Python的标准库提供了两个模块:_thread
和threading
,_thread
是低级模块,threading
是高级模块,对_thread
进行了封装。绝大多数情况下,我们只需要使用threading
这个高级模块。启动一个线程就是把一个函数传入并创建Thread
实例,然后调用start()
开始执行:
线程常用方法
方法 | 注释 |
---|---|
start() | 线程准备就绪,等待CPU调度 |
setName() | 为线程设置名称 |
getName() | 获取线程名称 |
setDaemon(True) | 设置为守护线程 |
join() | 逐个执行每个线程,执行完毕后继续往下执行 |
run() | 线程被cpu调度后自动执行线程对象的run方法,如果想自定义线程类,直接重写run方法就行了 |
1)、普通创建
import threading, time
def run(n):
print('任务', n)
time.sleep(1)
print('2s')
time.sleep(1)
print('1s')
time.sleep(1)
print('0s')
time.sleep(1)
t1 = threading.Thread(target=run, args=('t1',))
t2 = threading.Thread(target=run, args=('t2',))
t1.start()
t2.start()
任务 t1
任务 t2
2s
2s
1s
1s
0s
0s
2)、继承threading.Thread来自定义线程类(重构Thread类中的run方法)
import threading, time
class MyThread(threading.Thread):
def __init__(self, n):
super(MyThread, self).__init__() # 重构run函数
self.n = n
def run(self):
print('任务', self.n)
time.sleep(1)
print('2s')
time.sleep(1)
print('1s')
time.sleep(1)
print('0s')
time.sleep(1)
if __name__ == '__main__':
t1 = MyThread('t1')
t2 = MyThread('t2')
t1.start()
t2.start()
任务 t2
2s
2s
1s1s
0s
0s
3)、多线程创建
import time, threading
# 新线程执行的代码
def loop():
print('线程 %s 正在启动...' % threading.current_thread().name)
n = 0
while n < 6:
n = n + 1
print('线程 %s >>> %s' % (threading.current_thread().name, n))
time.sleep(1)
print('线程 %s 结束.' % threading.current_thread().name)
print('线程 %s 正在启动...' % threading.current_thread().name)
t = threading.Thread(target=loop, name='LoopThread')
t.start()
t.join()
print('线程 %s 结束.' % threading.current_thread().name)
线程 MainThread 正在启动...
线程 LoopThread 正在启动...
线程 LoopThread >>> 1
线程 LoopThread >>> 2
线程 LoopThread >>> 3
线程 LoopThread >>> 4
线程 LoopThread >>> 5
线程 LoopThread >>> 6
线程 LoopThread 结束.
线程 MainThread 结束.
任何进程默认就会启动一个线程,我们把该线程称为主线程,主线程又可以启动新的线程,Python的threading
模块有个current_thread()
函数,它永远返回当前线程的实例。主线程实例的名字叫MainThread
,子线程的名字在创建时指定,我们用LoopThread
命名子线程。名字仅仅在打印时用来显示,完全没有其他意义,如果不起名字Python就自动给线程命名为Thread-1,Thread-2……
4)、计算子线程执行的时间
sleep
的时候是不会占用cpu,在sleep
的时候操作系统会把线程暂时挂起。
import threading, time
def run(n):
print('任务', n, threading.current_thread()) # 输入当前线程
time.sleep(1)
print('3s')
time.sleep(1)
print('2s')
time.sleep(1)
print('1s')
start_time = time.time()
thread_obj = [] # 存放子线程实例
for i in range(3):
t = threading.Thread(target=run, args=('t-%s' % i,))
t.start()
thread_obj.append(t)
for t in thread_obj:
t.join() #为每个子线程添加join之后,主线程就会等这些子线程执行完之后再执行。
print('cost:', time.time() - start_time) # 主线程
print(threading.current_thread()) # 输出当前线程
任务 t-0 <Thread(Thread-1, started 8124)>
任务 t-1 <Thread(Thread-2, started 8944)>
任务 t-2 <Thread(Thread-3, started 948)>
3s
3s
3s
2s
2s
2s
1s
1s
1s
cost: 3.000999927520752
<_MainThread(MainThread, started 8756)>
5)、统计当前活跃的线程数
主线程比子线程快很多,当主线程执行active_count()
时,其他子线程都还没执行完毕,因此利用主线程统计的活跃的线程数num = sub_num(子线程数量)+1(主线程本身):
import threading, time
def run(n):
print('任务', n)
time.sleep(0.5) # 子线程停0.5s
for i in range(3):
t = threading.Thread(target=run, args=('t-%s' % i,))
t.start()
time.sleep(1) # 主线程 停1s
print(threading.active_count()) # 输出活跃线程数
任务 t-0
任务 t-1
任务 t-2
1
6)、守护进程
使用setDaemon(True)
把所有的子线程都变成了主线程的守护线程,因此当主进程结束后,子线程也会随之结束。所以当主线程结束后,整个程序就退出了。
import threading, time
def run(n):
print('任务', n)
time.sleep(1)
print('3')
time.sleep(1)
print('2')
time.sleep(1)
print('1')
for i in range(3):
t = threading.Thread(target=run, args=('t-%s' % i,))
t.setDaemon(True) # 子进程设置为守护线程,必须在start()之前设置
t.start()
time.sleep(0.5) # 主线程停0.5s
print(threading.active_count()) # 输出活跃的线程数
任务 t-0
任务 t-1
任务 t-2
4
1、Lock(互斥锁)
线程之间是进行随机调度,并且每个线程可能只执行n条执行之后,当多个线程同时修改同一条数据时可能会出现脏数据,所以,出现了线程锁,即同一时刻允许一个线程执行操作。线程锁用于锁定资源,你可以定义多个锁, 像下面的代码, 当你需要独占某一资源时,任何一个锁都可以锁这个资源,就好比你用不同的锁都可以把相同的一个门锁住是一个道理。
由于线程之间是进行随机调度,如果有多个线程同时操作一个对象,如果没有很好地保护该对象,会造成程序结果的不可预期,我们也称此为“线程不安全”。
多线程和多进程最大的不同在于,多进程中,同一个变量,各自有一份拷贝存在于每个进程中,互不影响,而多线程中,所有变量都由所有线程共享,所以,任何一个变量都可以被任何一个线程修改,因此,线程之间共享数据最大的危险在于多个线程同时改一个变量,把内容给改乱了。通过threading.Lock()
来实现锁。
import time, threading
# 银行存款:
balance = 0
lock = threading.Lock() #实例化一个锁对象
def change_it(n):
# 先存后取,结果应该为0:
global balance
balance = balance + n
balance = balance - n
def run_thread(n):
for i in range(100000):
# 获取锁:
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()
时,只有一个线程能成功地获取锁,然后继续执行代码,其他线程就继续等待直到获得锁为止。获得锁的线程用完后一定要释放锁,否则那些苦苦等待锁的线程将永远等待下去,成为死线程。所以我们用try...finally
来确保锁一定会被释放。
锁的好处就是确保了某段关键代码只能由一个线程从头到尾完整地执行,坏处当然也很多,首先是阻止了多线程并发执行,包含锁的某段代码实际上只能以单线程模式执行,效率就大大地下降了。其次,由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对方持有的锁时,可能会造成死锁,导致多个线程全部挂起,既不能执行,也无法结束,只能靠操作系统强制终止。
事件(Event类)
线程的事件用于主线程控制其他线程的执行,事件是一个简单的线程同步对象,其主要提供以下几个方法:
方法 | 注释 |
---|---|
clear | 将flag设置为“False” |
set | 将flag设置为“True” |
is_set | 判断是否设置了flag |
wait | 会一直监听flag,如果没有检测到flag就一直处于阻塞状态 |
事件处理的机制:全局定义了一个Flag
,当flag
值为False
,那么event.wait()
就会阻塞,当flag
值为True
,那么event.wait()
便不再阻塞。
#利用Event类模拟红绿灯
import threading
import time
event = threading.Event()
def lighter():
count = 0
event.set() #初始值为绿灯
while True:
if 5 < count <=10 :
event.clear() # 红灯,清除标志位
print("\33[41;1m 红灯 \033[0m")
elif count > 10:
event.set() # 绿灯,设置标志位
count = 0
else:
print("\33[42;1m 绿灯 \033[0m")
time.sleep(1)
count += 1
def car(name):
while True:
if event.is_set(): #判断是否设置了标志位
print("[%s] running..."%name)
time.sleep(1)
else:
print("[%s] 红灯,停车..."%name)
event.wait()
print("[%s] 绿灯,通行..."%name)
light = threading.Thread(target=lighter,)
light.start()
car = threading.Thread(target=car,args=("悍马",))
car.start()
定时器(Timer类)
from threading import Timer
def hello():
print('hello.world')
t = Timer(1, hello) # 1s后执行hello函数
t.start()
(三)ThreadLocal
多线程环境下,每个线程都有自己的数据。一个线程使用自己的局部变量比使用全局变量好,因为局部变量只有线程自己能看见,不会影响其他线程,而全局变量的修改必须加锁。
import threading
# 创建全局ThreadLocal对象
local_school = threading.local()
def process_student():
# 获取当前线程关联的student
std = local_school.student
print('Hello,%s(in %s)' % (std, threading.current_thread().name))
def process_thread(name):
# 绑定ThreadLocal的student
local_school.student = name
process_student()
t1 = threading.Thread(target=process_thread, args=('alice',), name='线程-A')
t2 = threading.Thread(target=process_thread, args=('bob',), name='线程-B')
t1.start()
t2.start()
t1.join()
t2.join()
Hello,alice(in 线程-A)
Hello,bob(in 线程-B)
全局变量local_school
就是一个ThreadLocal
对象,每个Thread
对它都可以读写student
属性,但互不影响。可以把local_school
看成全局变量,但每个属性如local_school.student
都是线程的局部变量,可以任意读写而互不干扰,也不用管理锁的问题,ThreadLocal
内部会处理。全局变量local_school
是一个dict
,不但可以用local_school.student
,还可以绑定其他变量,如local_school.teacher
等。ThreadLocal
最常用的地方就是为每个线程绑定一个数据库连接,HTTP请求,用户身份信息等,这样一个线程的所有调用到的处理函数都可以非常方便地访问这些资源。