python多线程和多进程

概念

  • 进程是操作系统分配资源的最小单元, 线程是操作系统调度的最小单元。
  • 一个应用程序至少包括1个进程,而1个进程包括1个或多个线程,线程的尺度更小。
  • 每个进程在执行过程中拥有独立的内存单元,而一个进程的多个线程在执行过程中共享内存。

    python多线程

    python多线程有两种实现方式
    1. 实例化一个threading.Thread的对象,并传入一个初始化函数对象(initial function )作为线程执行的入口
    2. 继承threading.Thread,并重写run函数

多线程实现

方式一
创建threading.Thread对象

1
2
3
4
5
6
7
8
9
10
11
import threading
import time
def tstart(arg):
time.sleep(0.5)
print("%s running...." % arg)
if __name__ == '__main__':
t1 = threading.Thread(target=tstart, args=('This is thread 1',))
t2 = threading.Thread(target=tstart, args=('This is thread 2',))
t1.start()
t2.start()
print("This is main function")

结果

1
2
3
This is main function
This is thread 2 running....
This is thread 1 running....

方式二
继承threading.Thread,并重写run

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import threading
import time
class CustomThread(threading.Thread):
def __init__(self, thread_name):
# step 1: call base __init__ function
super(CustomThread, self).__init__(name=thread_name)
self._tname = thread_name
def run(self):
# step 2: overide run function
time.sleep(0.5)
print("This is %s running...." % self._tname)
if __name__ == "__main__":
t1 = CustomThread("thread 1")
t2 = CustomThread("thread 2")
t1.start()
t2.start()
print("This is main function")

结果

1
2
3
 This is main function
This is thread 2 running....
This is thread 1 running....

上面两种方法本质上都是直接或者间接使用threading.Thread类

threading.Thread(group=None, target=None, name=None, args=(), kwargs={})

关联上面两种创建线程的方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import threading
import time
class CustomThread(threading.Thread):
def __init__(self, thread_name, target = None):
# step 1: call base __init__ function
super(CustomThread, self).__init__(name=thread_name, target=target, args = (thread_name,))
self._tname = thread_name
def run(self):
# step 2: overide run function
# time.sleep(0.5)
# print("This is %s running....@run" % self._tname)
super(CustomThread, self).run()
def target(arg):
time.sleep(0.5)
print("This is %s running....@target" % arg)
if __name__ == "__main__":
t1 = CustomThread("thread 1", target)
t2 = CustomThread("thread 2", target)
t1.start()
t2.start()
print("This is main function")

结果:

1
2
3
 This is main function
This is thread 1 running....@target
This is thread 2 running....@target

上面这段代码说明:

  1. 两种方式创建线程,指定的参数最终都会传给threading.Thread类;
  2. 传给线程的目标函数是在基类Thread的run函数体中被调用的,如果run没有被重写的话。

threading模块的一些属性和方法可以参照官网,这里重点介绍一下threading.Thread对象的方法
下面是threading.Thread提供的线程对象方法和属性:

  • start():创建线程后通过start启动线程,等待CPU调度,为run函数执行做准备;
  • run():线程开始执行的入口函数,函数体中会调用用户编写的target函数,或者执行被重载的run函数;
  • join([timeout]):阻塞挂起调用该函数的线程,直到被调用线程执行完成或超时。通常会在主线程中调用该方法,等待其他线程执行完成。
  • name、getName()&setName():线程名称相关的操作;
  • ident:整数类型的线程标识符,线程开始执行前(调用start之前)为None;
  • isAlive()、is_alive():start函数执行之后到run函数执行完之前都为True;
  • daemon、isDaemon()&setDaemon():守护线程相关;

这些是我们创建线程之后通过线程对象对线程进行管理和获取线程信息的方法。

多线程执行

在主线程中创建若线程之后,他们之间没有任何协作和同步,除主线程之外每个线程都是从run开始被执行,直到执行完毕。
join
我们可以通过join方法让主线程阻塞,等待其创建的线程执行完成。

