【multithreading】Python 多线程的概念和使用方法


前情提要

在计算中,进程是正在执行的计算机程序的一个实例。任何流程都有 3 个基本组成部分:

  • 一个可执行程序。
  • 程序所需的相关数据(变量、工作空间、缓冲区等)
  • 程序的执行上下文(进程状态)
  • 线程是进程中可以调度执行的实体。此外,它是可以在 OS(操作系统)中执行的最小处理单元。

简而言之,线程是程序中的一系列此类指令,可以独立于其他代码执行。为简单起见,可以假设线程只是进程的子集。

线程在线程控制块 (TCB) 中包含所有这些信息:

  • 线程标识符:为每个新线程分配唯一 id (TID)
  • 堆栈指针:指向进程中线程的堆栈。堆栈包含线程范围内的局部变量。
  • 程序计数器:存放线程当前正在执行的指令地址的寄存器。
  • 线程状态:可以是running、ready、waiting、start或done。
  • 线程的寄存器集:分配给线程进行计算的寄存器。
  • 父进程指针:指向线程所在进程的进程控制块 (PCB) 的指针。

在这里插入图片描述

多线程

一个进程中可以存在多个线程,其中:

  • 每个线程都包含自己的寄存器集和局部变量(存储在堆栈中)。
  • 一个进程的所有线程共享全局变量(存储在堆中)和程序代码

思考一下下图,内存中多个线程是如何存在的:
在这里插入图片描述
多线程被定义为处理器同时执行多个线程的能力。


实现

一、线程基础

1. 函数 - 创建线程

# 导入包
import threading

# 打印立方
def print_cube(num):
	print("Cube: {}".format(num * num * num))

# 打印平方
def print_square(num):
	print("Square: {}".format(num * num))

if __name__ == "__main__":
	# 创建进程
	t1 = threading.Thread(target=print_square, args=(10,))
	t2 = threading.Thread(target=print_cube, args=(10,))

	# 启动t1和t2
	t1.start()
	t2.start()

	# 主进程等待t1和t2执行完
	t1.join()
	t2.join()

	# t1和t2执行完后才能执行下面的代码
	print("Done!")

输出:

Square: 100
Cube: 1000
Done!

解释上面的demo:

  • 要导入线程模块:

    import threading
    
  • 要创建一个新线程,必须创建一个Thread类的对象。它的参数有:

    • target : 线程要执行的函数
    • args:要传递给目标函数的参数

    在上面的示例中,我们创建了 2 个具有不同目标函数的线程:

    t1 = threading.Thread(target=print_square, args=(10,)) 
    t2 = threading.Thread(target=print_cube, args=(10,))
    
  • 要启动一个线程,我们使用Thread类的start方法。

    t1.start() 
    t2.start()
    
  • 一旦线程启动,当前程序(你可以把它想象成一个主线程)也会继续执行。使用join方法,让主进程等待子进程运行结束后再运行。

    t1.join() 
    t2.join()
    

思考下图,以便更好地理解上述程序的工作原理:
在这里插入图片描述

线程运行

2. 如何更改线程的名称

import threading
import os

def task1():
	print("Task 1 线程名称: {}".format(threading.current_thread().name))
	print("task 1 所属进程的ID: {}".format(os.getpid()))

def task2():
	print("Task 2 线程名称: {}".format(threading.current_thread().name))
	print("task 2 所属进程的ID: {}".format(os.getpid()))

if __name__ == "__main__":

	# 打印当前进程的ID
	print("主进程ID: {}".format(os.getpid()))

	# 打印主线程的名称
	print("主线程名称: {}".format(threading.main_thread().name))

	# 创建子线程
	t1 = threading.Thread(target=task1, name='t1')
	t2 = threading.Thread(target=task2, name='t2')
	
	# 运行子线程
	t1.start()
	t2.start()
	
	print("主进程结束")

输出:

主进程ID: 11758
主线程名称: MainThread
Task 1 线程名称: t1
task 1 所属的进程ID: 11758
Task 2 线程名称: t2
task 2 所属的进程ID: 11758
主进程结束

