博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

python多线程和多进程

Posted on 2020-10-15 19:57  心默默言  阅读(232)  评论(0编辑  收藏  举报

python多线程

 

实例一:最简单的多线程

 

python中提供两个标准库thread和threading用于对线程的支持,python3中已放弃对前者的支持,后者是一种更高层次封装的线程库,接下来均以后者为例。

创建线程 python中有两种方式实现线程:

实例化一个threading.Thread的对象,并传入一个初始化函数对象(initial function )作为线程执行的入口; 继承threading.Thread,并重写run函数;

In [2]:
import threading, time


def run(n):
    print("task  ", n)
    time.sleep(2)


start_time = time.time()
t1 = threading.Thread(target=run, args=("t1",))
t2 = threading.Thread(target=run, args=("t2",))

t1.start()
t2.start()
# t1.join()
# t2.join()
print(time.time() - start_time)
 
task   t1
task  0.004959583282470703 t2

 

不加join的话,主线程和子线程完全是并行的,加了join主线程得等这个子线程执行完毕,才能继续往下走。这样才能得到这个程序真正的运行时间。

In [4]:
start_time = time.time()
t1 = threading.Thread(target=run, args=("t1",))
t2 = threading.Thread(target=run, args=("t2",))

t1.start()
t2.start()
t1.join()
t2.join()
print(time.time() - start_time)
 
task   t1
task   t2
2.018137216567993
 

实例二:继承式调用多线程

 

创建一个新的类来支持多线程,继承了threading.Thread类

In [5]:
import threading, time


class MyThread(threading.Thread):
    def __init__(self, n, sleep_time):
        super(MyThread, self).__init__()
        self.n = n
        self.sleep_time = sleep_time
        
    def run(self):
        print("run task", self.n)
        time.sleep(2)
        print("task done,", self.n)


t1 = MyThread("t1", 2)
t2 = MyThread("t2", 4)

t1.start()
t2.start()
t1.join()
t2.join()
print('main')
 
run task t1
run task t2
task done,task done, t2
 t1
main
 

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():守护线程相关;
这些是我们创建线程之后通过线程对象对线程进行管理和获取线程信息的方法。

 

实例三: 一次性启动多个线程

 

第一个程序,使用循环来创建线程,但是这个程序中一共有51个线程,我们创建了50个线程,但是还有一个程序本身的线程,是主线程。这51个线程是并行的。注意:这个程序中是主线程启动了子线程。

In [6]:
import threading, time


def run(n):
    print("task ", n)
    time.sleep(2)
    print("task done,", n)


start_time = time.time()
for i in range(50):
    t = threading.Thread(target=run, args=("t- %s" % i,))
    t.start()

print("cost  :", time.time() - start_time)
 
task  t- 0
task  t- 1
task  t- 2
task  t- 3
task  t- 4
task  t- 5
task  t- 6
task  t- 7
task  t- 8
task  t- 9
task  t- 10
task  t- 11
task  t- 12
task  t- 13
task  t- 14
task  t- 15
task  t- 16
task  t- 17
task  t- 18
task  t- 19
task  t- 20
task  t- 21
task  t- 22
task  t- 23
task  t- 24
task  t- 25
task  t- 26
task  t- 27
task  t- 28
task  t- 29
task  t- 30
task  t- 31
task  t- 32
task  t- 33
task  t- 34
task  t- 35
task  t- 36
task  t- 37
task  t- 38
task  t- 39
task  t- 40
task  t- 41
task  t- 42
task  t- 43
task  t- 44
task  t- 45
task  t- 46
task  t- 47
task  t- 48
task  t- 49
cost  : 0.09465932846069336
 

我们观察结果会发现,程序显示的执行时间只有0.007秒,这是因为最后一个print函数它存在于主线程,而整个程序主线程和所有子线程是并行的,那么可想而知,在子线程还没有执行完毕的时候print函数就已经执行了,总的来说,这个时间只是执行了一个线程也就是主线程所用的时间。

 

接下来这个程序,吸取了上面这个程序的缺点,创建了一个列表,把所有的线程实例都存进去,然后使用一个for循环依次对线程实例调用join方法,这样就可以使得主线程等待所创建的所有子线程执行完毕才能往下走。注意实验结果:和两个线程的结果都是两秒多一点

In [7]:
import threading, time


def run(n):
    print("task ", n)
    time.sleep(2)
    print("task done,", n)


start_time = time.time()
t_obj = []
for i in range(50):
    t = threading.Thread(target=run, args=("t- %s" % i,))
    t_obj.append(t)
    t.start()

for t in t_obj:
    t.join()

print("cost  :", time.time() - start_time)
 
task  t- 0
task  t- 1
task  t- 2
task  t- 3
task  t- 4
task  t- 5
task  t- 6
task  t- 7
task  t- 8
task  t- 9
task  t- 10
task  t- 11
task  t- 12
task  t- 13
task  t- 14
task  t- 15
task  t- 16
task  t- 17
task  t- 18
task  t- 19
task  t- 20
task  t- 21
task  t- 22
task  t- 23
task  t- 24
task  t- 25
task  t- 26
task  t- 27
task  t- 28
task  t- 29
task  t- 30
task  t- 31
task  t- 32
task  t- 33
task  t- 34
task  t- 35
task  t- 36
task  t- 37
task  t- 38
task  t- 39
task  t- 40
task  t- 41
task  t- 42
task  t- 43
task  t- 44
task  t- 45
task  t- 46
task  t- 47
task  t- 48
task  t- 49
task done,task done, t- 1
 t- 0
task done,task done, task done,task done, t- 4
task done, t- 3 t- 2

task done,t- 5
 t- 7
 t- 6
task done, t- 8
task done,task done, t- 15
task done, t- 12
task done, t- 14
task done, t- 9
task done,task done, t- 11
 t- 10
 t- 13
task done,task done,task done,task done,task done, t- 23
 t- 18
task done, t- 19
task done, t- 17
 t- 16
 t- 22
task done, t- 21
 t- 20
task done,task done, t- 27 t- 31
task done, t- 24task done, t- 29


task done,task done, t- 28
 t- 30
task done,task done, t- 26
 t- 25
task done,task done, task done, t- 34
 t- 32
t- 36
task done,task done, t- 37
 t- 35
task done, t- 33
task done,task done, t- 42
 t- 44
task done,task done, t- 45task done, t- 40
task done, t- 39

task done,task done, t- 38
 t- 43
 t- 41
task done,task done, t- 49task done,task done, t- 46
 t- 47

 t- 48
cost  : 2.1159629821777344
In [8]:
import threading
import time


def run(n):
    print("task", n)
    time.sleep(2)
    print("task has done!")
    
    
start_time = time.time()
for i in range(50):
    t = threading.Thread(target=run, args=("t-%s" % i,))
    t.setDaemon(True)  # 把当前线程设置为守护线程,一定在start前设置
    t.start()
print(threading.current_thread(), threading.active_count())
print(time.time() - start_time)
 
task task t-1
t-0
task t-2
task t-3
task t-4
task t-5
task t-6
task t-7
task t-8
task t-9
task t-10
task t-11
tasktask t-13
 t-12
task t-14
task t-15
task t-16
task t-17
task t-18
task t-19
task t-20
task t-21
task t-22
task t-23
task t-24
task t-25
task t-26
task t-27
task t-28
task t-29
task t-30
task t-31
task t-32
task t-33
task t-34
task t-35
task t-36
task t-37
task t-38
task t-39
task t-40
task t-41
task t-42
task t-43
task t-44
task t-45
task t-46
task t-47
task t-48
task t-49
<_MainThread(MainThread, started 14164)> 55
0.09246182441711426
 

注意观察实验结果,并没有执行打印task has done,并且程序执行时间极其短。 这是因为在主线程启动子线程前把子线程设置为守护线程。 只要主线程执行完毕,不管子线程是否执行完毕,就结束。但是会等待非守护线程执行完毕 主线程退出,守护线程全部强制退出。皇帝死了,仆人也跟着殉葬

 

实例四:线程锁(互斥锁Mutex)

 

介绍一下python的GIL,全局解释器锁。并不是python的特性,是实现python解析器的时候引入的概念。这个锁是为了保证同一份数据不能被多个线程同时修改。因为cpython是使用c封装的,所以线程也是用c语言实现的,在和cpu交互的时候使用的c接口。(java,c + +等语言的自己实现的线程,所以自己可以直接控制cpu),而python就创造了一个全局解释器锁,来保证同一份数据不能被多个线程同时修改。 所以,这就造成了python的一个缺陷,无论多少核的机器,同一时刻只能有一个线程在执行。jpython没有这个问题。

In [9]:
import time
import threading


def run():
    lock.acquire()  #修改数据前加锁
    global num
    num += 1
    lock.release()  # 修改完后释放


lock = threading.Lock()
num = 0
t_objs = []
for i in range(1000):
    t = threading.Thread(target=run)
    t.start()
    t_objs.append(t)
for t in t_objs:  # 循环线程实例列表,等待子线程执行完毕
    t.join()
print(num)
 
1000
In [17]:
import time
import threading


def run():
    # lock.acquire()  #修改数据前加锁
    global num
    num += 1
    # lock.release()  # 修改完后释放


# lock = threading.Lock()
num = 0
t_objs = []
for i in range(1000):
    t = threading.Thread(target=run)
    t.start()
    t_objs.append(t)
for t in t_objs:  #循环线程实例列表,等待子线程执行完毕
    t.join()
print(num)
 
1000
 

注意:gil只是为了减低程序开发复杂度。但是在2.几的版本上,需要加用户态的锁(gil的缺陷)而在3点几的版本上,加锁不加锁都一样。

 

python多进程

 

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

创建进程的方式和创建线程的方式类似:

实例化一个multiprocessing.Process的对象,并传入一个初始化函数对象(initial function )作为新建进程执行入口;
继承multiprocessing.Process,并重写run函数;

In [11]:
# 方式1:
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())
 
