Python的多线程模块threading

概述

Python3的多线程编程中常用的两个模块为:_thread,threading。

推荐使用threading模块。毕竟_thread模块是相当底层的模块,虽然该模块可以让你对线程进行细致的管理,但由于没有提供高级函数,所以用起来比较费劲。(轮子不如threading模块的好使)

以下内容将基于threading模块展开。

创建线程

threading模块支持两种创建线程的方式,分别是函数方式创建,以及使用来包装线程对象的方式。

""" 案例1:函数式创建线程 """

import threading
import time


# 线程执行函数
def run(num):
    time.sleep(1)
    print(f'i am threading{num}')


if __name__ == '__main__':
    # 获取开始的时间戳
    start = time.time()
    # 存放线程的列表,用于阻塞线程
    thread_list = list()
    # 创建4个线程,并将每个创建好的线程对象放到thread_list中
    for i in range(1, 5):
        # 创建线程
        thread = threading.Thread(target=run, args=(i,))
        # 启动该线程
        thread.start()
        thread_list.append(thread)
    
    # 只有线程全部结束,再向下执行
    for j in thread_list:
        j.join()
    
    # 结束的时间戳
    end = time.time()
    # 打印该程序运行了几秒
    print(end-start)
    
# i am threading1
# i am threading3
# i am threading2
# i am threading4
# 1.0026652812957764

函数式创建线程是非常简单的,只需要将实例化Thread对象就可以了。

target参数为线程的执行函数,args参数是一个元组,里面可以存放该执行函数的参数,没有可以省略。

需要注意的是,args元组参数里的逗号,千万不要忘。

""" 案例2:类继承创建线程 """

import threading
import time


# 继承Thread,转变为线程类
class MyThread(threading.Thread):

    def __init__(self, threadId):
        # 必须实现Thread类的init方法
        super().__init__()
        self.threadId = threadId
    
    # 重写执行函数
    def run(self):
        time.sleep(1)
        print(f'i am thread{self.threadId}')


if __name__ == '__main__':
    # 获取开始的时间戳
    start = time.time()
    # 存放线程的列表,用于阻塞线程
    thread_list = list()
    # 创建4个线程,并将每个创建好的线程对象放到thread_list中
    for i in range(1, 5):
        thread = MyThread(i)
        # 启动该线程
        thread.start()
        thread_list.append(thread)
        
    # 只有线程全部结束,再向下执行
    for j in thread_list:
        j.join()
    
    # 结束的时间戳
    end = time.time()
    # 打印该程序运行了几秒
    print(end-start)

# i am thread1
# i am thread3
# i am thread4
# i am thread2
# 1.0024833679199219

一般来说,线程的执行函数是没有返回值的。但如果需要返回值,则可以通过定义一个全局变量来获取。当然,如果使用的是类继承的方式创建,则可以直接使用类属性来接收返回值。

守护线程

守护线程作用是为其他线程提供便利服务。只要当前主线程中尚存任何一个非守护线程没有结束,守护线程就全部工作,只有当最后一个非守护线程结束,守护线程随着主线程一同结束工作。

""" 案例3:守护线程 """

import threading
import time


def run():
    # 无限循环打印
    while True:
        time.sleep(0.3)
        print('i am alive!')


def fly():
    # 打印3次
    for i in range(3):
        time.sleep(0.3)
        print('you alive!')
    print('you die...')


if __name__ == '__main__':
    # 创建线程1,执行函数为run
    thread1 = threading.Thread(target=run)
    # 创建线程2,执行函数为fly
    thread2 = threading.Thread(target=fly)

    # 将线程1设置为守护线程
    thread1.daemon = True

    # 启动这2个线程
    thread1.start()
    thread2.start()
    
# you alive!
# i am alive!
# you alive!
# i am alive!
# i am alive!
# you alive!
# you die...

由上述案例可看出,多线程任务中,如果子线程不是守护线程,主线程运行完毕后会进入停止状态,但是主线程的占用资源没有释放(程序不会退出),一直到所有的子线程全部执行完成后才会释放资源,退出程序。

另外,需要注意,设置守护线程时一定要在start方法之前设置。

threading模块的常用方法

关于这些方法,值得一提的是,threading模块仍然支持 Python 2.x 的以驼峰命名实现的方法,不过笔者还是建议使用下划线的方法。(当然,如果是为了兼容 Python 2.x ,那就请忽略笔者的建议。)

# 启动线程
threading.start()

# 等待至该线程中止,可以达到资源独占的作用
threading.join([time])

# 返回线程是否存活的
threading.is_alive()

# 返回当前的Thread对象
threading.current_thread()

# 设置守护线程
threading.setDaemon(True)

