【python内功修炼006】:基于threading模块的多线程操作(详解)

一、threading模块介绍

注: 本文使用的Python版本是Python3.8,threading模块内容较多,将分成两篇博客介绍本文主要着重于threading的基本介绍和使用。

Python的线程操作在python2.7和python3版本中引入threading模块,它是基于原thread模块上构造成的较高级别线程接口。thread模块所有的功能在threading中都能得到体现。参考: https://docs.python.org/zh-cn/3.7/library/threading.html

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

二、:threading模块主要对象

对象 功能
Thread 执行(开启)线程对象
Lock Lock锁,线程同步
RLock 和lock用法基本一致,但是对RLock进行多次acquire()操作,程序不会阻塞。也叫递归锁
Condition 条件变量对象,使得一个线程等待另一个线程满足特定条件
Event 条件变量的通用版本,任意数量的线程等待某个事件的发生,该事件发生后所有线程将被激活
Semaphore 为线程间的共享资源提供了一个计数器,如果没有可用资源时会被阻塞
BoundedSemaphone 与Semaphore相似,不过它不允许超过初始值
Timer 与Thread相似,不过运行前要等待一段时间
Barrier 创建一个”障碍“,必须要达到指定数量的线程才能继续

三、threading.Thread对象

threading模块执行和开启线程,是基于Tread对象实现的, 目前Thread还没有优先级和线程组的功能,而且创建的线程也不能被销毁、停止、暂定、恢复或中断。

1、语法

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

如果这个类的初始化方法被重写,请确保在重写的初始化方法中做任何事之前先调用threading.Thread类的__init__方法。

2、参数

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

3、常用方法

  • start(): 开启线程活动。它将使得run()方法在一个独立的控制线程中被调用,需要注意的是同一个线程对象的start()方法只能被调用一次,如果调用多次,则会报RuntimeError错误。
  • run(): 此方法代表线程活动。如果是继承类,则需要将执行代码放在run()下
  • join(timeout=None): 让当前调用者线程等待,直到线程结束,timeout参数是以秒为单位的浮点数,用于设置操作超时的时间,返回值为None。join方法可以被调用多次。如果对当前线程使用join方法(即线程在内部调用自己的join方法),或者在线程没有开始前使用join方法,都会报RuntimeError错误。
  • name: 线程的名称字符串,多个线程可以赋予相同的名称,初始值由初始化方法来设置。
  • ident: 线程的标识符,如果线程还没有启动,则为None。ident是一个非零整数,参见threading.get_ident()函数。当线程结束后,它的ident可能被其他新创建的线程复用,当然就算该线程结束了,它的ident依旧是可用的。
  • is_alive(): 线程是否存活,返回True或者False。在线程的run()运行之后直到run()结束,该方法返回True。
  • daemon: 表示该线程是否是守护线程,True或者False。设置一个线程的daemon必须在线程的start()方法之前,否则会报RuntimeError错误。这个值默认继承自创建它的线程,主线程默认是非守护线程的,所以在主线程中创建的线程默认都是非守护线程的,即daemon=False。

四、python开启线程的两种方法

1、使用threading.Thread 线程对象

from threading import Thread
import time
import random

# 直接调用Thread 线程对象
def Opening_method(name):
    print('%s 开启线程 ' % name)
    time.sleep(random.randint(1, 3))
    print('%s 关闭线程  ' % name)


if __name__ == "__main__":
    obj = Thread(target=Opening_method, args=('金鞍少年',))

    obj.start()

    print('主进程/主线程')

    ''' 打印结果
    金鞍少年 开启线程 
    主进程/主线程
    金鞍少年 关闭线程  
    
    '''

2、继承父类threading.Thread

rom threading import Thread
import time
import random

class MyThread(Thread):  # 继承父类threading.Thread方法
   
    def __init__(self, name):
        super().__init__()
        self.name = name

    def run(self):
        print('%s  线程开启 ' % self.name)
        time.sleep(random.randint(1, 3))
        print('%s 线程结束 ' % self.name)

if __name__ == "__main__":
    obj = MyThread('金鞍少年')
    obj.start()
    print('主进程/主线程')
    
    print('主进程/主线程')

    ''' 打印结果
    金鞍少年 开启线程 
    主进程/主线程
    金鞍少年 关闭线程  
    
    '''

五、Threading模块主要函数

1、主要函数介绍

​ 在Python3中方法名和函数名统一成了以字母小写加下划线的命令方式,但是Python2.x中threading模块的某些以驼峰命名的方法和函数仍然可用,如threading.active_count()和threading.activeCount()是一样的。

​ 通常情况下,Python程序启动时,Python解释器会启动一个继承自threading.Thread的threading._MainThread线程对象作为主线程,所以涉及到threading.Thread的方法和函数时通常都算上了这个主线程的,比如在启动程序时打印threading.active_count()的结果就已经是1了。

