一、线程
是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行。线程有就绪、阻塞和运行三种基本状态。
二、线程的两种创建方式
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阻塞的程序,就是串行,一个一个线程去执行。
这里,就可以看出前面进程和线程比较时,关于时间多少的问题了。