1
2
3
4
5
6
7
8
9
10
11
import threading
import time
def tstart(arg):
print("%s running....at: %s" % (arg,time.time()))
time.sleep(1)
print("%s is finished! at: %s" % (arg,time.time()))
if __name__ == '__main__':
t1 = threading.Thread(target=tstart, args=('This is thread 1',))
t1.start()
t1.join() # 当前线程阻塞,等待t1线程执行完成
print("This is main function at:%s" % time.time())

结果:

1
2
3
 This is thread 1 running....at: 1564906617.43
This is thread 1 is finished! at: 1564906618.43
This is main function at:1564906618.43

如果不加任何限制,当主线程执行完毕之后,当前程序并不会结束,必须等到所有线程都结束之后才能结束当前进程。
将上面程序中的t1.join()去掉,执行结果如下:

1
2
3
This is thread 1 running....at: 1564906769.52
This is main function at:1564906769.52
This is thread 1 is finished! at: 1564906770.52

deamon守护线程
可以通过将创建的线程指定为守护线程(daemon),这样主线程执行完毕之后会立即结束未执行完的线程,然后结束程序。

1
2
3
4
5
6
7
8
9
10
11
12
import threading
import time
def tstart(arg):
print("%s running....at: %s" % (arg,time.time()))
time.sleep(1)
print("%s is finished! at: %s" % (arg,time.time()))
if __name__ == '__main__':
t1 = threading.Thread(target=tstart, args=('This is thread 1',))
t1.setDaemon(True)
t1.start()
# t1.join() # 当前线程阻塞,等待t1线程执行完成
print("This is main function at:%s" % time.time())

结果:

1
2
  This is thread 1 running....at: 1564906847.85
This is main function at:1564906847.85

python多进程

相比较于threading模块用于创建python多线程,python提供multiprocessing用于创建多进程。先看一下创建进程的两种方式。

多进程实现

创建进程的方式和创建线程的方式类似:
1.实例化一个multiprocessing.Process的对象,并传入一个初始化函数对象(initial function )作为新建进程执行入口;
2.继承multiprocessing.Process,并重写run函数;

方式一

1
2
3
4
5
6
7
8
9
10
11
from multiprocessing import Process 
import os, time
def pstart(name):
# time.sleep(0.1)
print("Process name: %s, pid: %s "%(name, os.getpid()))
if __name__ == "__main__":
subproc = Process(target=pstart, args=('subprocess',))
subproc.start()
subproc.join()
print("subprocess pid: %s"%subproc.pid)
print("current process pid: %s" % os.getpid())

结果:

1
2
3
Process name: subprocess, pid: 4888 
subprocess pid: 4888
current process pid: 9912

方式二

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from multiprocessing import Process 
import os, time
class CustomProcess(Process):
def __init__(self, p_name, target=None):
# step 1: call base __init__ function()
super(CustomProcess, self).__init__(name=p_name, target=target, args=(p_name,))

def run(self):
# step 2:
# time.sleep(0.1)
print("Custom Process name: %s, pid: %s "%(self.name, os.getpid()))
if __name__ == '__main__':
p1 = CustomProcess("process_1")
p1.start()
p1.join()
print("subprocess pid: %s"%p1.pid)
print("current process pid: %s" % os.getpid())

python多进程和多线程比较

先来看两个例子:

开启两个python线程分别做一亿次加一操作,和单独使用一个线程做一亿次加一操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def tstart(arg):
var = 0
for i in xrange(100000000):
var += 1

if __name__ == '__main__':
t1 = threading.Thread(target=tstart, args=('This is thread 1',))
t2 = threading.Thread(target=tstart, args=('This is thread 2',))
start_time = time.time()
t1.start()
t2.start()
t1.join()
t2.join()
print("Two thread cost time: %s" % (time.time() - start_time))
start_time = time.time()
tstart("This is thread 0")
print("Main thread cost time: %s" % (time.time() - start_time))

结果:

1
2
Two thread cost time: 20.6570000648
Main thread cost time: 2.52800011635

使用两个进程进行上面的操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def pstart(arg):
var = 0
for i in xrange(100000000):
var += 1
if __name__ == '__main__':
p1 = Process(target = pstart, args = ("1", ))
p2 = Process(target = pstart, args = ("2", ))
start_time = time.time()
p1.start()
p2.start()
p1.join()
p2.join()
print("Two process cost time: %s" % (time.time() - start_time))
start_time = time.time()
pstart("0")
print("Current process cost time: %s" % (time.time() - start_time))