上述demo解析:

  • os.getpid() 函数来获取当前进程的 ID
    print("主进程ID: {}".format(os.getpid()))
    
    可以看出子线程运行时的主进程ID是不变的。
  • threading.main_thread() 函数来获取主线程对象。在正常情况下,主线程是启动 Python 解释器的线程。线程对象的name属性用于获取线程的名称。
    print("主线程名称: {}".format(threading.main_thread().name))
    
  • threading.current_thread() 函数来获取当前线程对象。
    print("Task 1 线程名称: {}".format(threading.current_thread().name))
    

3. 线程中使用Logging模块

我们能够通过打印线程名来识别当前运行的是哪个线程。然而,我们真正需要的是Logging模块的支持,该模块将使用的 %(threadName)s可以将线程名嵌入到每个日志消息中。在日志消息中包含线程名可以更容易地将这些消息追溯到其源。注意,Logging是线程安全的,因此来自不同线程的消息在输出中保持不同。

import threading
import time
import logging

logging.basicConfig(level=logging.DEBUG,
                      format='[%(levelname)s] (%(threadName)-9s) %(message)s',)

def f1():
    logging.debug('开始')
    time.sleep(1)
    logging.debug('结束')

def f2():
    logging.debug('开始')
    time.sleep(2)
    logging.debug('结束')

def f3():
    logging.debug('开始')
    time.sleep(3)
    logging.debug('结束')

t1 = threading.Thread(target=f1) # 使用默认线程名
t2 = threading.Thread(name='f2', target=f2)
t3 = threading.Thread(name='f3', target=f3)

t1.start()
t2.start()
t3.start()

输出

[DEBUG] (Thread-1 ) 开始
[DEBUG] (f2       ) 开始
[DEBUG] (f3       ) 开始
[DEBUG] (Thread-1 ) 结束
[DEBUG] (f2       ) 结束
[DEBUG] (f3       ) 结束

4. 守护线程 setDaemon

守护线程是在后台运行并为主线程或非守护线程提供支持,一般那些后台执行的线程被视为守护线程。守护线程不会阻止主线程退出。
它的最佳示例之一是垃圾收集器,因为我们假设主线程正在执行或运行,此时如果发生任何内存问题, python 虚拟机(PVM)将立即执行垃圾收集器垃圾收集器将在后台执行并销毁所有无用的对象,然后释放出空闲内存,一旦有空闲内存可用,主线程会将毫无问题地执行。

(1) 非守护线程不会在主线程结束后停止运行
from threading
import time

def thread_1():				
	for i in range(5):
		print('这是非守护进程')
		time.sleep(2)

T = Thread(target=thread_1)
T.start()	

time.sleep(5)				
print('主线程结束')

输出:

这是非守护进程
这是非守护进程
这是非守护进程
主线程结束
这是非守护进程
这是非守护进程
(2) 将子线程设置为守护线程
  • obj.setDaemon():参数 True/False
from threading
import time

def thread_1():				
	for i in range(5):
		print('这是非守护进程')
		time.sleep(2)

T = Thread(target=thread_1)
T.setDaemon(True) 
T.start()	

time.sleep(5)				
print('主线程结束')

输出

这是非守护进程
这是非守护进程
这是非守护进程
主线程结束
(3) 否是守护线程
  • obj.isDaemon()
  • obj.daemon
注意:

obj.setDaemon() 设置线程为守护线程时,一定要在 obj.start() 前。因为活动的线程无法设置为守护线程。否则会报错:RuntimeError: cannot set daemon status of active thread

5. 加入线程 join

此方法可以理解为:在主线程的执行顺序里加入一个子线程,这时,子线程就占用了主线程的执行时间,必须等待子线程结束后才能继续执行主线程的后续代码。
在调用 join() 方法时,调用线程被阻塞,直到线程对象停止运行。线程对象可以在以下任何一种情况下终止:

  • 正常结束。
  • 异常退出。
  • 直到超时。

语法:

object_name.join()
# 只等待timeout秒,超时将强行结束
object_name.join(timeout) 

6. 类 - 创建线程

import threading

class MyThread(threading.Thread):
	'''
	初始化父类Thread类中的参数。
	group: 应为None;当ThreadGroup类实现时,保留给将来的扩展。
	target: run()方法调用的可调用对象。默认为None。它就接收函数式线程名的参数。
	name: 线程名。默认情况下,以“Thread-N”的形式的唯一名称。
	'''
    def __init__(self, group=None, target=None, name=None, args=(), kwargs=None):
    	super(MyThread, self).__init__(group=group, target=target, name=name)
    	self.args = args
    	self.kwargs = kwargs

    def run(self):
    	print('%s running! args: %s, kwargs: %s' % (threading.current_thread().name, self.args, self.kwargs))


if __name__ == "__main__":
   	for i in range(3):
		t = MyThread(args=(i,), kwargs={'a':1, 'b':2})
		t.start()

输出:

Thread-1 running! args: (0,), kwargs: {'a': 1, 'b': 2}
Thread-2 running! args: (1,), kwargs: {'a': 1, 'b': 2}
Thread-3 running! args: (2,), kwargs: {'a': 1, 'b': 2}

注意:一般情况下只需要重写 __init__run 函数就可以了,不建议重写其他函数,有可能导致错误。

7. 主线程与子线程的关系

下图介绍了主线程与子线程的运行关系:
在这里插入图片描述


二、线程同步

线程同步被定义为一种机制,它确保多个并发线程不会同时执行临界区的代码段。

临界区是指访问共享资源的程序部分。
在这里插入图片描述

1. 为什么需要同步

import threading

x = 0

def increment():
    global x
    x += 1
  
def thread_task():
    for _ in range(100000):
        increment()

def main_task():
	global x
	x = 0
	
	for _ in range(2):
		t = threading.Thread(target=thread_task)
		t.start()
		t.join()

if __name__ == "__main__":
	for i in range(10):
		main_task()
		print("迭代 {0}: x = {1}".format(i,x))

输出

迭代 0: x = 175005
迭代 1: x = 200000
迭代 2: x = 200000
迭代 3: x = 169432
迭代 4: x = 153316
迭代 5: x = 200000
迭代 6: x = 167322
迭代 7: x = 200000
迭代 8: x = 169917
迭代 9: x = 153589

在上面的程序中:

  • main_task函数中创建了两个线程t1t2 ,并将全局变量x设置为 0。
  • 每个线程都有一个目标函数thread_task函数,每个threat_task函数被调用100000次。
  • thread_task函数将在每次调用中将全局变量x增加 1。

x的预期结果应该是200000,实际结果在10 次迭代中的值并不是200000。
这是由于线程对共享变量x并发访问而发生的。x值的这种不可预测性是由于线程竞争造成的,这种情况也被称为线程不安全
在这里插入图片描述

线程竞争

因此,我们需要一个工具来在多个线程之间进行适当的同步。

2. Lock 线程锁

threading模块提供了一个Lock类来处理竞争条件。Lock是使用操作系统提供的Semaphore(信号量)对象实现的。

Semaphore信号量是一个同步对象,它控制并行编程环境中多个进程/线程对公共资源的访问。它只是操作系统(或内核)存储中指定位置的一个值,每个进程/线程都可以检查并更改它。根据找到的值,进程/线程可以使用该资源,如果检查它已经被使用,就必须等待一段时间才能再次尝试。信号量可以是二进制的(0或1),也可以有额外的值。通常,使用信号量的进程/线程会检查该值,然后,如果它使用该资源,就会更改该这个值,以便后续信号量的使用者知道要等待。

