线程
线程概念
-
线程
在一个进程的内部, 要同时干多件事, 就需要同时运行多个"子任务", 我们把进程内的这些"子任务"叫做线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中, 是进程的实际运作单位。一条线程指的是进程中一个单一顺序的控制流, 一个进程中可以并发多个线程, 每条线程并行执行不同的任务。在Unix System V 及 SunOS 中也被称为轻量进程(lightweight processes), 但轻量进程更多指内核线程(kernel thread), 而把用户线程(user thread) 称为线程
线程通常叫做轻型的进程。线程是共享内存空间的并发执行的多任务, 每一个线程都共享一个进程的资源
线程是最小的执行单元, 而一个进程由至少一个线程组成。如何调度进程和线程, 完全由操作系统决定, 程序自己不能决定什么时候执行, 执行多长时间 -
多线程
是指从软件或硬件上实现多个线程并发执行的技术。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程, 进而提升整体处理性能。具有这种能力的系统包括对称多处理机、多核心处理器以及芯片级多处理器(Chip-level multithreading) 或同时多线程(Simultaneous multithreading) 处理器。 -
主线程
任何进程都会有一个默认的主线程, 如果主进程死掉, 子线程也死掉, 所以子线程依赖于主线程 -
GIL
其他语言, CPU多核是支持多个线程同时执行。但在Python中, 无论是单核还是多核, 同时只能有一个线程在执行。其根源是GIL的存在。
GIL的全称是Global Interpreter Lock(全局解释器), 来源是Python设计之初的考虑, 为了数据安全所做的决定。某个线程想要执行, 必须先拿到GIL, 我们可以把GIL看成是"通行证", 并且在一个Python进程中, GIL只有一个。拿不到通行证的线程, 就不允许进入CPU执行。
并且由于GIL锁存在, Python里一个进程永远只能同时执行一个线程(拿到GIL的线程才能执行), 这就是为什么在多核CPU上, Python的多线程效率并不高的根本原因。 -
模块
_thread模块: 低级模块
threading模块: 高级模块, 对_thread进行了封装
使用 _thread 模块, 去创建线程
导入模块
import _thread
开启线程
_thread.start_new_thread(函数名, 参数)
注意:
- 参数必须为元组类型
- 如果主线程执行完毕 子线程就会死掉
- 如果线程不需要传参数的时候 也必须传递一个空元组占位
实例
import _thread
import time
i = 0;
def run():
global i;
print(i)
i = i+1;
def run1():
print('main')
if __name__ == "__main__":
_thread.start_new_thread(run, ())
time.sleep(3)
run1()
threading创建线程(重点)
导入模块
import threading
threading创建线程的方式
myThread = threading.Thread(target=函数名[,args=(参数,),name=“你指定的线程名称”])
参数
- target: 指定线程执行的函数
- name: 指定当前线程的名称
- args: 传递子线程的参数, (元组形式)
开启线程
myThread.start()
线程等待
myThread.join()
返回当前线程对象
- threading.current_thread()
- threading.currentThread()
获取当前线程的名称
- threading.current_thread().name
- threading.currentThread().getName()
设置线程名
setName()
Thread(target=fun).setName(‘name’)
返回主线程对象
threading.main_thread()
返回当前活着的所有线程总数, 包括主线程main
- threading.active_count()
- threading.activeCount()
判断线程是不是活的, 即线程是否已经结束
- Thread.is_alive()
- Thread.isAlive()
线程守护
设置子线程是否随主线程一起结束
Thread.setDaemon(True)
还有一个要特别注意的: 必须在start()方法调用之前设置
if __name__ == '__main__':
t = Thread(target=fun, args=(1,))
t.setDaemon(True)
t.start()
print('over')
获取当前所有的线程名称
threading.enumerate() # 返回当前包含所有线程的列表
启动线程实现多任务
import threading
def run():
print(threading.current_thread().name)
if __name__ == '__main__':
thread_list = []
for i in range(5):
thread_list.append(threading.Thread(target=run, args=(),name=f"child_thread_{i}"))
thread_list[i].start()
for item in thread_list:
item.join()
run()
线程间共享数据
概述
多线程和多进程最大的不同在于, 多进程中, 同一个变量, 各自有一份拷贝存在每个进程中, 互不影响。而多线程中, 所有变量都由所有线程共享。所以, 任何一个变量都可以被任意一个线程修改, 因此, 线程之间共享数据的最大危险在于多个线程同时修改一个变量, 容易把内容改乱了。
Lock线程锁(多线程内存错乱问题)
- 概述
Lock锁是线程模块中的一个类, 有两个主要方法: acquire()和release()当调用acquire()方法时, 它锁定锁的执行并阻塞锁定执行,直到其他线程调用release()方法将其设置为解锁状态。锁帮助我们有效地访问程序中的共享资源, 以防止数据损坏, 它遵循互斥, 因为一次只能有一个线程访问特定的资源。 - 作用
避免线程冲突 - 锁: 确保了这段代码只能由一个线程从头到尾的完整执行阻止了多线程的并发执行, 包含锁的某段代码实际上只能以单线程模式执行, 所以效率大大的降低了 由于可以存在多个锁, 不同线程持有不同的!锁, 并试图获取其他的锁,可能造成死锁, 导致多个线程只能挂起, 只能靠操作系统强行终止
- 注意:
- 当前线程锁定以后 后面的线程会等待(线程等待/线程阻塞)
- 需要release解锁以后才正常
- 不能重复锁定
Timer定时执行
- 概述
Timer是Thread的子类, 可以指定时间间隔后再执行某个操作 - 使用
import threading
def go():
print("go")
# t = threading.Timer(秒数, 函数名)
t = threading.Timer(3, go)
t.start()
print('我是主线程的代码')
线程池ThreadPoolExecutor
- 模块
concurrent.futures - 导入 Executor
from concurrent.futures
方法
- submit(fun[, args]) 传入放入线程池的函数以及传参
- map(fun[, iterable_args]) 统一管理
区别
- submit与map参数不同 submit每次都需要提交一个目标函数和对应的参数map只需要提交一次目标函数
目标函数的参数放在一个可迭代对象(列表、字典…)里就可以
使用
from concurrent.futures import ThreadPoolExecutor
import time
# 线程池 统一管理 线程
def go(str):
print("hello", str)
time.sleep(2)
name_list = ['lucky', 'zhangsan', 'lisi', 'wangwu', 'zhaoliu']
pool = ThreadPoolExecutor(5) # 控制线程的并发数
方式一:
for i in name_list:
pool.submit(go, i)
简写:
all_task = [pool.submit(go, i) for i in name_list]
方式二:
# 统一放到线程池使用
pool.map(go, name_list)
# 多个参数
# pool.map(go, name_list1, name_list2...)
map(fn, *iterables, timeout=None)
fn: 第一个参数 fn 是需要线程执行的函数;
iterables: 第二个参数接受一个可迭代对象;
timeout: 第三个参数timeout 跟 wait() 的 timeout 一样, 但由于 map 是返回线程执行的结果, 如果 timeout 小于线程执行时间会抛异常 TimeoutError。
注意: 使用 map 方法, 无需提前使用 submit 方法, map 方法与 python 高阶函数 map 的含义相同, 都是将序列中的每个元素都执行同一个函数。
获取返回值
- 方式一:
import random
from concurrent.futures import ThreadPoolExecutor, as_completed
import time
# 线程池 统一管理 线程
def go(str):
print("hello", str)
time.sleep(random.randint(1, 4))
return str
name_list = ['lucky', 'zhangsan', 'lisi', 'wangwu', 'zhaoliu']
pool = ThreadPoolExecutor(5) # 控制线程的并发数
all_task = [pool.submit(go, i) for i in name_list]
# 统一放到进程池使用
for future in as_completed(all_task):
print("finish the task")
obj_data = future.result()
print("obj_data is", obj_data)
as_completed
当子线程中的任务执行完后, 使用 result() 获取返回结果
该方法是一个生成器, 在没有任务完成的时候, 会一直阻塞, 除非设置了 timeout。当有某个任务完成的时候, 会 yield 这个任务, 就能执行 for 循环下面的语句, 然后继续阻塞住, 循环到所有任务结束, 同时, 先完成的任务会先返回给主线程
- 方式二
for result in poor.map(go, name_list):
print("task:{}".format(result))
- wait等待线程执行完毕 再继续向下执行
from concurrent.futures import ThreadPoolExecutor, wait
import time
# 参数times用来模拟下载的时间
def down_video(times):
time.sleep(times)
print("down video {}s finished".format(times))
return times
executor = ThreadPoolExecutor(max_workers=2)
# 通过submit函数提交执行的函数到线程池中, submit函数立即返回, 不阻塞
task1 = executor.submit(down_video, 3)
task1 = executor.submit(down_video, 1)
# done方法用于判定某个任务是否完成
print("任务1是否已经完成: ", task1.done())
time.sleep(4)
print('wait')
print('任务1是否已经完成: ', task1.done())
print('任务2是否已经完成: ', task2.done())
result方法可以获取task的执行结果
print(task1.result())
线程池与线程对比
线程池是在程序运行开始, 创建好的n个线程挂起等待任务的到来。而多线程是在任务到来得时候进行创建, 然后执行任务。
线程池中的线程执行完之后不会回收线程, 会继续将线程放在等待队列中; 多线程程序在每次任务完成之后会回收该线程。
由于线程池中线程是创建好的, 所以在效率上相对于多线程会高很多。
线程池在高并发的情况下有着较好的性能; 不容易挂掉。多线程在创建线程数较多的情况下, 很容易挂掉。
队列模块queue
- 导入队列模块
import queue - 概述
queue是python标准库中的线程安全的队列(FIFO)实现, 提供了一个适用于多线程编程的先进先出的数据结构, 及队列, 用来在生产者和消费者线程之间的信息传递 - 基本FIFO队列
queue.Queue(maxsize=0)
FIFO及First in First Out, 先进先出。Queue提供了一个基本的FIFO容器, 使用方法很简单, maxsize是个整数, 指明了队列中能存放的数据个数的上限, 插入会导致阻塞, 直到队列中的数据被消费掉。
如果maxsize小于或者等于0, 队列大小没有限制。
举个例子:
import queue
q = queue.Queue()
for i in range(5):
q.put(i)
while not q.empty():
print(q.get())
- 一些常用方法
- task_done()
意味着之前入队的一个任务已经完成。由队列的消费者线程调用。每一个get()调用得到一个任务, 接下来的task_done()调用告诉队列该任务已经处理完毕。
如果当前一个join()正在阻塞, 它将在队列中的所有任务都处理完时恢复执行(即每一个由put()调用入队的任务都有一个对应的task_done()调用)。 - join()
阻塞调用线程, 直到队列中的所有任务被处理掉。
只要有数据被加入队列, 未完成的任务数就会增加。当消费者线程调用task_done()(意味着有消费者取得任务并完成任务), 未完成的任务数就会减少。当未完成的任务数降到0, join()解除阻塞。 - put(item[, block[, timeout]])
将item放入队列中- 如果可选的参数block为True且timeout为空对象(默认的情况,阻塞调用,无超时)。
- 如果timeout是个正整数, 阻塞调用进程最多timeout秒, 如果一直无空间可用, 抛出Full异常(带超时的阻塞调用)
- 如果block为False, 如果有空闲空间可用将数据放入队列, 否则立即抛出异常
- 其非阻塞版本为put_nowait等同于put(item, False)
- get([block[, timeout]])
从队列中移除并返回一个数据。 block跟timeout参数同 put 方法
其非阻塞方法为get_nowait()相当于get(False) - empty()
如果队列为空, 返回True, 反之返回False
- task_done()