函数名 功能
threading.active_count() 返回当前存活的threading.Thread线程对象数量,等同于len(threading.enumerate())。
threading.current_thread() 返回此函数的调用者控制的threading.Thread线程对象。如果当前调用者控制的线程不是通过threading.Thread创建的,则返回一个功能受限的虚拟线程对象
threading.get_ident() 返回当前线程的线程标识符。注意当一个线程退出时,它的线程标识符可能会被之后新创建的线程复用。
threading.enumerate() 返回当前活着的Thread对象的列表。该列表包括守护线程、由current_thread()创建的虚假线程对象和主线程。它不包括已终止的线程和尚未开始的线程
threading.main_thread() 返回主线程对象,通常情况下,就是程序启动时Python解释器创建的threading._MainThread线程对象
threading.stack_size([size]) 返回创建线程时使用的堆栈大小。也可以使用可选参数size指定之后创建线程时的堆栈大小,size可以是0或者一个不小于32KiB的正整数。如果参数没有指定,则默认为0。

2、代码实例

from threading import Thread
import threading

def work():
    import time
    time.sleep(3)
    print('子线程对象:', threading.current_thread())


if __name__ == '__main__':
    # 在主进程下开启线程
    t = Thread(target=work)
    t.start()

    print('调用线程对象:', threading.current_thread())  # 返回此函数的调用者控制的threading.Thread线程对象
    print('调用线程对象名字: ',threading.current_thread().getName())  # 获取主线程名字
    print('运行线程:', threading.enumerate())  # 返回当前活着的Thread对象的列表。
    print('存活线程数量:', threading.active_count())  # 返回当前存活的Thread线程对象数量
    print('主线程对象:', threading.main_thread())  # 返回主线程对象
    print('主线程/主进程')

    '''
    打印结果:
    调用线程对象: <_MainThread(MainThread, started 14308)>
    调用线程对象名字:  MainThread
    运行线程: [<_MainThread(MainThread, started 14308)>, <Thread(Thread-1, started 14072)>]
    存活线程数量: 2
    主线程对象: <_MainThread(MainThread, started 14308)>
    主线程/主进程
    子线程对象: <Thread(Thread-1, started 14072)>
    '''

六、守护线程和非守护线程

1、守护线程介绍

  • 守护线程:只有所有守护线程都结束,整个Python程序才会退出,但并不是说Python程序会等待守护线程运行完毕,相反,当程序退出时,如果还有守护线程在运行,程序会去强制终结所有守护线程,当所有守护线程都终结后,程序才会真正退出。

  • 非守护线程:一般创建的线程默认就是非守护线程,包括主线程也是,即在Python程序退出时,如果还有非守护线程在运行,程序会等待直到所有非守护线程都结束后才会退出。

2、守护进程实例 (1)

'''

无论是进程还是线程,都遵循:守护xxx会等待主xxx运行完毕后被销毁

需要强调的是:运行完毕并非终止运行

'''

from threading import Thread
import time

def guard(name):
    time.sleep(0.00001)
    print('%s say hello' % name)

if __name__ == '__main__':

    t = Thread(target=guard, args=('金鞍少年',))
    t.setDaemon(True)  # 必须在t.start()之前设置 等同于 t.daemon = True
    t.start()

    print('主线程')
    print(t.is_alive())  # 子进程还在运行
    '''
    主线程
    True
    '''

思考:为什么主线程结束了,子线程还是存活状态?

3、守护进程实例 (2)

from threading import Thread
import time

def one():
    print('one 开启')
    time.sleep(1)
    print("one 关闭")

def two():
    print('two 开启')
    time.sleep(3)
    print('two 开启')


if __name__ == '__main__':
    t1 = Thread(target=one)  # 线程一
    t2 = Thread(target=two)  # 线程二

    t1.daemon = True  # 线程1 设置守护线程
    t1.start()
    t2.start()
    print("main-------")
    
    '''结果:
    
    one 开启
	two 开启
	main-------
	one 关闭
	two 开启
	
	
	思考:线程one 是守护线程,当主线程结束之后,one 关闭不应该打印输出的,但是打印结果却有?
	
    '''
   

结论:

运行完毕,并不意味着运行终止了

主进程在其代码结束后就已经算运行完毕了(守护进程在此时就被回收),然后主进程会一直等非守护的子进程都运行完毕后回收子进程的资源(否则会产生僵尸进程),才会结束,

主线程在其他非守护线程运行完毕后才算运行完毕(守护线程在此时就被回收)。因为主线程的结束意味着进程的结束,进程整体的资源都将被回收,而进程必须保证非守护线程都运行完毕后才能结束。

七、线程同步(Look)

1、代码案例

需求:构建100个线程,每个线程对主进程定义的变量n=100 做减1 操作,得到结果应该是0

from threading import Thread
import time

def work():
    global n
    temp = n
    time.sleep(0.1)
    n = temp-1

if __name__ == '__main__':
    n = 100
    l = []
    for i in range(100):
        T = Thread(target=work)  # 生成100个线程
        l.append(T)
        T.start()

    for T in l:
        T.join()

    print(n)  # 结果:99

结论:

线程开销小,执行速度很快,在time.sleep(0.1)等待期间,100个线程拿到的temp都是100,执行减1操作,结果99,如果想让结果变为0,则需要给线程加上Lock锁