结果:

1
2
Two process cost time: 2.91599988937
Current process cost time: 2.52400016785

对比分析
双进程并行执行和单进程执行相同的运算代码,耗时基本相同,双进程耗时会稍微多一些,可能的原因是进程创建和销毁会进行系统调用,造成额外的时间开销。

但是对于python线程,双线程并行执行耗时比单线程要高的多,效率相差近10倍。如果将两个并行线程改成串行执行,即:

1
2
3
4
t1.start()
t1.join()
t2.start()
t2.join()

结果

1
2
Two thread cost time: 5.12199997902
Main thread cost time: 2.54200005531

可以看到三个线程串行执行,每一个执行的时间基本相同。

本质原因双线程是并发执行的,而不是真正的并行执行。原因就在于GIL锁。
GIL锁
提起python多线程就不得不提一下GIL(Global Interpreter Lock 全局解释器锁),这是目前占统治地位的python解释器CPython中为了保证数据安全所实现的一种锁。不管进程中有多少线程,只有拿到了GIL锁的线程才可以在CPU上运行,即时是多核处理器。对一个进程而言,不管有多少线程,任一时刻,只会有一个线程在执行。对于CPU密集型的线程,其效率不仅仅不高,反而有可能比较低。python多线程比较适用于IO密集型的程序。对于的确需要并行运行的程序,可以考虑多进程。

多线程对锁的争夺,CPU对线程的调度,线程之间的切换等均会有时间开销。
下面简单的比较一下线程与进程

  • 进程是资源分配的基本单位,线程是CPU执行和调度的基本单位;
  • 通信/同步方式:
    • 进程:
      • 通信方式:管道,FIFO,消息队列,信号,共享内存,socket,stream流;
      • 同步方式:PV信号量,管程
    • 线程:
      • 同步方式:互斥锁,递归锁,条件变量,信号量
      • 通信方式:位于同一进程的线程共享进程资源,因此线程间没有类似于进程间用于数据传递的通信方式,线程间的通信主要是用于线程同步。
  • CPU上真正执行的是线程,线程比进程轻量,其切换和调度代价比进程要小;
  • 线程间对于共享的进程数据需要考虑线程安全问题,由于进程之间是隔离的,拥有独立的内存空间资源,相对比较安全,只能通过上面列出的IPC(Inter-Process Communication)进行数据传输;
  • 系统有一个个进程组成,每个进程包含代码段、数据段、堆空间和栈空间,以及操作系统共享部分 ,有等待,就绪和运行三种状态;
  • 一个进程可以包含多个线程,线程之间共享进程的资源(文件描述符、全局变量、堆空间等),寄存器变量和栈空间等是线程私有的;
  • 操作系统中一个进程挂掉不会影响其他进程,如果一个进程中的某个线程挂掉而且OS对线程的支持是多对一模型,那么会导致当前进程挂掉;
  • 如果CPU和系统支持多线程与多进程,多个进程并行执行的同时,每个进程中的线程也可以并行执行,这样才能最大限度的榨取硬件的性能;

线程和进程的上下文切换
进程切换过程切换牵涉到非常多的东西,寄存器内容保存到任务状态段TSS,切换页表,堆栈等。简单来说可以分为下面两步:

  1. 页全局目录切换,使CPU到新进程的线性地址空间寻址;
  2. 切换内核态堆栈和硬件上下文,硬件上下文包含CPU寄存器的内容,存放在TSS中;

线程运行于进程地址空间,切换过程不涉及到空间的变换,只牵涉到第二步;
使用多线程还是多进程

  • CPU密集型:程序需要占用CPU进行大量的运算和数据处理;
  • I/O密集型:程序中需要频繁的进行I/O操作;例如网络中socket数据传输和读取等;

由于python多线程并不是并行执行,因此较适合与I/O密集型程序,多进程并行执行适用于CPU密集型程序;

--------------------本文结束,感谢您的阅读--------------------