Lock类提供以下方法:

  • acquire([blocking]):获取锁。锁可以是阻塞的,也可以是非阻塞的。

    • 当blocking参数设置为True(默认值)时,线程执行将被阻塞,直到锁被解锁,然后锁被设置为locked并返回True。
    • 当blocking参数设置为False时,线程执行不会被阻塞。如果锁被解锁,则将其设置为locked,并返回True,否则立即返回False。
  • release():释放锁。

    • 如果是Lock是acquire,会释放锁,然后返回。其他线程阻塞等待锁,只有使用锁的线程解锁后,其他线程会竞争锁,最终只有一个线程会获取到锁并执行,其它线程继续阻塞等待。
    • 如果已经解锁,则会引发ThreadError。

小demo:

import threading

x = 0

def increment():
	global x
	x += 1

def thread_task(lock):
	for _ in range(100000):
		lock.acquire()  # 执行关键代码前,获取锁
		increment()
		lock.release()  # 执行完后,解锁

def main_task():
	global x
	x = 0

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

	# 将lock通过线程参数传递进去
	t1 = threading.Thread(target=thread_task, args=(lock,))
	t2 = threading.Thread(target=thread_task, args=(lock,))

	t1.start()
	t2.start()

	t1.join()
	t2.join()

if __name__ == "__main__":
	for i in range(10):
		main_task()
		print("迭代 {0}: x = {1}".format(i,x))

结果

迭代 0:x = 200000
迭代 1:x = 200000
迭代 2:x = 200000
迭代 3:x = 200000
迭代 4:x = 200000
迭代 5:x = 200000
迭代 6:x = 200000
迭代 7:x = 200000
迭代 8:x = 200000
迭代 9:x = 200000
  • 解释上面的demo:
    首先,使用以下命令创建一个Lock对象:
    lock = threading.Lock()
    
  • 然后,lock对象作为参数传入函数:
     t1 = threading.Thread(target=thread_task, args=(lock,))
     t2 = threading.Thread(target=thread_task, args=(lock,))
    
  • 在目标函数的关键部分,使用lock.acquire()方法应用锁。一旦获得锁,在使用lock.release()方法释放锁之前,没有其他线程可以访问临界区(这里是increment函数) 。
    lock.acquire()
    increment()
    lock.release()
    

