本次分享主要示例创建多线程的两种方法:1、使用threading模块的Thread类的构造器创建线程(创建Thread类的实例,传递一个函数,简单明了)2、继承threading模块的Thread类创建线程类(重写run()方法,面向对象的接口)


一般情况下,我们用到的程序完成特定的功能都是单线程运行的,一条线程指的是进程中一个单一顺序的控制流——程序依次执行每行代码,如果程序在某个位置遇到阻塞(出错),则程序在该处停止退出。我们使用DE工具运行调试出bug的程序,很容易定位到运行出 错位置。


但实际对于GUI程序来说,单线程程序往往功能有限,满足不了需求。要解决这个问题就要涉及多线程的知识。


线程是一种对于非顺序依赖的多个任务进行解耦的技术。多线程可以提高应用的响应效率,当接收用户输入的同时,保持其他任务在后台运行。


多线程应用场景:一个浏览器必须同时下载多张图片,一个web服务器必须能同时响应多个用户请求,GUI应用主线程更新界面、子线程实时处理数据等。


一、进程和线程

1、进程是系统中资源分配和资源调度的基本单位,例如QQ、微信、word、浏览器等,每个独立执行的应用程序在系统中都算是一个进程。


2、线程(thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。


当进程被初始化后,主线程就被创建了。每个进程中都可以同时包含多个线程,例如qq软件包含很多功能,比如收发信息、下载文件、播放音乐等,这些线程(功能执行程序)可以同时独立运行且互不干扰,这就是使用线程的并发机制——一个进程中可以并发多个线程,每条线程并行执行不同的任务。

Python多线程编程之线程的两种创建方式_创建线程

注意:

并发(Concurrency)指同一时刻只有一条指令执行,但多个线程在系统中快速轮流切换执行有限的CPU时间,我们察觉不到任何中断,这是因为相对于人的感觉,CPU转换执行太快,因此,对我们来说,每个进程好像在同时执行一样。

并行(Parallel)指同一时刻有多条进程在多个处理器上同时执行。



二、多线程的优势和缺点

  • 多线程优势


主要优势在于充分利用用户事件间很小的CPU时间,在系统后台轮流处理,使得同一个进程可以同时并发处理多个任务。提高用户的响应速度,使得进程的整体运行效率得到较大提高,同时增强了应用程序的灵活性。


由于同一进程的所有线程是共享同一内存资源,因此,编程更加方便,线程之间很容易通信,不需要特殊的数据传送机制,不需要建立共享存储区或共享文件,从而使得不同任务之间的协调操作与运行、数据的交互、资源的分配等问题更加易于解决。


  • 多线程缺点

大量的线程会占用大量的处理器时间,造成大多数线程进度明显滞后。过多线程调度会带来很大的性能开销。


创建进程和线程的数目会受到内存的限制;确保线程不会妨碍同一进程中其他线程;销毁线程需要了解可能发生的问题并进行处理。


三、Thread类方法


Python中主要用threading模块来支持多线程,Thread类主要方法及说明见下表:

Thread类方法

__init__(self,group=None,target=None,name=None,args=(),kwargs=None,*,daemon=None)

实例化一个线程对象,需要一个可调用的target对象,以及参数args(指定一个元组)或者kwargs(指定一个字典,以关键字参数形式为target指定函数的传入参数)。还可以传递name参数。daemon的值将会设定thread.daemon的属性,指定所构建的线程是否为后台线程。

start()

开始执行该线程

run()

定义线程的方法。(通常应该在子类中重写)

join(timeout=None)

直至启动的线程终止之前一直挂起;除非给出了timeout(单位秒),否则一直被阻塞即让一个线程等待另外一个线程完成的方法。timeout参数,该指定等待被join的线程时间最长为timeout秒。

Thread类属性包括name(线程名)、ident(线程的标识符)、daemon(布尔值,表示线程是否为后台线程)


四、两种创建线程方法


  • 使用threading模块的Thread类的构造器创建线程


1 默认启动主线程

程序默认执行只在一个线程,这个线程称为主线程(默认情况下,名称为MainThread)。创建一个"干活"的主线程:

#threading的current_thread()方法返回当前线程
t = threading.current_thread()
#返回指定线程的名字,其他常用方法,getName()获取线程id,i判断线程是否存活等。
print(t)  #<_MainThread(MainThread, started 14440)>
print(t.getName()) # MainThread
print(t.ident) # 14440
print(t.isAlive()) # True
t.setName("THREAD") #为线程设置名称
print(t.getName()) #THREAD

注:threading.active_count() #获取已激活的线程数


2 创建线程

   创建Thread类的实例,传递一个函数

创建一个线程 

my_thread = threading.Thread() 

创建一个名称为my_thread的线程 

my_thread = threading.Thread(name='my_thread',target=thread_job) 

注:接收参数 target 代表这个线程要完成的任务,需要做些什么,需自行定义可调用函数。

#自定义任务函数
def action(i):
    print(threading.current_thread().getName()+'开始运行:'+str(i))

my_thread = threading.Thread(target=action,args=(1,)) #创建线程

my_thread.start() #调用线程对象my_thread的start()方法启动线程。


3、创建多线程

创建一个多线程,启动多个线程名称依次为Thread-1、Thread-2、....Thread-n等:

import threading
from datetime import datetime
import time

def action(max):
    for i in range(max):
        time.sleep(0.1)
        print('当前线程%s,运行结束时间为:%s' % (threading.current_thread().getName(), datetime.today()))


def main():
    threads = [threading.Thread(target=action,args=(5,)) for i in range(3)]

    [threads[i].start() for i in range(len(threads))]


if __name__=='__main__':
    main()

运行以上代码,启动3个线程,运行结果如下,默认Thread-1、Thread-2、Thread-3三个线程。注意观察,这三个线程执行没有先后顺序,以并发的方式执行,线程间轮换执行一段时间,从而给我们带来多个线程同时执行的错觉。

当前线程Thread-2,运行结束时间为:2020-12-19 16:57:21.503568
当前线程Thread-1,运行结束时间为:2020-12-19 16:57:21.503568
当前线程Thread-3,运行结束时间为:2020-12-19 16:57:21.503568
当前线程Thread-3,运行结束时间为:2020-12-19 16:57:21.604272
当前线程Thread-1,运行结束时间为:2020-12-19 16:57:21.604272
当前线程Thread-2,运行结束时间为:2020-12-19 16:57:21.604272
当前线程Thread-2,运行结束时间为:2020-12-19 16:57:21.705032
当前线程Thread-1,运行结束时间为:2020-12-19 16:57:21.705032
当前线程Thread-3,运行结束时间为:2020-12-19 16:57:21.705032
当前线程Thread-1,运行结束时间为:2020-12-19 16:57:21.805765
当前线程Thread-2,运行结束时间为:2020-12-19 16:57:21.805765
当前线程Thread-3,运行结束时间为:2020-12-19 16:57:21.805765
当前线程Thread-3,运行结束时间为:2020-12-19 16:57:21.906463
当前线程Thread-2,运行结束时间为:2020-12-19 16:57:21.906463
当前线程Thread-1,运行结束时间为:2020-12-19 16:57:21.906463

被操作系统轮询分配CPU执行时间,时间耗尽中断此线程执行,然后切换给其他线程,以此轮换运行......


  • 继承threading模块的Thread类创建线程类


MyThread子类的构造函数必须先调用其父类(Thread)的构造函数,并重写run()方法,增加自定义调试信息的输出以及结果的输出方法。

单线程模式简单一次调用每个函数,并将结果打印出来;而多线程将3个计算任务运行在3个并行的线程中,不会立即显示结果,等到所有线程都join后,再调用getRes()方法显示每个函数的返回值。

以下代码展示了 多线程threading 模块如何在后台运行任务,且不影响主程序的继续运行。

#继承Thread类创建线程类

class MyThread(threading.Thread):
    def __init__(self,func,args,name=''):
        threading.Thread.__init__(self)
        self.func=func
        self.name = name
        self.args=args
   
   #重写run()方法,此处自定义设置
    def run(self):  
        start = time.time()
        print('当前线程%s,开始运行时间为:%s' % (self.name, datetime.today()))
        self.res = self.func(*self.args)
        stop = time.time()
        print('当前线程%s,运行结束时间为:%s' % (self.name, datetime.today()))
        ret=stop - start
        print('当前线程%s,执行消耗时间为:%s' % (self.name,ret))
    
    #返回结果
    def getRes(self):
        return self.res  

#完成以下三个任务
# 斐波那契
def fib(x):
    time.sleep(0.005)
    if x < 2:
        return 1
    return fib(x - 1) + fib(x - 2)

# 阶乘
def fac(x):
    time.sleep(0.1)
    if x < 2:
        return 1
    return x * fac(x - 1)

# 累加
def sum(x):
    time.sleep(0.1)
    if x < 2:
        return 1
    return x + sum(x - 1)

#功能函数名称列表
funcs = [fib, fac, sum]
#传入计算参数
n = 10

#主函数执行体
def main():
    nfuncs = range(len(funcs))

    # 单线程
    print('单线程模式')
    for i in nfuncs:
        print('当前线程%s,开始运行时间为:%s' % (funcs[i].__name__, datetime.today()))
        print(funcs[i](n))
        print('当前线程%s,运行结束时间为:%s' % (funcs[i].__name__, datetime.today()))

    # 多线程
    print('多线程模式')

    threads = [MyThread(funcs[i], (n,), funcs[i].__name__) for i in nfuncs]

    [threads[i].start() for i in nfuncs] #开始创建所有的线程

    for i in nfuncs:
        threads[i].join()
        print(threads[i].getRes())  #等待所有的线程执行完毕

    print('任务结束')


if __name__ == '__main__':
    main()

 对比单线程和多线程的结果:

单线程模式
当前线程fib,开始运行时间为:2020-12-19 19:15:35.602806
89
当前线程fib,运行结束时间为:2020-12-19 19:15:36.626262
当前线程fac,开始运行时间为:2020-12-19 19:15:36.626262
3628800
当前线程fac,运行结束时间为:2020-12-19 19:15:37.633569
当前线程sum,开始运行时间为:2020-12-19 19:15:37.633569
55
当前线程sum,运行结束时间为:2020-12-19 19:15:38.640929
多线程模式
当前线程fib,开始运行时间为:2020-12-19 19:15:38.640929
当前线程fac,开始运行时间为:2020-12-19 19:15:38.641894
当前线程sum,开始运行时间为:2020-12-19 19:15:38.641894
当前线程sum,运行结束时间为:2020-12-19 19:15:39.649204
当前线程sum,执行消耗时间为:1.007310152053833
当前线程fac,运行结束时间为:2020-12-19 19:15:39.649204
当前线程fac,执行消耗时间为:1.007310152053833
当前线程fib,运行结束时间为:2020-12-19 19:15:39.699070
当前线程fib,执行消耗时间为:1.0581409931182861
89
3628800
55
任务结束

总的来说,把任务函数的运行分配到多个线程上,每个线程处理一个请求,本线程阻塞不影响新请求进入,这能一定程度上加快用户的操作响应。

多线程应用设计需要协调多个线程之间需要共享数据资源,通常程序无法准确控制线程的轮换执行,容易突然出现“错误情况”,因为系统中线程调度具有一定的随机性。多线程threading 模块提供了多个同步操作,包括线程锁、事件、条件变量和信号量。

Python中可通过线程通信来保证线程的多任务协调运行,主要包括:使用Condition控制线程通信、使用队列Queue控制线程通信、使用Event控制线程通信。