subprocess pid: 8800
current process pid: 15120
In [18]:
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())
 
subprocess pid: 14772
current process pid: 15120
 

python多线程与多进程比较

 

先来看两个例子:

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

In [20]:
def tstart(arg):
    var = 0
    for i in range(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))
 
Two thread cost time: 7.077333211898804
Main thread cost time: 3.5601541996002197
In [21]:
def tstart(arg):
    var = 0
    for i in range(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()
    t1.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))
 
Two thread cost time: 3.669692039489746
Main thread cost time: 3.430530309677124
 

上面的例子如果只开启t1和t2两个线程中的一个,那么运行时间和主线程基本一致。 使用两个进程进行上面的操作:

In [24]:
def pstart(arg):
    var = 0
    for i in range(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))
 
Two process cost time: 0.08162665367126465
Current process cost time: 3.4535038471221924
 

concurrent.futures.ThreadPoolExecutor并发库详解

 

Python3.2带来的新版功能。是Python并发执行的标准库。

这个模块具有线程池和进程池、管理并行编程任务、处理非确定性的执行流程、进程/线程同步等功能。

concurrent.futures 是两个文件放在一起作为这个模块,因为concurrent文件夹下只有futures这个文件夹,而futures下有两个主要文件thread.py和process.py :

深度理解,参见《Python并行编程 中文版》、《concurrent.futures官方文档》 ———————————————— 版权声明:本文为CSDN博主「quantLearner」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/The_Time_Runner/article/details/99652083

 
  1. 它可以解决大部分的复杂问题      【但并不是全部,如果尝试后效果不好,还需要使用他们的高级用法】

  2. 而且统一了线程和进程的用法

