Python并发之多线程

线程

线程(Thread)也叫轻量级进程,是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。线程自己不拥有系统资源,只拥有一点在运行中必不可少的资源,但它可与同属的一个进程的其它线程共享进程所拥有的全部资源。一个线程可以创建和撤销另一个线程,同一进程中的多个线程之间可以并发执行。

多线程

多线程是一个可以提高程序运行效率的方法。一些按顺序执行的程序可以使用多线程实现并行执行,从而提高整体效率。然而,多线程也不是可以提升所有程序的执行效率。执行的程序分为CPU密集型I/O密集型两种,多线程技术比较适用于后者。

因为在串行结构中读写磁盘或者进行网络通信的时候CPU是闲着的,毕竟网络比磁盘要慢几个数量级,磁盘比内存要慢几个数量级,内存又比CPU慢几个数量级。

比如你使用单线程对多个网站发送请求,如果这多个网站中的其中2、3个网站的响应速度很慢,而其他网站需要等待这几个网站加载完成才发送请求,这样就会影响了整体的效率。而使用多线程并发请求这多个网站,就可以在那些响应较慢的网站加载的同时去请求其他网站,大大提升整个程序的运行效率。

GIL锁

GIL的全称是Global Interpreter Lock(全局解释器锁),Python最初的设计理念在于,为了解决多线程之间数据完整性和状态同步的问题,设计为在任意时刻只能由一个线程在解释器中运行。因此Python中的多线程是表面上的多线程(同一时刻只有一个线程),不是真正的多线程。

真正意义上的多线程是由CPU来控制的,Python中的多线程是由GIL控制的。如果一个CPU密集型的程序,用C语言写,运行在一个四核处理器上,采用多线程的话最多可以获得4倍的效率提升。但是用Python写的话,效率不会提高,甚至会变慢,因为除了程序本身执行外,还多了线程切换所花的时间。

因此,Python多线程相对更适合写I/O密集型的程序,真正对效率要求高的CPU密集型程序都用C/C++去写。

Python多线程

在Python中,我们一般使用threading模块来实现多进程操作。

threading模块中包含了关于线程操作的丰富功能,包括:常用的线程函数、线程对象、锁对象、递归锁对象、事件对象、条件变量对象、信号量对象、定时器对象、栅栏对象。

threading.Thread

我们使用threading模块中的Thread类来创建线程对象
threading.Thread(group=None, target=None, name=None, args=(), kwargs={}, *, daemon=None)

下面是Thread的参数说明

  • group:默认为None(该参数是为了以后实现ThreadGroup类而保留的)
  • target:在run方法中调用的可调用对象,即需要开启线程的可调用对象,比如函数或方法。
  • name:线程名称,默认为“Thread-N”形式的名称,N为较小的十进制数。
  • args:在参数target中传入的可调用对象的参数元组,默认为空元组()。
  • kwargs:在参数target中传入的可调用对象的关键字参数字典,默认为空字典{}。
  • daemon:默认为None,即继承当前调用者线程(即开启线程的线程,一般就是主线程)的守护模式属性,如果不为None,则无论该线程是否为守护模式,都会被设置为“守护模式”。

线程对象的一些重要方法

  • start():开启线程活动。它将使得run()方法在一个独立的控制线程中被调用,需要注意的是同一个线程对象的start()方法只能被调用一次,如果调用多次,则会报RuntimeError错误。
  • run():此方法代表线程活动。
  • join(timeout=None):让当前调用者线程(一般为主线程)等待,直到线程结束。
  • daemon:表示该线程是否为守护线程,True或者False。设置一个线程的daemon必须在线程的start()方法之前,否则会报RuntimeError错误。这个值默认继承自创建它的线程,主线程默认是非守护线程,所以在主线程中创建的线程默认都是非守护线程的,即daemon=False。
直接创建
import time
import random
import threading


def func(name):
    s = random.randint(1, 5)
    print(f'current thread is {name}, sleeping {s}s.')
    time.sleep(s)
    print(f'thread {name} is over')


if __name__ == '__main__':
    for i in range(1, 5):
        t = threading.Thread(target=func, args=(i,))
        t.start()
    print('Main Thread')

结果如下