# 代表线程活动的方法,可以在子类型里重载
threading.run()

# 返回一个包含正在运行的线程的list
threading.enumerate()

# 返回正在运行的线程数量,等价于len(threading.enumerate())
threading.active_count()

# 设置线程名
threading.setName()

# 返回线程名
threading.getName()

锁对象与共享变量

多线程同时访问同一变量时,会产生共享变量的问题,造成变量冲突。也就是当线程需要共享数据时,可能存在数据不同步的问题。这时候可以使用threading.Lock来把线程中的变量锁定,使用完再释放。

# 创建锁对象
lock = threading.Lock()

# 获取锁,用于线程同步
lock.acquire()

# 释放锁,开启下一个线程
lock.release()

具体使用可以参考下列案例。

""" 案例4:线程锁 """

import threading


def func():
    # 引用全局变量num
    global num
    # 为num循环加1
    for i in range(1, 1000001):
        # 为下面1行代码上锁
        lock.acquire()
        num += 1
        # 解锁上1行代码
        lock.release()
    # threading.current_thread().name为打印线程名
    print(threading.current_thread().name, num)


if __name__ == '__main__':
    # 计数君
    num = 0

    # 创建锁对象
    lock = threading.Lock()

    # 创建2个线程
    thread1 = threading.Thread(target=func)
    thread2 = threading.Thread(target=func)

    # 为这两个Thread对象(线程)起名
    thread1.name = 'thread_1'
    thread2.name = 'thread_2'

    # 启动这2个线程
    thread1.start()
    thread2.start()

    # 等待线程结束
    thread1.join()
    thread2.join()

    print('程序运行完毕!')

# thread_1 1655942
# thread_2 2000000
# 程序运行完毕!

本文中笔者有提到,可以通过使用全局变量来接收返回值。案例4的代码就相当于是使用了num接收函数func的返回值,即num加1000000的值。

然后,这个案例对线程锁的意义在哪?实际上,当range里的数字比较小时,对程序本身的影响并不大。然而,当range里的数字过大时,也就是代表计算密集度非常大时,就会出现数据的混乱。

""" 去掉线程锁时的打印结果 """

# thread_1 1101612
# thread_2 1367565
# 程序运行完毕!

由上述案例便可看出,线程锁的重要性。当然,可能有些读者会问,为什么案例4第一条打印的是1655942而不是1000000?其实呢,只需要在多线程的角度思考一下就可以相通了。

最后还需要提一下,线程锁不仅仅只能通过threading.Lock()创建,还可以使用threading.RLock()多重锁,用法和threading.Lock()相同。因为threading.Lock()在线程中必须等待锁释放后(release)才能再次上锁。而threading.RLock在同一线程中可用被多次acquire。需要注意的是,在threading.RLock中acquire和release必须成对出现

死锁

在线程共享多个资源的时候,如果两个线程分别占有一部分资源并且同时等待对方的资源,这种情况便会造成死锁。

在Python中的死锁一般都是由于threading.Lock的acquire使用不合理导致的。情景就类似于哲学家吃饭的问题:由于服务员的疏忽,导致2个哲学家手中分别拿到的是2把叉子和2把餐刀,其中一个哲学家说,你给我叉子,我给你餐刀,另一个哲学家则说,你给我餐刀,我给你叉子......

""" 案例5:死锁 """

import threading
import time


# 哲学家a
def philosopher_a():
    if lock_b.acquire():
        print('我有餐刀')
        print('给我叉子')
        time.sleep(0.5)
        if lock_a.acquire():
            print('我给你餐刀')
            lock_a.release()
        lock_b.release()


# 哲学家b
def philosopher_b():
    if lock_a.acquire():
        print('我有叉子')
        print('给我餐刀')
        time.sleep(0.5)
        if lock_b.acquire():
            print('我给你叉子')
            lock_b.release()
        lock_a.release()


if __name__ == "__main__":
    # 创建2个锁对象
    lock_a = threading.Lock()
    lock_b = threading.Lock()

    # 创建2个线程
    thread1 = threading.Thread(target=philosopher_a)
    thread2 = threading.Thread(target=philosopher_b)

    # 启动这2个线程
    thread1.start()
    thread2.start()

上述代码一旦运行,就会使程序挂死,原理很简单,就是2个锁在互相等待对方解锁。你不解锁,我不解锁,那就停住咯。是不是像极了你和女朋友吵架,你不道歉,我不道歉,那就谁也不理谁咯。

所以说,在程序设计中,一定要严格分析锁定的数据,避免死锁的问题,将死锁扼杀在摇篮里才是最安全的。

你可能感兴趣的:(Python的多线程模块threading)