这里是目录
前情提要
在计算中,进程是正在执行的计算机程序的一个实例。任何流程都有 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
可以看出子线程运行时的主进程ID是不变的。print("主进程ID: {}".format(os.getpid()))
- 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函数中创建了两个线程t1和t2 ,并将全局变量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多线程并不是真正意义上的多线程。 |