current thread is 1, sleeping 5s.
current thread is 2, sleeping 3s.
current thread is 3, sleeping 3s.
current thread is 4, sleeping 2s.
Main Thread
thread 4 is over
thread 2 is over
thread 3 is over
thread 1 is over

上面例子开启了4个线程,4个线程并发执行任务,先完成任务的线程先输出结果。

继承创建
import time
import random
import threading


class Func(threading.Thread):
    def __init__(self, name):
        super().__init__()
        self.name = name

    def run(self):
        s = random.randint(1, 5)
        print(f'current thread is {self.name}, sleeping {s}s.')
        time.sleep(s)
        print(f'thread {self.name} is over')


if __name__ == '__main__':
    for i in range(1, 5):
        t = Func(str(i))
        t.start()
    print('Main Thread')

结果如下

current thread is 1, sleeping 3s.
current thread is 2, sleeping 4s.
current thread is 3, sleeping 3s.
current thread is 4, sleeping 5s.
Main Thread
thread 1 is over
thread 3 is over
thread 2 is over
thread 4 is over

线程对象的start()实际上调用了Func类中的run()方法来开启一个线程。

join方法的简单示例

在第一个例子的基础上加入join方法

import time
import random
import threading


def func(name):
    s = random.randint(1, 5)
    print(f'current thread is {name}, sleeping {s}s.')
    time.sleep(s)
    print(f'thread {name} is over')


if __name__ == '__main__':
    print('Main Thread start')
    tlist = []
    for i in range(1, 5):
        t = threading.Thread(target=func, args=(i,))
        t.start()
        tlist.append(t)
    for t in tlist:
        t.join()
    print('do something')
    print('Main Thread over')

结果如下

Main Thread start
current thread is 1, sleeping 4s.
current thread is 2, sleeping 1s.
current thread is 3, sleeping 5s.
current thread is 4, sleeping 1s.
thread 2 is over
thread 4 is over
thread 1 is over
thread 3 is over
do something
Main Thread over

先开启所有子线程,然后放到线程列表tlist中,在对每一个线程调用join()方法来进行阻塞,直到每个子线程执行完毕后,才会继续执行主线程后面的代码。

Python线程池

系统启动一个新线程的成本是很高的,因为它涉及与操作系统的交互。在这种情况下,使用线程池可以很好地提升性能,尤其是当程序中需要创建大量生存期很短暂的线程时,更应该考虑使用线程池。

线程池在系统启动时即创建大量空闲的线程,程序只要将一个函数提交给线程池,线程池就会启动一个空闲的线程来执行它。当该函数执行结束后,该线程并不会死亡,而是再次返回到线程池中变成空闲状态,等待下一个函数。

此外,使用线程池可以有效地控制系统中并发线程的数量。当系统中包含有大量的并发线程时,会导致系统性能急剧下降,甚至导致Python解释器崩溃,而线程池的最大线程数参数可以控制系统中并发线程的数量不超过此数。

python线程池的使用

在过去,我们可以使用第三方模块threadpool来创建线程池。但是现在主流的使用线程池的模块是python3中自带的模块concurrent.futures模块中的ThreadPoolExecutor,如果对threadpool感兴趣的小伙伴可以自行搜索相关的信息。

下面主要介绍一下ThreadPoolExecutor的使用方法。

import time
import random
from concurrent.futures import ThreadPoolExecutor


def func(name):
    s = random.randint(1, 5)
    print(f'current thread is {name}, sleeping {s}s.')
    time.sleep(s)
    print(f'thread {name} is over')


if __name__ == '__main__':
    with ThreadPoolExecutor(max_workers=3) as t:
        for i in range(1, 6):
            t.submit(func, i)

结果如下

current thread is 1, sleeping 1s.
current thread is 2, sleeping 1s.
current thread is 3, sleeping 2s.
thread 1 is over
current thread is 4, sleeping 2s.
thread 2 is over
current thread is 5, sleeping 4s.
thread 3 is over
thread 4 is over
thread 5 is over

创建一个最大容纳数量为3的线程池对象t,通过submit提交执行的函数到线程池中,但线程池中的某个线程(thread 1)执行完成,则把空闲的线程(thread 4)放入到池子中,直到所有线程执行完成则程序结束。

posted @ 2020-09-25 11:47  蓝莓薄荷  阅读(3933)  评论(0编辑  收藏  举报