线程
1.线程介绍
1.什么是线程
线程(Thread)也叫轻量级进程,是操作系统能够进行运算调度的最小单位,它被包涵在进程之中,是进程中的实际运作单位。线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行。
2. 有了进程为什么要有线程
进程有很多优点,它提供了多道编程,让我们感觉我们每个人都拥有自己的CPU和其他资源,可以提高计算机的利用率。很多人就不理解了,既然进程这么优秀,为什么还要线程呢?其实,仔细观察就会发现进程还是有很多缺陷的,主要体现在两点上:
-
进程只能在一个时间干一件事,如果想同时干两件事或多件事,进程就无能为力了。
-
进程在执行的过程中如果阻塞,例如等待输入,整个进程就会挂起,即使进程中有些工作不依赖于输入的数据,也将无法执行。
3.进程和线程的关系
线程与进程的区别可以归纳为以下4点
- 地址空间和其他资源:进程间相互独立,统一进程的歌线程间共享,某进程内的线程在其他进程不可见。
- 通信:进程间通信IPC,线程间可以直接读写进程进程数据段(如全局变量)来进行通信,需要进程同步和互斥手段的辅助,以保住数据的一致性。
- 调度和切换:线程上下文切换比进程上下文切换要快的多。
- 在多线程操作系统中,进程不是一个可执行的实体。
4.线程的特点
TCB包括以下信息: (1)线程状态。 (2)当线程不运行时,被保存的现场资源。 (3)一组执行堆栈。 (4)存放每个线程的局部变量主存区。 (5)访问同一个进程中的主存和其它资源。 用于指示被执行指令序列的程序计数器、保留局部变量、少数状态参数和返回地址等的一组寄存器和堆栈。
5.为什么要使用多线程
线程在程序中是独立的,并发的执行流。与分流的进程相比,进程中线程之间的隔离程度要小,它们共享内存,文件句柄和其他进程应有的状态。
因为线程的划分尺度小于进程,使得多线程程序的并发性高。进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。
线程比进程具有更高的性能,这是由于用一个进程中的线程都有共性多个线程共享同一个进程的虚拟空间,线程共享的还击包括进程代码段,进程的公有数据等,利用这些共享的数据,线程之间和容易实现通信。
操作系统在创建进程时,必须为该进程分配独立的内存空间,并分配大量的相关资源,但创建线程则简单的多,因此,使用多线程来实现并发比使用多进程的性能要高的多的。
总结起来,使用多线程编码具有以下优点:
- dua进程之间不能共享内存,但线程之间共享内存非常容易。
- 操作系统在创建进程时,需要为该进程重新分配系统资源,但是创建线程的代价则小的多。因此,使用多线程来实现多任务并发执行比使用多进程的效率高。
- Python语言内置了多线程功能的支持,而不是单纯地作为底层操作系统的调度方式,从而简化了Python的多线程编程。
2.线程和Python
1. 全局解释器锁CIL
2.Python线程模块的选择
Python提供了几个用于多线程编程的模块,包括thread、threading和Queue等。thread和threading模块允许程序员创建和管理线程。thread模块提供了基本的线程和锁的支持,threading提供了更高级别、功能更强的线程管理的功能。Queue模块允许用户创建一个可以用于多个线程之间共享数据的队列数据结构。
避免使用thread模块,因为更高级别的threading模块更为先进,对线程的支持更为完善,而且使用thread模块里的属性有可能会与threading出现冲突;其次低级别的thread模块的同步原语很少(实际上只有一个),而threading模块则有很多;再者,thread模块中当主线程结束时,所有的线程都会被强制结束掉,没有警告也不会有正常的清除工作,至少threading模块能确保重要的子线程退出后进程才退出。
thread模块不支持守护线程,当主线程退出时,所有的子线程不论它们是否还在工作,都会被强行退出。而threading模块支持守护线程,守护线程一般是一个等待客户请求的服务器,如果没有客户提出请求它就在那等着,如果设定一个线程为守护线程,就表示这个线程是不重要的,在进程退出的时候,不用等待这个线程退出。
3.线程的创建
import time from threading import Thread def func(i): print("start%s" % i) time.sleep(1) print("end%s" % i) if __name__ == '__main__': Thread(target=func, args=(1,)).start()
面向对象的方式开启线程
from threading import Thread class MyThread(Thread): def __init__(self, a, b): self.a = a self.b = b super().__init__() def run(self): print(self.ident) t = MyThread(1, 2) t.start() # 开启线程,才在线程中执行run方法 print(t.ident)
4.Thread类的其他方法
# Thread实例对象的方法 isAlive(): 返回线程是否活动的 getName(): 返回线程名 setName(): 设置线程名。 # threading模块提供的一些方法: threading.currentThread(): 返回当前的线程变量。 threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。 threading.activeCount(): 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果
5.守护线程
主线程会等待子线程结束之后才结束
守护线程随着主线程的结束而结束
守护线程会在主线程的代码结束之后继续守护其他子线程
import time from threading import Thread def func(): while True: print("in func") time.sleep(1) def son(): for i in range(3): print("in son") time.sleep(1) t = Thread(target=func) t.daemon = True # 守护线程 t.start() Thread(target=son).start()
守护进程
- 会随着主进程的代码结束而结束
- 如果主进程代码结束之后还有其他子进程在运行,守护进程不守护
守护线程
- 会随着主线程的结束而结束
- 如果主线程代码结束之后还有其他子线程在运行,守护线程也守护
守护进程与守护线程的区别
- 守护进程和守护线程的结束原理不同
- 守护进程需要主进程回收资源
- 守护线程是随着进程的结束才结束的
- 其他子线程结束 --> 主线程结束 --> 主进程结束 --> 整个进程中所有资源都被回收 --> 守护线程也会被回收
6.线程锁
- 多个线程同时操作全局变量/静态变量,会产生数据不安全的现象
- 不要去操作去全局变量,不要去操作类里面的静态变量
- +=,-=,*=,/=,if,while 数据不安全
- queue,logging , 列表,字典中的方法去操作,数据是安全的的
互斥锁
+=, -=, *= , /=, if ,while 数据不安全,+ 和赋值是分开的两个操作
from threading import Thread, Lock n = 0 def add(lock): for i in range(50000): global n with lock: n += 1 def sub(lock): for i in range(50000): global n with lock: n -= 1 t_list = [] lock = Lock() for i in range(2): t1 = Thread(target=add, args=(lock,)) t1.start() t2 = Thread(target=sub, args=(lock,)) t2.start() t_list.append(t1) t_list.append(t2) for t in t_list: t.join() print(n)
append pop strip数据安全 列表中的方法或者字典中的方法去操作全局变量的时候 数据安全的
import time from threading import Thread, Lock n = [] def append(): for i in range(50000): n.append(1) def pop(lock): for i in range(50000): with lock: if not n: time.sleep(0.0000001) n.pop() t_list = [] lock = Lock() for i in range(20): t1 = Thread(target=append) t1.start() t2 = Thread(target=pop, args=(lock,)) t2.start() t_list.append(t1) t_list.append(t2) for t in t_list: t.join() print(n)
# 不加锁:并发执行,速度快,数据不安全 import time from threading import current_thread, Thread, Lock def task(): global n print('%s is running' % current_thread().getName()) temp = n time.sleep(0.5) n = temp - 1 if __name__ == '__main__': n = 100 lock = Lock() threads = [] start_time = time.time() for i in range(100): t = Thread(target=task) threads.append(t) t.start() for t in threads: t.join() stop_time = time.time() print('主:%s n:%s' % (stop_time - start_time, n)) # 执行结果 """ Thread-1 is running Thread-2 is running ...... Thread-100 is running 主:0.5216062068939209 n:99 """ # 不加锁:未加锁部分并发执行,加锁部分串行执行,速度慢,数据安全 import time from threading import current_thread,Thread, Lock def task(): # 未加锁的代码并发运行 time.sleep(3) print('%s start to run' % current_thread().getName()) global n # 加锁的代码串行运行 lock.acquire() temp = n time.sleep(0.5) n = temp - 1 lock.release() if __name__ == '__main__': n = 100 lock = Lock() threads = [] start_time = time.time() for i in range(100): t = Thread(target=task) threads.append(t) t.start() for t in threads: t.join() stop_time = time.time() print('主:%s n:%s' % (stop_time - start_time, n)) # 执行结果 """ Thread-1 is running Thread-2 is running ...... Thread-100 is running 主:53.294203758239746 n:0 """ # 在此可能有疑问:既然加锁会让运行变成串行,那么我在start之后立即使用join,就不用加锁了啊,也是串行的效果。没错,在start之后立刻使用jion,肯定会将100个任务的执行变成串行,毫无疑问,最终n的结果也肯定是0,是安全的,但问题是start后立即join:任务内的所有代码都是串行执行的,而加锁,只是加锁的部分即修改共享数据的部分是串行的 # 单从保证数据安全方面,二者都可以实现,但很明显是加锁的效率更高. from threading import current_thread, Thread, Lock import time def task(): time.sleep(3) print('%s start to run' % current_thread().getName()) global n temp = n time.sleep(0.5) n = temp - 1 if __name__ == '__main__': n = 100 lock = Lock() start_time = time.time() for i in range(100): t = Thread(target=task) t.start() t.join() stop_time = time.time() print('主:%s n:%s' % (stop_time - start_time, n)) # 执行结果 """ Thread-1 start to run Thread-2 start to run ...... Thread-100 start to run 主:350.6937336921692 n:0 # 耗时是多么的恐怖 """
递归锁
from threading import Lock,RLock Lock 互斥锁,效率高 RLock 递归锁,效率相对低 # 互斥锁,在同一个线程中只能被acquire一次 lis = Lock() lis.acquire() print("希望被锁住的代码") lis.release() # 递归锁 ,在同一个线程中可以被acquire多次 lis = RLock() lis.acquire() print("希望被锁住的代码") lis.release()
递归锁,在同一个线程中acquire次数和release次数一致
from threading import Thread,ALock def func(i,lock): lock.acquire() lock.acquire() print(i,":start") lock.release() lock.release() print(i,":end") lock = RLock() for i in range(3): Thread(target = func,args =(i,lock)).start() # 执行结果: # 0 : start # 0 : end # 1 : start # 1 : end # 2 : start # 2 : end
单例模式
import time from threading import Thread, Lock class Singleton(object): __instance = None lock = Lock() def __new__(cls, *args, **kwargs): with cls.lock: if not cls.__instance: time.sleep(0.0000001) cls.__instance = super().__new__(cls) return cls.__instance def func(): sing = Singleton() print(sing) for i in range(5): Thread(target=func).start() # 执行结果: <__main__.Singleton object at 0x000000000288FE48> <__main__.Singleton object at 0x000000000288FE48> <__main__.Singleton object at 0x000000000288FE48> <__main__.Singleton object at 0x000000000288FE48> <__main__.Singleton object at 0x000000000288FE48>
from threading import RLock class Singleton(object): instance = None # 静态字段,类变量 lock = RLock() def __init__(self, name): """ 初始化对象 :param name: """ self.name = name def __new__(cls, *args, **kwargs): """ 创建对象 :param args: :param kwargs: :return: """ if cls.instance: return cls.instance with cls.lock: if not cls.instance: cls.instance = object().__new__(cls) return cls.instance for i in range(5): obj = Singleton("小白") print(obj.name)
死锁现象
所谓死锁: 是指两个或两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
死锁现象是怎么产生的?
- 多把(互斥/递归)锁,并且在多个线程中交叉使用
- 如果是互斥锁,出现了死锁现象,最快的解决方案把所有的互斥锁都改成一把递归锁,程序的效率会降低
- 递归锁,效率低,但是解决死锁现象有奇效
- 互斥锁,效率高,但是多把容易出现死锁现象
死锁现象典型问题:科学家吃面
import time from threading import Thread, RLock noodle_lock = RLock() fork_lock = RLock() def eat(name): noodle_lock.acquire() print(name, '抢到面了') fork_lock.acquire() print(name, '抢到叉子了') print(name, '吃面') time.sleep(0.1) fork_lock.release() print(name, '放下叉子了') noodle_lock.release() print(name, '放下面了') def eat2(name): fork_lock.acquire() print(name, '抢到叉子了') noodle_lock.acquire() print(name, '抢到面了') print(name, '吃面') time.sleep(0.1) noodle_lock.release() print(name, '放下面了') fork_lock.release() print(name, '放下叉子了') Thread(target=eat, args=('小白',)).start() Thread(target=eat2, args=('星月',)).start() Thread(target=eat, args=('落花',)).start() Thread(target=eat2, args=('吃货',)).start()
import time from threading import Thread, Lock fork_noodle_lock = Lock() def eat(name): fork_noodle_lock.acquire() print(name, '抢到面了') print(name, '抢到叉子了') print(name, '吃面') time.sleep(0.1) fork_noodle_lock.release() print(name, '放下叉子了') print(name, '放下面了') def eat2(name): fork_noodle_lock.acquire() print(name, '抢到叉子了') print(name, '抢到面了') print(name, '吃面') fork_noodle_lock.release() print(name, '放下面了') print(name, '放下叉子了') Thread(target=eat, args=('小白',)).start() Thread(target=eat2, args=('星月',)).start() Thread(target=eat, args=('落花',)).start() Thread(target=eat2, args=('吃货',)).start()
3.队列
queue 队列, 线程之间数据安全的容器队列
先进先出
import queue q = queue.Queue() q.put('first') q.put('second') q.put('third') print(q.get()) print(q.get()) print(q.get()) # 执行结果 first second third
import queue from queue import Empty # 不是内置的错误类型,而是queue模块中的错误 q = queue.Queue(4) # fifo 先进先出的队列 try: q.get_nowait() # 取值,如果遇到队列为空,则就会抛出异常 except Empty:pass print('队列为空,继续其他内容') # 队列为空,继续其他内容
后进先出
from queue import LifoQueue lq = LifoQueue() lq.put(1) lq.put(2) lq.put(3) print(lq.get()) print(lq.get()) print(lq.get()) # 执行结果 third second first
优先级队列:是按照ASCII码的顺序取值的
from queue import PriorityQueue priq = PriorityQueue() # put进入一个元组,元组的第一个元素是优先级(通常是数字,也可以是非数字之间的比较),数字越小优先级越高 priq.put((2, "小白")) priq.put((1, "寂寞")) priq.put((3, "相逢")) print(priq.get()) print(priq.get()) print(priq.get()) # 执行结果 (1, '寂寞') (2, '小白') (3, '相逢')
待续