2、Look线程同步案例

from threading import Thread ,Lock
import time

def work():
    global n
    R.acquire()  # 请求锁
    temp = n
    time.sleep(0.01)
    n = temp-1
    R.release()  # 释放锁


if __name__ == '__main__':
    R = Lock()  # 构建锁对象
    n = 100
    l = []
    for i in range(100):
        T = Thread(target=work)  # 生成100个线程
        l.append(T)
        T.start()
    for T in l:
        T.join()
    print(n)  # 结果:0

3、 with语法用于线程同步案例

Lock、RLock、Condition、Semaphore等模块中所有带有acquire()和release()方法的对象,都可以使用with语句。当进入with语句块时,acquire()方法被自动调用,当离开with语句块时,release()语句块被自动调用。

with some_lock:
    # do something
    pass

等同于:

some_lock.acquire()
try:
    # do something
    pass
finally:
    some_lock.release()

八、多线程与多进程的理解(代码实例)

1、 共享数据(内存地址)

多线程可以共享主进程内的数据,但是多进程用的都是各自的数据,无法共享。

'''
测试思路:
	1、在主进程定义一个常用,然后在子进程或者子线程调用修改,看看打印结果有何不同
	2、os.getpid()函数能获取进程/线程的id地址,通过id地址来判断是否指向同一个内存地址

'''
from threading import Thread
from multiprocessing import Process
import os

# 线程
n = 100
def MyThread():
    global n
    n = 1
    print('线程:%s 中 n 的值为:%s' % (os.getpid(), n))

if __name__ == '__main__':
    T1 = Thread(target=MyThread)
    T1.start()

    print('主线程(%s)中n的值为:%s'%(os.getpid(),n))

    '''
    结果:
    线程:13268 中 n 的值为:1
    主进程/主线程(10700)中n的值为:1
    '''

# 进程
n = 100
def MyProcess():
    global n
    n = 1
    print('子进程:%s 中 n 的值为:%s' % (os.getpid(), n))

if __name__ == '__main__':
    P1 = Process(target=MyProcess)
    P1.start()

    print('\n主进程/主线程(%s)中n的值为:%s'%(os.getpid(),n))

    '''
    结果:
    主进程/主线程(10700)中n的值为:100
    子进程:11384 中 n 的值为:1
    '''

2、进程和线程的执行速度

在计算密集型的情况下,多进程执行速度要比多线程速度快;在IO密集型的情况下,多线程速度要比多进程快。

1、计算密集型

# 练习二,测试进程和线程的执行速度
# 
# 测试一 计算密集型
from multiprocessing import Process
import os
import time
from threading import Thread

def task():
    res = 1
    for i in range(1,100000):
        res *= i
    return res

if __name__ == '__main__':

    # print(os.cpu_count())  # 查看电脑的cpu是几核的 ,4核
    T_start = time.time()
    l = []
    for i in range(4):
        # P = Process(target=task)  # 创造4进程  执行时间:7.665618181228638
        P = Thread(target=task)  # 创造4 个线程  执行时间:13.401337385177612
        l.append(P)
        P.start()

    for P in l:
        P.join()

    T_end = time.time()
    print(task())  # 太长了
    print('4个进程/线程执行的时间:%s' % (T_end-T_start))

结论:

由于GIL锁的原因,python的多线程在同一时间 同一个进程中,只有一个线程运行

2、IO密集型业务

from multiprocessing import Process
import os
import time
from threading import Thread

# 测试 IO密集型
def task():
    time.sleep(2)  # 常见的IO等待场景

if __name__ == '__main__':

    # print(os.cpu_count())  # 查看电脑的cpu是几核的 ,4核
    T_start = time.time()
    l = []
    for i in range(100):  #
        # P = Process(target=task)  # 创造100进程  执行时间:7.4927191734313965
        P = Thread(target=task)  # 创造100个线程  执行时间:2.0271480083465576
        l.append(P)
        P.start()

    for P in l:
        P.join()

    T_end = time.time()
    print('100个进程/线程执行的时间:%s' % (T_end-T_start))

结论:

在处理IO业务时,线程和进程执行速度一样,但不同的是,进程需要开辟新的内存空间,内存开销比线程大,大部分时间耗在内存开销上

3、其他案例

基于上面代码实例,可知:

CPU多核利用: Python解释器的线程只能在CPU单核上运行,开销小,但是这也是缺点,因为没有利用CPU多核的特点。Python的多进程是可以利用多个CPU核心的,但也有其他语言的多线程是可以利用多核的。

单核与多核: 一个CPU的主要作用是用来做计算的,多个CPU核心如果都用来做计算,那么效率肯定会提高很多,但是对于IO来说,多个CPU核心也没有太大用处,因为没有输入,后面的动作也无法执行。所以如果一个程序是计算密集型的,那么就该利用多核的优势(比如使用Python的多进程),如果是IO密集型的,那么使用单核的多线程就完全够了。

线程或进程间的切换: 线程间的切换是要快于进程间的切换的。

发布了72 篇原创文章 · 获赞 79 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/weixin_42444693/article/details/105031618