结果正是我们想要的,每次x的值都是 200000。
流程如下:
![在这里插入图片描述](https://img-blog.csdnimg.cn/86d3810962fe4568a386abcdeb346e01.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5aSn5L2s5qmZMTIxNQ==,size_16,color_FFFFFF,t_70,g_se,x_16,#pic_center在这里插入图片描述


三、线程通信

1. 队列Queue

Queue 模块主要用于处理多个线程上的大量数据。它可以创建一个新的队列对象,并设置队列长度。

  • get(): 从队列中获取一个元素,并将它从队列中删除
  • put(): 在队列末尾放入一个元素
  • qsize(): 获取队列长度
  • empty(): 返回 True/False,判断队列是否为空
  • full(): 返回 True/False,判断队列是否已满
import queue
import threading
import time

# 线程退出标志
thread_exit_Flag = 0

class sample_Thread (threading.Thread):
	def __init__(self, threadID, name, q):
		super(sample_Thread, self).__init__()
		self.threadID = threadID
		self.name = name
		self.q = q
		
	def run(self):
		print ("初始化 " + self.name)
		process_data(self.name, self.q)
		print ("退出 " + self.name)

	
def process_data(threadName, q):
# 如果退出标志为0,就运行;为1则退出
    while not thread_exit_Flag:
        if not workQueue.empty():
            queueLock.acquire()
            data = q.get()
            queueLock.release()
            print ("% s 执行了 % s" % (threadName, data))
        else:
            time.sleep(1)

thread_list = ["Thread-1", "Thread-2", "Thread-3"]
name_list = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L']
queueLock = threading.Lock()
workQueue = queue.Queue(10)
threads = []
threadID = 1


# 创建线程
for thread_name in thread_list:
	thread = sample_Thread(threadID, thread_name, workQueue)
	thread.start()
	threads.append(thread)
	threadID += 1
    
# 等待队列为空,设置标志位为1
while True:
    if name_list and not workQueue.full():
        queueLock.acquire()
        workQueue.put(name_list.pop())
        queueLock.release()
    elif workQueue.empty():
        thread_exit_Flag = 1
        break

    
# 阻塞等待所有线程执行完毕
for t in threads:
	t.join()
print ("主线程退出")

输出

初始化 Thread-1
初始化 Thread-2
初始化 Thread-3
Thread-3 执行了 L
Thread-2 执行了 K
Thread-2 执行了 J
Thread-2 执行了 I
Thread-2 执行了 H
Thread-2 执行了 G
Thread-2 执行了 F
Thread-2 执行了 E
Thread-2 执行了 D
Thread-2 执行了 C
Thread-2 执行了 B
Thread-3 执行了 A
退出 Thread-1
退出 Thread-2
退出 Thread-3
主线程退出

2. 条件Condition

  在讨论线程间通信的 Condition() 实现之前,我们先对线程间通信进行一些简短的讨论,当任何线程需要从另一个线程得到一些东西时,它们将组织彼此之间的通信,通过这种通信,线程之间将满足它们的需求。这意味着: Condition是一个线程之间为任何类型的需求进行通信的过程。

Condition 方法:
  可以这么说,使用 condition() 方法实现线程间通信是对用于线程间通信的事件对象的升级。这里的Condition表示线程之间的一些状态变化,比如 “发送通知”, “接收通知”。

语法:

condition_object = threading.condition( )

在这种情况下,线程可以等待该条件,一旦该条件执行,线程就可以根据该条件进行修改。简单地说,我们可以说条件对象允许线程等待,直到另一个线程通知它们。条件对象在内部与 锁(RLock) 概念相关。
下面我们将讨论以下 Condition类 的一些方法:

方法解释语法
release()当线程满足condition object的需求时,使用 release() 方法,该方法帮助我们将条件对象从其任务中释放出来,并突然释放线程获取的内部锁。condition_object.release()
acquire()我们想要获取或接管任何condition object以进行线程间通信时,使用acquire() 方法。acquire() 方法是强制性的。当我们使用这种方法时,线程突然获得内部锁定系统。condition_object.acquire()
notify()当我们只想向一个处于等待状态的线程发送通知时,使用 notify() 方法。这里,如果一个线程想要成为条件对象升级,则使用notify()condition_object.notify()
wait()这个方法可用于使线程等待,直到收到nofity/notifyAll,也直到给定时间(time)结束。简而言之,我们可以说线程将等待,直到 notify 方法执行之前。如果我们设置了time参数,我们可以在其中使用时间,然后执行将停止,直到时间结束之后,它将执行仍然剩余的指令condition_object.wait()
notifyAll()此处的 notifyAll 方法用于为所有wait的线程发送通知。notifyAll调用时, 所有等待条件对象的线程都会被更新条件。condition_object.notifyAll()
# code

# import modules

import time
from threading import *
import random



class appointment:
	
    def patient(self):
        condition_object.acquire()
        print('patient john waiting for appointment')
        condition_object.wait() # Thread is in waiting state
        print('successfully got the appointment')
        condition_object.release()

    def doctor(self):
        condition_object.acquire()
        print('doctor jarry checking the time for appointment')
        time=0
        time=random.randint(1,13)
        print('time checked')
        print('oppointed time is {} PM'.format(time))
        condition_object.notify()
        condition_object.release()

	
condition_object = Condition()
class_obj=appointment()
T1 = Thread(target=class_obj.patient)
T2 = Thread(target=class_obj.doctor)

T1.start()
T2.start()

四、多线程的优点及缺点

优点缺点
1、线程之间彼此独立。
2、由于线程并行执行任务,因此可以更好地利用系统资源。
3、增强单核多处理器机器的性能。
1、随着线程数的增加,程序复杂性也会增加。
2、操作共享资源(对象、数据)时,必须进行同步。
3、调试难度高,结果有时不可预测。
4、程序设计糟糕的话会导致死锁。
5、只适用于IO密集型
6、Python中为了数据安全加入全局解释器锁(GIL),导致单核CPU下多个线程无法并发只能并行运行。所以python多线程并不是真正意义上的多线程。
  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值