一、线程
是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行。线程有就绪、阻塞和运行三种基本状态。
二、线程的两种创建方式
1.第一种方式
from threading import Thread T = Thread(function,args=(arg1,arg2,...)) T.start()
2.第二种方式
from threading import Thread Class MThread(Thread): Def run(self): Pass mt = MThread() mt.start()
三、线程的空间
- 查看pid
import os def f(): print(os.getpid()) t = Thread(target=f,) t.start()
2.线程空间不是隔离的
import os def f(): print('子线程pid:', os.getpid())#(1) t = Thread(target=f,) t.start() print('主线程pid:', os.getpid())#(2) 说明:(1)、(2)两处的值是一样的。可见,线程都是在一个进程内,而一个进程都有自己独立的空间。
3.线程与进程的效率对比
(1)只是创建线程和进程
def f1(): pass def f2(): pass if __name__ == '__main__': t_s = time.time() t_lst = [] for i in range(20): t = Thread(target=f1,) t.start() t_lst.append(t) for tt in t_lst: tt.join() t_e = time.time() p_s = time.time() p_lst = [] for i in range(20): p = Process(target=f2,) p.start() p_lst.append(p) for pp in p_lst: pp.join() p_e = time.time() print('线程创建时间:', t_e - t_s) print('进程创建时间:', p_e - p_s) 结果: 线程创建时间: 0.004002809524536133 进程创建时间: 1.838303565979004
通过结果可以看出来,同样创建20个,线程只需要了0.004秒,也就是4毫秒,而创建进程却是1838.3毫秒。得出一个结论:进程创建过程比线程创建过程要麻烦。因为进程创建的过程是,需要在内存里开辟一个空间,把解释器代码加载进来,还需要把自己写的程序也加载进去。而线程是进程里的一个实体,只需要进程中的一点资源。所有线程共享进程中的所有资源。
(2)再来一个线程中有I/O阻塞的比较
(2)def f1(): print('f1>>>>>aaaaa') time.sleep(1) print("f1>>>>>bbbbb") def f2(): print('f2>>>>>aaaaa') time.sleep(1) print("f2>>>>>bbbbb") if __name__ == '__main__': t_s = time.time() t_lst = [] for i in range(20): t = Thread(target=f1,) t.start() t_lst.append(t) for tt in t_lst: tt.join() t_e = time.time() p_s = time.time() p_lst = [] for i in range(20): p = Process(target=f2,) p.start() p_lst.append(p) for pp in p_lst: pp.join() p_e = time.time() print('线程创建时间:', t_e - t_s) print('进程创建时间:', p_e - p_s) 结果: 。。。。。 线程创建时间: 1.0074326992034912 进程创建时间: 3.006932497024536
线程只是在一个进程中操作,这样就利用多道技术,实现了并发(Python中的线程不能实现多核方式,后面介绍)。进程却要创建20个,开销,时间都要比线程的多。
(3)这个是计算型的操作
def f1(): # print('f1>>>>>aaaaa') # time.sleep(1) # print("f1>>>>>bbbbb") n = 10 for i in range(10000000): n += i def f2(): # print('f2>>>>>aaaaa') # time.sleep(1) # print("f2>>>>>bbbbb") n = 10 for i in range(10000000): n += i if __name__ == '__main__': t_s = time.time() t_lst = [] for i in range(5): t = Thread(target=f1,) t.start() t_lst.append(t) for tt in t_lst: tt.join() t_e = time.time() p_s = time.time() p_lst = [] for i in range(5): p = Process(target=f2,) p.start() p_lst.append(p) for pp in p_lst: pp.join() p_e = time.time() print('线程操作时间:', t_e - t_s) print('进程操作时间:', p_e - p_s) 结果: 线程操作时间: 3.7418324947357178 进程操作时间: 2.925071954727173
好神奇啊,这次进程使用的时间短了,线程的却多了。这也是因为,线程不能使用多核技术。
四、锁
- Lock
先看第一段代码:
num = 100 def f(): global num tmp = num tmp -= 1 time.sleep(0.01) num = tmp if __name__ == '__main__': t_lst = [] for i in range(10): t1 = Thread(target=f) t1.start() t_lst.append(t1) [t.join() for t in t_lst] print(num) 执行结果: 99
上面这段代码的作用是,想循环10次,为num每次减1,但是结果却是99。原因就是,线程执行的太快了,导致10个线程都执行到了time.sleep(0.01)这里,然后都去等着操作系统再次调用。调用到了后,再次执行赋值的操作,这样num就是99了。
为了数据安全,加把锁吧,看代码:
num = 100 def f(loc): global num loc.acquire() tmp = num tmp -= 1 time.sleep(0.01) num = tmp loc.release() if __name__ == '__main__': loc = Lock() t_lst = [] for i in range(10): t1 = Thread(target=f,args=(loc,)) t1.start() t_lst.append(t1) [t.join() for t in t_lst] print(num) 结果: 90
这次完成了心愿,结果是90了。当第一个线程拿到锁后,执行所有的操作,即便有sleep需要等待,其他9个线程也得等着,必须等着第一完成了。第一个完成后,接下来就是第二个线程,也跟第一个一样,不管其他的怎么着急,就是慢慢执行自己的。依次类推,最后结果就是90了。
2.死锁
在工作中有可能会有锁的嵌套,稍有不慎,那么就会死锁了。还是看代码:
# def f1(locA, locB): # locA.acquire() # print('f1aaaaaaaaaaaa') # time.sleep(0.1) # locB.acquire() # print('f1bbbbbbbbb') # locB.release() # locA.release() # def f2(locA, locB): # locB.acquire() # print('f2--aaaaaaaa') # time.sleep(0.1) # locA.acquire() # print('f2--bbbbbbbb') # locA.release() # locB.release() # if __name__ == '__main__': # locA = Lock() # locB = Lock() # # t1 = Thread(target=f1, args=(locA, locB)) # t2 = Thread(target=f2, args=(locA, locB)) # # t1.start() # t2.start()
运行后,会看到程序一直处于运行中。不会往下走了,因为线程t1等着要locB的锁,而线程t2等着要线程locA的锁,从而导致两边就这样互相等待着,程序一直不运行。
3.递归锁
为了解决死锁,Python出现了递归锁。看代码:
# def f1(locA, locB): # locA.acquire() # print('f1aaaaaaaaaaaa') # time.sleep(0.1) # locB.acquire() # print('f1bbbbbbbbb') # locB.release() # locA.release() # def f2(locA, locB): # # locB.acquire() # with locB: # print('f2--aaaaaaaa') # time.sleep(0.1) # # locA.acquire() # with locA: # print('f2--bbbbbbbb') # # locA.release() # # locB.release() # if __name__ == '__main__': # locA = locB = RLock() # t1 = Thread(target=f1, args=(locA, locB)) # t2 = Thread(target=f2, args=(locA, locB)) # # t1.start() # t2.start()
递归锁,当acquire时,内部会有计数器,加1;前面acquire几次,就会记为几,当释放时,会依次再把锁释放掉。
4.GIL
这个是加载cpython解释器上的一把锁,因为它而导致Python的线程不能使用多核技术,只能串行。看图:
接下来看图说话:当我们运行了一个py文件后,其实就是启动了一个进程,操作系统就会把代码读取到内容中,并为这个进程分配相应的内存空间。在这个进程中,还会读入解释器的代码。编辑器会把这些代码处理成C语言的字节码,然后虚拟机把这些字节码再处理成二进制,这样CPU就可以处理了。
GIL锁就是加在解释器上的,每次只能有一个线程拿到这个GIL锁,其他的线程只能等待前面的把锁释放了再拿。遇到I/O阻塞的,操作系统,会把GIL锁拿回来,交给下一个线程。如果再遇到I/O阻塞还会继续拿过来交给下一个线程。这样就实现了类似单核的并发。
还有一些计算型的程序,使用线程时,第一个线程拿到了GIL锁后,会一直执行完毕。然后,操作系统把锁再交给下一个线程,这个线程还是从头执行到尾。也就是遇到计算型,中间没有I/O阻塞的程序,就是串行,一个一个线程去执行。
这里,就可以看出前面进程和线程比较时,关于时间多少的问题了。