1.进程回顾
之前已经了解了操作系统中进程的概念,程序并不能单独运行,只有将程序装载到内存中,系统为它分配资源才能运行,而这种执行的程序就称之为进程。程序和进程的区别就在于:程序是指令的集合,它是进程运行的静态描述文本;进程是程序的一次执行活动,属于动态概念。在多道编程中,我们允许多个程序同时加载到内存中,在操作系统的调度下,可以实现并发地执行。这是这样的设计,大大提高了CPU的利用率。进程的出现让每个用户感觉到自己独享CPU,因此,进程就是为了在CPU上实现多道编程而提出的。
2.有了进程为什么还需要线程?
进程有很多优点,它提供了多道编程,让我们感觉我们每个人都拥有自己的CPU和其他资源,可以提高计算机的利用率。很多人就不理解了,既然进程这么优秀,为什么还要线程呢?其实,仔细观察就会发现进程还是有很多缺陷的,主要体现在两点上: 1.进程只能在一个时间干一件事,如果想同时干两件事或多件事,进程就无能为力了。 2.进程在执行的过程中如果阻塞,例如等待输入,整个进程就会挂起,即使进程中有些工作不依赖于输入的数据,也将无法执行,浪费系统资源。
3.线程的基本概念
线程是进程中的一个单位 进程:计算机中最小的资源分配单位 线程:计算机中被CPU调度的最小单位
4.为什么要使用线程?
1.线程也叫轻量级的进程,最大的特点是同一个进程中的多个子线程是共享一部分数据(一个进程的多个子进程和父进程相互隔离,没有关系)2.线程的创建\销毁\上线文切换要比进程高效
5.进程和线程的区别?
进程:数据隔离(但也可以使用Manager模块实现数据共享),使用开销大 线程:数据共享(全局变量),使用开销小 通过小的细节来体现进程和线程的区别: 1.os.getpid() 进程:父进程和子进程的pid都不同,所以都各自有自己的内存空间 2.if __name_- == '__main__' 进程和线程的创建原理不同,所以不需要if __name__ == '__main__' 因为新的线程是在主线程的内存中,所以新的线程和主线程共享同一段代码,不需要import导入,也就不存在子线程中又重复一次创建线程的过程
在cpython中多进程可以使用多核,但是多线程不能使用多核是cpython解释器的原因造成的,因为cpython存在GIL全局解释器锁,导致同一时间内只能有一个线程访问cpu,保证数据安全,但是jpython和pypy就能访问多核的cpu 正常的多个线程和多个进程可不可以利用多核(多个cpu )? 可以,因为cpu执行的是进程中的线程
解释性语言不能多个线程同时访问多个cpu,这样做能保证数据不出错
6.Python中的线程模块
threading 和 multiprocessing在使用上基本相同 先有的threading模块,没有池的功能 multiprocessing完全模仿threading模块完成的,实现了池的功能 concurrent.futures,实现了线程池\进程池
如何开启一个线程
from threading import Thread import os def func(): print('in fucn ',os.getpid()) print('in main ',os.getpid()) Thread(target=func).start() #两个打印pid是一样的,说明线程是由进程产生的
方法1
from threading import Thread import time class Sayhi(Thread): def __init__(self,name): super().__init__() self.name=name def run(self): time.sleep(2) print('%s say hello' % self.name) if __name__ == '__main__': t = Sayhi('kobe') t.start() print('主线程')
方法2
开启一个多线程
import time from threading import Thread import os def func(i): time.sleep(1) print('in fucn',i,os.getpid()) print('in main',os.getpid()) for i in range(20): #Thread(target=func,args=(i,)).start() #问题:多个线程在同一个进程中处理数据的时候,数据需不需要锁? 当然需要,这就是GIL锁存在的意义:同一进程内的同一时刻只能有一个线程访问cpu来修改同一个数据,导致Python中的多线程是‘伪多线程’,所以cpython解释器不能使用多核
线程的其他方法
Thread实例对象的方法 isAlive(): 返回线程是否活动的。 getName(): 返回线程名。 setName(): 设置线程名。 threading模块提供的一些方法: threading.currentThread(): 返回当前的线程变量。 threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行子线程启动后、结束前,不包括启动前和终止后的线程。 threading.active_count(): 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果 主线程也是一个线程,每个线程没有被start()的时候们不会被执行 判断主线程是否结束了 线程有terminate么? 没有terminate 不能强制结束 所有的子线程都会在执行完所有的任务之后自动结束 # 返回一个存储着所有线程对象的列表 from threading import enumerate,Thread def func(): print('in son thread') Thread(target=func).start() print(enumerate())
通过dis模块查看机器码
from dis import dis def func(): a =[] a.append(1) dis(func) 注意:cpu在0.9ms内可以处理450w机器码
python解释器 --> 字节码 --> 机器码
7.线程中守护进程
import time from threading import Thread,activeCount #守护线程守护的 def daemon_func(): while True: time.sleep(0.5) print('守护线程') def son_func(): print('start son') time.sleep(10) print('end son') print('子线程:',time.time()-start) start= time.time() #子线程 t = Thread(target=daemon_func) t.daemon = True t.start() #子线程 Thread(target=son_func).start() #主线程 print(activeCount()) time.sleep(3) print('主线程结束') print('主线程:',time.time()-start)
线程中的守护线程
无论是进程还是线程,都遵循:守护xx会等待主xx运行完毕后被销毁。需要强调的是:运行完毕并非终止运行
1.对主进程来说,运行完毕指的是主进程代码运行完毕
2.对主线程来说,运行完毕指的是主线程所在的进程内所有非守护线程统统运行完毕,主线程才算运行完毕
如果你设置一个线程为守护线程,就表示你在说这个线程是不重要的,在进程退出的时候,不用等待这个线程退出。如果你的主线程在退出的时候,不用等待那些子线程完成,那就设置这些线程的daemon属性。
1.主线程会等待子线程的结束而结束,主线程不会管守护线程的线程是否执行完毕2.守护线程会随着主线程的结束而结束,注意是运行完毕,并非终止运行,守护线程会守护主线程和所有的子线程3.整个Python会在所有的非守护线程退出后才会结束,即进程中没有非守护线程存在的时候才结束。(主进程会随着主线程的结束而结束)
问题 1.主线程需不需要回收子线程的资源 # 不需要,线程资源属于进程,所以进程结束了,线程的资源自然就被回收了 2.主线程为什么要等待子线程结束之后才结束 # 主线程结束意味着进程进程,进程结束,所有的子线程都会结束,要想让子线程能够顺利执行完,主线程只能等 3.守护线程到底是怎么结束的 # 主线程结束了,主进程也结束,守护线程被主进程的结束(运行完毕)给杀死了
8.思考:有了GIL锁,线程中还需要锁么?
有必要理论上有了GIL锁会保证同一时间内只有一个线程执行cpu指令,但是运行多线程不加锁的话,会产生数据不安全的情况有下列代码: 当tt里面的第一个线程在执行的时候,假如说执行到count =count+1 但是还没来得及把count放回去的时候,cpu就切换到下一个线程执行,此时的count并没有 修改,还是0 ,于是就将coun加1,count=1,在切换到上一个线程继续执行,cpu寄存器里面 存放的上一个线程的状态是要将count+1的结果存回给count,所以在两个线程运行的时候只加了一次 造成count值不对,也就造成了数据不安全的问题。所以要加上锁 这种情况主要是由于是cpu切换线程造成的 ''' coun = 0 def func(): count +=1 for i in range(2): tt = Thread(target=func) tt.start() '''线程中不安全现象: 1.对全局变量进行修改 2.对某个值进行+=,-=,*=,/+,%=等等操作 产生的原因是cpu切换线程造成的 为什么某个值进行+=,-=,*=,/+,%=在cpu切换的时候造成数据不安全的现象? 因为在count +=1等操作,在cpu的机器码中执行是可再分的,分成两步执行,count +1,count=count+1 所以在 cpu切换线程的时候,有可能出现两个线程分别对一个数据进行修改,而返回的时候只是返回一个 但是Python中常用的数据类型,list dict tuple set等修改或增加的方法都具有原子性(不是增加就是修改,就算cpu切换也无所谓) 具有原子性也是在cpu底层机器码中体现出来的 注意: 队列(queue)的原子性比以上的数据类型更强,因为队列的queue.get()获取不到数据的时候回阻塞住,而列表的list.pop()在没有数据时会报错
9.锁
互斥锁:在同一个线程中连续acquire两次,并且可以做到多个线程被锁的代码同时只有一个线程执行,中就是互斥锁,就是死锁 递归锁:在同一个线程中可以连续acquire多次,并且可以做到多个线程被锁的代码同时只有一个线程执行,但是大部分时候递归锁的情况都能使用互斥锁来解决(通常使用递归锁的代码都是有逻辑上问题的) 一般情况互斥锁的性能和时间复杂度要比递归所低,但是递归锁一把锁就可以打天下 递归锁最大的特点是:在同一个线程和进程不会出现死锁现象,并且能连续acquire两次,这是最大的特点
死锁现象;创建了两把锁以上,并且交替使用就可能出现死锁 只要是1把锁,递归锁永远不会死锁 只要是2把锁以及以上,都有可能产生死锁现象 r1 = r2 = RLock() 这是同一把锁 只要是2把锁以上,交替使用,递归锁和互斥锁都会产生死锁
1.gil 保证线程同一时刻只能一个线程访问CPU,不可能有两个线程同时在CPU上执行指令2.lock 锁 保证某一段代码 在没有执行完毕之后,不可能有另一个线程也执
from threading import Lock,Thread import time noodle_lock = Lock() fork_lock = Lock() def eat1(name): noodle_lock.acquire() # 阻塞等面 time.sleep(0.5) print('%s拿到面了'%name) fork_lock.acquire() print('%s拿到叉子了' % name) print('%s吃面'%name) fork_lock.release() print('%s放下叉子了' % name) noodle_lock.release() print('%s放下面了' % name) def eat2(name): fork_lock.acquire() # 阻塞等叉子 print('%s拿到叉子了' % name) noodle_lock.acquire() print('%s拿到面了'%name) print('%s吃面'%name) noodle_lock.release() print('%s放下面了' % name) fork_lock.release() print('%s放下叉子了' % name) Thread(target=eat1,args = ('admin',)).start() Thread(target=eat2,args = ('kobe',)).start() Thread(target=eat1,args = ('jordan',)).start()
死锁
from threading import RLock,Thread fork_lock = noodle_lock = RLock() def eat1(name): noodle_lock.acquire() # 阻塞 宝元等面 print('%s拿到面了'%name) fork_lock.acquire() print('%s拿到叉子了' % name) print('%s吃面'%name) fork_lock.release() print('%s放下叉子了' % name) noodle_lock.release() print('%s放下面了' % name) def eat2(name): fork_lock.acquire() # 阻塞 wusir等叉子 print('%s拿到叉子了' % name) noodle_lock.acquire() print('%s拿到面了'%name) print('%s吃面'%name) noodle_lock.release() print('%s放下面了' % name) fork_lock.release() print('%s放下叉子了' % name) Thread(target=eat1,args = ('alex',)).start() Thread(target=eat2,args = ('wusir',)).start() Thread(target=eat1,args = ('baoyuan',)).start()
递归锁
10.队列
Queue就是一个线程队列的类,自带lock锁,实现了线程安全的数据类型队列是一个线程安全的数据类型,队列在多线程中占有重要的安全位置from queue import Queue:是线程安全的,和进程无关,进程通信是ipc,线程是共享内存的,二者通信方式不同
from queue import Queue #Queue就是一个线程队列的类,自带lock锁,实现了线程安全的数据类型 #队列是一个线程安全的数据类型,只有几个特点的属性不是安全的 q = Queue() # 先进先出队列 # 在多线程下都不准 # q.empty() 判断是否为空 # q.full() 判断是否为满 # q.qsize() 队列的大小 #在多进程下都是数据安全的 q.put({1,2,3}) q.put_nowait('abc') print(q.get_nowait()) print(q.get())
先进先出队列
# 先进后出的队列 last in first out from queue import LifoQueue #线程安全的队列 栈和后进先出的场景都可以用 lfq = LifoQueue() lfq.put(1) lfq.put('abc') lfq.put({'}) print(lfq.get()) print(lfq.get()) print(lfq.get())
后进先出队列
from queue import PriorityQueue # 优先级队列 pq = PriorityQueue() pq.put((10,'askdhiu')) #前面的数字就是优先级,越小越优先 pq.put((2,'asljlg')) pq.put((20,'asljlg')) print(pq.get()) print(pq.get()) print(pq.get())
优先级队列