一、线程和进程
线程和进程普遍特点:
- 线程是最小的调度单位
- 进程是最小的管理单元
- 一个进程必须至少有一个线程
- 没有线程,进程也就不复存在
二、多线程
多线程特点
python线程特点:
- 线程的并发是利用cpu上下文的切换(是并发,不是并行)
- 多线程执行顺序是无序的
1.无序的,并发的
import threading
import time
def test1(n):
time.sleep(2)
print('task', n)
for i in range(10):
t = threading.Thread(target=test1,args=('t-%s' % i,))
t.start()
输出:
task t-4
task t-0
task t-1
task t-3
task t-2
task t-6
task t-7
task t-9
task t-5
task t-8
2.多线程共享全局变量
import threading
g = 0
def test1():
global g
for i in range(10):
g += 1
print(g)
def test2():
global g
for i in range(10):
g += 1
print(g)
t1 = threading.Thread(target=test1)
t2 = threading.Thread(target=test2)
t1.start()
t2.start()
输出:
10
20
3.线程是继承在进程里的,没有进程就没有线程
4.GIL全局解释器锁
我们来看下面代码,我们起了两个线程对global_num执行加一操作,但是结果并非我们想要的
import threading
global_num = 0
def test1():
global global_num
for i in range(1000000):
global_num += 1
def test2():
global global_num
for i in range(1000000):
global_num += 1
t1 = threading.Thread(target=test1)
t2 = threading.Thread(target=test2)
t1.start()
t2.start()
print(global_num)
输出:
136936 # 再次执行数字又会变,但总不会超过1000000
加入.join()等待执行完毕
import threading
global_num = 0
def test1():
global global_num
for i in range(1000000):
global_num += 1
def test2():
global global_num
for i in range(1000000):
global_num += 1
t1 = threading.Thread(target=test1)
t2 = threading.Thread(target=test2)
t1.start()
t2.start()
t1.join() #加入等待执行完毕
t2.join()
print(global_num)
输出:
1200733 #多次执行,结果都在1000000-2000000之间
由于多线成中共享全局变量的时候会有线程对全局变量进行的资源竞争我们引入GIL
GIL全称Global Interpreter Lock,虽然我们在现在的解释器用到了这个概念但它并不是python的特性,而是通常情况下用到的Cpython编译器。就比如c++编程语言只是一种语法或编程标准,它可以用不同的编译器来编译成可执行代码。c++比较有名的编译器有GCC,VC++等,而python也类似,也有比如Cpython,PyPy还有Jpython编译环境,但Jpython就没有GIL概念,所以在很多人的概念里Cpython就是python,也就想当然的把GIL归结为python的缺陷。
所有我们要明确一点:GIL并不是python的特性,python完全不依赖于GIL
我们来看下官方的解释看下GIL在Cpython中是什么:
In CPython, the global interpreter lock, or GIL, is a mutex that prevents multiple native threads from executing Python bytecodes at once. This lock is necessary mainly because CPython’s memory management is not thread-safe. (However, since the GIL exists, other features have grown to depend on the guarantees that it enforces.)
在CPython中,全局解释器锁(global interpreter lock, GIL)是一个互斥体,它防止多个本机线程同时执行Python字节码。这个锁是必要的,主要是因为CPython的内存管理非线程安全的。(然而,自从GIL存在以来,其他特性已经逐渐依赖于它强制执行的保证。)
那么为什么会有GIL?
随着电脑多核cpu的出现核cpu频率的提升,为了充分利用多核处理器,进行多线程的编程方式更为普及,随之而来的困难是线程之间数据的一致性和状态同步,而python也利用了多核,所以也逃不开这个困难,为了解决这个数据不能同步的问题,设计了gil全局解释器锁。
下面我们为之前的程序加上互斥锁:
import threading
lock = threading.Lock()
global_num = 0
def test1():
global global_num
lock.acquire()
for i in range(1000000):
global_num += 1
lock.release()
def test2():
global global_num
lock.acquire()
for i in range(1000000):
global_num += 1
lock.release()
t1 = threading.Thread(target=test1)
t2 = threading.Thread(target=test2)
t1.start()
t2.start()
print(global_num)
输出:
149361 #输出结果基本上在几十万上下
加入.join()等待执行完毕
import threading
lock = threading.Lock()
global_num = 0
def test1():
global global_num
lock.acquire()
for i in range(1000000):
global_num += 1
lock.release()
def test2():
global global_num
lock.acquire()
for i in range(1000000):
global_num += 1
lock.release()
t1 = threading.Thread(target=test1)
t2 = threading.Thread(target=test2)
t1.start()
t2.start()
t1.join()
t2.join()
print(global_num)
输出:
2000000 #得到了我们想要的结果
GIL实现原理过程
- 线程一首先拿到共享数据池内count
- 申请gil lock将数据全局变量count声明除线程一以外任何线程不可调用变量count
- python解释器调用系统原生线程(程序不能直接调用硬件而是通过系统来调用)
- 系统线程将变量count拿到cpu1上执行例如赋值等操作
- 变量count执行到时间后被要求释放gil,此时变量count又可被其他线程调用
- 线程二也要修改变量count
- 重复上述2
- 重复上述3
- 重复上述4过程
- 此次变量count在cpu上执行完成了count++操作并将结果交给python解释器
- python解释器将赋值结果返回线程共享数据池并释放gil,此时变量count=1
- 由于线程一之前在cpu中执行count赋值操作没有执行完就被释放了gil,所以必须重复之前第一次执行的所有过程
- 当cpu执行完count++操作后释放gil并赋值给count,此时在count=1的基础上又赋值count=1
总结:即在同一个Python进程中,在开启多线程的情况下,同一时刻只能有一个线程执行,因为cpython的内存管理不是线程安全,这样就导致了在现在多核处理器上,一个Python进程无法充分利用处理器的多核处理。在Python的多线程执行环境中,每个线程都需要去先争取GIL锁,等到获取锁之后,才能继续执行。
5.在IO密集型的代码里,适合用多线程
三、多进程
1.一个程序运行起来之后,代码加用到的资源称之为进程,它是操作系统分配资源的基本单位,不仅可以通过线程完成多任务,进程也是可以的
2.进程 之间是相互独立的,多进程并发
import multiprocessing
import time
g = 0
def test1(n):
time.sleep(3)
global g
for i in range(10):
g += 1
print(g)
def test2(n):
time.sleep(3)
global g
for i in range(10):
g += 1
print(g)
if __name__ == '__main__':
p1 = multiprocessing.Process(target=test1,args=(1,))
p2 = multiprocessing.Process(target=test2,args=(2,))
p1.start()
p2.start()
输出:
10
10
3.进程池并发
import multiprocessing
from multiprocessing import Pool
import time
import threading
g_num = 0
def test1(n):
for i in range(n):
time.sleep(1)
print('test1', i)
def test2(n):
for i in range(n):
time.sleep(1)
print('test2', i)
def test3(n):
for i in range(n):
time.sleep(1)
print('test3', i)
def test4(n):
for i in range(n):
time.sleep(1)
print('test4', i)
if __name__ == '__main__':
pool = Pool(3)#把进程声明出来括号里不写东西说明无限制,如果写数字,就是最大的进程数
pool.apply_async(test1,(10,))#用pool去调用函数test1,参数为10格式为(10,)
pool.apply_async(test2,(10,))#用pool去调用函数test2,参数为10格式为(10,)
pool.apply_async(test3,(10,))#用pool去调用函数test3,参数为10格式为(10,)
pool.apply_async(test4,(10,))#用pool去调用函数test4,参数为10格式为(10,)
pool.close() # close必须在join的前面
pool.join()