以下是自己学习后总结的知识点,如有错误请大家指出错误,我来修改。
线程基础知识
1、线程是CPU能够进行运算调度的最小单位。
2、进程是计算机分配资源的的最小单元,进程可以为线程提供运行资源。
3、一个进程中可以有多个线程,同一个进程中的线程可以共享当前进程的资源。此外线程是进程中的一个实体。
4、线程的基本状态为新建、就绪、运行、阻塞和结束。
使用线程
我们以前刚刚开始接触的python代码都是同步单线程代码,都是依次运行的。
1、比如写了两个任务函数。使用线程实现。
import threading
import time
def task_1():
print('前_任务1')
time.sleep(6)
print('后_任务1')
def task_2():
print('前_任务2')
time.sleep(4)
print('后_任务2')
if __name__ == '__main__':
t1 = threading.Thread(target=task_1)
t2 = threading.Thread(target=task_2)
t1.start()
t2.start()
print("执行完毕")
运行结果为:
前_任务1
前_任务2
执行完毕
后_任务2
后_任务1
可以看出程序运行并不等待所以任务完成才执行最后的打印,而是打印两个任务程序的前_任务后直接执行打印执行完毕,再继续等待。
t1 = threading.Thread(target=task_1) # 创建子线程
t1.start() # 当前线程准备就绪,等待调度,具体调度时间由cpu决定。
上述代码中我们应该要知道:
1、一个py文件被解释器执行时会在操作系统中创建进程。
2、在进程中创建线程来执行当前文件中的代码,我们把最初创建的线程称之为主线程。
3、当主线程执行到Thread代码时会创建一个新的线程,我们一般称之为子线程。
4、当前代码中的主线程与子线程交替执行。
5、子线程被执行时主线程不会等待,继续往下执行到没有代码时等待子线程执行完毕再退出。
但是在一下程序中我们遇到了一个问题像这样:
import threading
num = 0
def add():
global num
for i in range(100000):
num += i
t = threading.Thread(target=add)
t.start()
print(num)
# 连续五次都不一样,这是为什么呢?
原因是主线程并不会等待子线程执行完成后才结束,而是主线程执行到打印函数后直接结束程序。我们怎么解决这个问题?
接下来我们要使用join()来解决这个问题。我们先看程序:
import threading
num = 0
def add():
global num
for i in range(100000):
num += i
t = threading.Thread(target=add)
t.start()
t.join()
print(num)
连续运行五次结果都为:4999950000。
join()的作用是堵塞主线程,当子线程执行完毕后才解开堵塞,让主线程继续往下执行。
守护线程
# t.deamon=True 的作用是将线程 t 设置为守护线程。
# 例如:
import time
import threading
def work():
for i in range(5):
print(i)
time.sleep(1)
t = threading.Thread(target=work)
t.daemon=False
t.start()
print('主线程即将退出...')
t.deamon = False 代表着该线程为非守护线程,作用为主线程结束时,如果还有非守护线程还在运行,则等待这些非守护线程执行完毕。
t.deamon = True 代表着该线程为守护线程,当主线程结束时守护线程自动终止,不会阻止程序的退出。
线程池
import time
from concurrent.futures import ThreadPoolExecutor # 线程池库
# 创建线程池对象
# max_workers:最大线程数
executor = ThreadPoolExecutor(max_workers=2)
def task(tt):
print('---开始执行任务---')
time.sleep(tt)
print('---任务执行完毕---')
if __name__ == '__main__':
# 提交任务
# 通过submit提交需要执行的函数到线程池中,并且submit是立即返回对象不会堵塞
t1 = executor.submit(task, 3)
t2 = executor.submit(task, 2)
t3 = executor.submit(task, 4)
# done方法用于判定某个任务是否完成
print('t1完成情况:', t1.done())
print('t2完成情况:', t2.done())
print('t3完成情况:', t3.done())
# 可以使用cancel取消任务 但是运行中的任务无法取消,可以将线程数量修改成1
# print('t2任务取消:', t2.cancel())
# print('t2任务取消:', t2.cancel())
# print('t2任务取消:', t2.cancel())
# result方法可以获取任务的返回值 当前获取为阻塞
print('t1返回结果:', t1.result())
print('t2返回结果:', t2.result())
print('t3返回结果:', t3.result())
使用场景
我们使用线程时需要在什么场景使用?
1、属于IO密集型任务那么优先使用线程方式。
2、属于计算密集型任务那么优先使用进程方式
那么什么是IO密集型任务什么是计算密集型任务?
IO密集型任务:指在程序运行中花了大量时间在输入输出操作上,这类任务的特点是频繁的与外部进行资源交换,如读写磁盘文件,网络请求等。
计算密集型任务:指主要消耗CPU资源的任务,花大量时间在数据计算和数据处理上。对CPU的计算能力要求较高如:视频或音频解码,科学计算等。
python全局解释器锁(GIL锁)
主要功能:是让一个进程在同一时刻只允许一个线程进行工作。
例如在某一时刻同一个线程中,只有一个线程在执行,其他线程在等待cpu调度。这种情况无法发挥出多核cpu的优势。如果想绕开GIL锁,只能使用多进程的方式,创建多个进程是极其浪费计算机资源的。