concurrent.future 提供了 ThreadPoolExecutor 和 ProcessPoolExecutor 两个类,其实是对 线程池和进程池 的进一步抽象,而且具有以下特点:

  1. 主程序可以获取子程序的状态和返回值

  2. 子程序完成时,主程序能立刻知道

 

此模块由以下部分组成:

concurrent.futures.Executor: 这是一个虚拟基类,提供了异步执行的方法。 submit(function, argument): 调度函数(可调用的对象)的执行,将 argument 作为参数传入。 map(function, argument): 将 argument 作为参数执行函数,以 异步 的方式。 shutdown(Wait=True): 发出让执行者释放所有资源的信号。 concurrent.futures.Future: 其中包括函数的异步执行。Future对象是submit任务(即带有参数的functions)到executor的实例。 Executor是抽象类,可以通过子类访问,即线程或进程的 ExecutorPools 。因为,线程或进程的实例是依赖于资源的任务,所以最好以“池”的形式将他们组织在一起,作为可以重用的launcher或executor。

 

ThreadPoolExecutor

 

ThreadPoolExecutor 是 Executor 的子类,它使用线程池来异步执行调用。

concurrent.futures.ThreadPoolExecutor(max_workers=None, thread_name_prefix='', initializer=None, initargs=())

Executor 的一个子类,使用最多 max_workers 个线程的线程池来异步执行调用。如果 max_workers 为 None 或没有指定,将默认为机器处理器的个数。

 

