Bootstrap

多线程概念及其方法讲解

1 认识多线程

1.1 线程的概念

线程是CPU分配资源的基本单位。当一程序开始运行,这个程序就变成了一个进程,而一个进程相当于一个或者多个线程。当没有多线程编程时,一个进程相当于一个主线程;当有多线程编程时,一个进程包含多个线程(含主线程)。使用线程可以实现程序大的开发。

多线程的程序设计的特点就是能够提高程序执行效率和处理速度。

2 线程创建与管理

2.1 创建线程

Python提供了threading模块来进行线程的创建与管理,创建一个线程需要指定该线程执行的任务(函数名)、以及该函数需要的参数,示例代码如下:

import threading

def demo_one(args):
    for i in range(5):
        print("demo1在执行:", i)


if __name__ == '__main__':
    # 创建线程
    thread_one = threading.Thread(target=demo_one, args=("参数",))
    # 设置守护线程【可选】
    thread_one.setDaemon(True)
    # 启动线程
    thread_one.start()

1.2 设置守护线程

线程是程序执行的最小单位,Python在进程启动起来后,会自动创建一个主线程,之后使用多线程机制可以在此基础上进行分支,产生新的子线程。子线程启动起来后,主线程默认会等待所有线程执行完成之后再退出。但是我们可以将子线程设置为守护线程,此时主线程任务一旦完成,所有子线程将会和主线程一起结束(就算子线程没有执行完也会退出)。

thread_one.setDaemon(True)
# 方法二
thread_one = threading.Thread(target=demo_one, args=("参数",),daemon=True)

1.3 设置线程阻塞

我们可以用join()方法使主线程陷入阻塞,以等待某个线程执行完毕。因此这也是实现线程同步的一种方式。参数 timeout 可以用来设置主线程陷入阻塞的时间,如果线程不是守护线程,即没有设置daemon为True,那么参数 timeout 是无效的,主线程会一直阻塞,直到子线程执行结束。

# -*- coding:utf-8 -*-
import threading


def func_one(args):
    for i in range(5):
        print("func_one在执行:", i)


def func_two(args):
    for i in range(5):
        print("func_two在执行:", i)


if __name__ == '__main__':
    # 创建线程
    thread_one = threading.Thread(target=func_one, args=("参数",), daemon=True)
    thread_two = threading.Thread(target=func_two, args=("参数",), daemon=True)

    thread_one.start()
    thread_two.start()

    print("程序因线程一陷入阻塞")
    thread_one.join(timeout=3)
    print("程序因线程二陷入阻塞")
    thread_two.join(timeout=3)
    print("主线程已退出")

1.4 线程间的通信

  1. 线程之间共享同一块内存。子线程虽然可以通过指定target来执行一个函数,但是这个函数的返回值是没有办法直接传回主线程的。我们使用多线程一般是用于并行执行一些其他任务,因此获取子线程的执行结果十分有必要。
  2. 直接使用全局变量虽然可行,但是资源的并发读写会引来线程安全问题。下面给出常用的两种处理方式:

1.4.1 线程锁

其一是可以考虑使用锁来处理,当多个线程对同一份资源进行读写操作时,我们可以通过加锁来确保数据安全。Python中给出了多种锁的实现,例如:同步锁 Lock,递归锁 RLock,条件锁 Condition,事件锁 Event,信号量锁 Semaphore,这里演示 Lock锁的使用方式

from threading import Thread, Lock
from time import sleep

book_num = 100  # 图书馆最开始有100本图书
bookLock = Lock()


def books_lease():
    global book_num
    while True:
        bookLock.acquire()
        book_num -= 1
        print("借走1本,现有图书{}本".format(book_num))
        bookLock.release()
        sleep(1)


def books_return():
    global book_num
    while True:
        bookLock.acquire()
        book_num += 1
        print("归还1本,现有图书{}本".format(book_num))
        bookLock.release()
        sleep(1)


if __name__ == "__main__":
    thread_lease = Thread(target=books_lease)
    thread_return = Thread(target=books_return)
    thread_lease.start()
    thread_return.start()

1.4.2 queue队列

或者,我们可以采用Python的queue模块来实现线程通信。Python中的queue模块实现了多生产者、多消费者队列,特别适用于在多线程间安全的进行信息交换。该模块提供了4种我们可以利用的队列容器

  • 队列当中的方法
Queue(maxsize=5)  # 创建一个FIFO队列,并制定队列大小,若maxsize被指定为小于等于0,则队列无限大

Queue.qsize() # 返回队列的大致大小,注意并不是确切值,所以不能被用来当做后续线程是否会被阻塞的依据

Queue.empty() # 判断队列为空是否成立,同样不能作为阻塞依据

Queue.full()  # 判断队列为满是否成立,同样不能作为阻塞依据

Queue.put(item, block=True, timeout=None) # 投放元素进入队列,block为True表示如果队列满了投放失败,将阻塞该线程,timeout可用来设置线程阻塞的时间长短(秒);
# 注意,如果block为False,如果队列为满,则将直接引发Full异常,timeout将被忽略(在外界用try处理异常即可)
Queue.put_nowait(item) # 相当于put(item, block=False)

Queue.get(block=True, timeout=False) # 从队列中取出元素,block为False而队列为空时,会引发Empty异常
Queue.get_nowait() # 相当于get(block=False)

Queue.task_done() # 每个线程使用get方法从队列中获取一个元素,该线程通过调用task_done()表示该元素已处理完成。

Queue.join() # 阻塞至队列中所有元素都被处理完成,即队列中所有元素都已被接收,且接收线程全已调用task_done()。

生产者消费者实例:

# -*- coding:utf-8 -*-
import threading
from queue import Queue
from random import choice

dealList = ["红烧猪蹄", "卤鸡爪", "酸菜鱼", "糖醋里脊", "九转大肠", "阳春面", "烤鸭", "烧鸡", "剁椒鱼头", "酸汤肥牛", "炖羊肉"]

queue = Queue(maxsize=5)
# 厨子生产
def func_one(name):
    for num in range(4):
        # 随机从列表当中获取一个元素
        data_veg = choice(dealList)
        queue.put(data_veg, block=True)
        print(f"厨师{name}给大家做了一道:{data_veg}")

# 客人消费
def func_two(name:str):
    for num in range(3):
        veg_data = queue.get()
        print(f"客人{name}吃掉了:{veg_data}")
        queue.task_done()


if __name__ == '__main__':
    # 创建生产者线程,总共三个厨子,相当于给每个厨子创建了一个县城
    for name in ["张三", "李四", "王五"]:
        thread_one = threading.Thread(target=func_one, args=(name,))
        thread_one.start()
    # 创建消费者四个线程
    for name in ["客人甲", "客人乙", "坤哥", "凡哥"]:
        thread_two = threading.Thread(target=func_two, args=(name,))
        thread_two.start()
    queue.join()
;