效率验证

 

求最大公约数,测试数据如下

In [27]:
def gcd(pair):
    # 最大公约数
    a, b = pair
    low = min(a, b)
    for i in range(low, 0, -1):
        if a % i == 0 and b % i == 0:
            return i


numbers = [(1963309, 2265973), (2030677, 3814172), (1551645, 2229620), (2039045, 2020802)]
 

无并发

In [33]:
sum = 0
for i in range(20):
    start = time.time()
    results = list(map(gcd, numbers))
    end = time.time()
    sum += end - start

print(sum/20)          
 
0.326509153842926
 

多线程

In [35]:
from concurrent.futures import ThreadPoolExecutor
sum = 0
for i in range(20):
    start = time.time()
    pool = ThreadPoolExecutor(max_workers=10)
    results = list(pool.map(gcd, numbers))
    end = time.time()
    sum += end - start

print(sum/20)              
 
0.3289961576461792
 

分析:由于全局解释器锁GIL的存在,多线程无法利用多核CPU进行并行计算,而是只使用了一个核,加上本身的开销,计算效率更低了。

通过 资源管理器 查看 CPU 使用率:25%左右    【4核,用了一个】

 

多进程

In [43]:
from concurrent.futures import ProcessPoolExecutor
import time


def gcd(pair):
    # 最大公约数
    a, b = pair
    low = min(a, b)
    for i in range(low, 0, -1):
        if a % i == 0 and b % i == 0:
            return i


numbers = [(1963309, 2265973), (2030677, 3814172), (1551645, 2229620), (2039045, 2020802)]

if __name__ == '__main__':

    sum = 0
    for i in range(20):
        start = time.time()
        pool2 = ProcessPoolExecutor(max_workers=3)
        results2 = list(pool2.map(gcd, numbers))
        end = time.time()
        sum += end - start

    print(sum / 20)  # 0.2696140885353088
 

分析:利用多核CPU并行计算,比多线程快了点,但是由于本身的开销,还是没有无并发效率高,

通过 资源管理器 查看 CPU 使用率:75%左右     【4核,用了三个,max_workers=3】

这主要是数据量太小了,体现不出并发的优势,于是我把数据量稍微加大点

numbers = [(1963309, 2265973), (2030677, 3814172), (1551645, 2229620), (2039045, 2020802)] * 10 重新测试,无并发 7s,多进程 2s,效果明显提高。

注意,在使用多进程时,必须把 多进程代码 写在 if name == 'main' 下面,否则异常,甚至报错

concurrent.futures.process.BrokenProcessPool: A process in the process pool was terminated abruptly while the future was running or pending.

小结:多线程不适合计算密集型,适合IO密集型,后面我会验证,多进程适合计算密集型。

参考资料:

https://www.jianshu.com/p/b9b3d66aa0be