深入理解python的多线程,多进程和协程(多线程)

1.前言

  1.  什么是多线程,多进程

  2.  GIL锁

2.多线程

  1. 多线程开发

  2. 线程安全

  3. 线程锁

  4. 死锁

  5. 线程池

3.多进程

  1. 进程的三大模式

  2. 进程的常见功能

  3. 进程锁

  4. 进程池

4.协程


前言:

我们开发的程序中所有的行为都只能通过串行的形式运行,排队逐一执行,前面没有完成后面就没有办法运行,所以就有我们的多线程,多进程和协程。也就是并发式编程

线程计算机中可以被cpu调度的最小单元
进程计算机资源分配的最小单元(为线程提供资源)

一个进程可以有多个线程,同一个进程中的线程可以共享此进程中的资源

接下来我们看一个例子

import time

# 定义一个变量
result = 0
# 利用time模块拿到循环前的时间
print(time.time())
for i in range(100000000):
    result += i
print(result)
# 拿到循环后的时间
print(time.time())

我们用for循环让变量累加1亿次,运行了5s多,那怎样让速度加快呢,我们后面再说。

 GIL锁:

GIL,全局解释器锁(GLobal Interpreter Lock),是CPython解释器特有的(python解释器底层是用c来写的),让一个进程中同一时刻只能有一个线程被cpu调用由于GIL锁的存在则:

  • 计算密集型的程序:用多进程(大数据计算)
  • IO密集型的程序:用多线程(如文件读写,网络数据传输)

接下里步入正题进入多线程

1.多线程开发

之前的例子

import time

# 定义一个变量
result = 0
# 利用time模块拿到循环前的时间
print(time.time())
for i in range(100000000):
    result += i
print(result)
# 拿到循环后的时间
print(time.time())

 首先了解一下多线程的常见方法:

import threading导入多线程模块
start()线程准备结束(等待cpu调度)
join()等待子线程结束再继续执行主线程
setDaemon(逻辑布尔值)守护线程函数
setDaemon(Ture)为守护线程,主线程执行完毕后,子线程自动关闭
setDaemon(False)非守护线程,要等子线程执行后,主线程才能关闭

了解这些函数后我们改造一下代码:

这时我有一个需求:就是让一个数自增1亿,自减1亿我们用多线程来写一下

import threading

# 定义全局变量
number = 0


def _add():
    global number
    for i in range(100000000):
        number += 1


def _sub():
    global number
    for i in range(100000000):
        number -= 1


# 创建多线程(函数中参数target后面放的是多线程要执行的函数)
t1 = threading.Thread(target=_add)
t2 = threading.Thread(target=_sub)
# t1 t2准备完毕,等待cpu调动
t1.start()
t2.start()
# t1结束后再往下进行
t1.join()
# t2同理
t2.join()
print(number)

这就是这个需求的代码,我们运行一下

 这是我们发现不对呀,再运行一遍

 这时我们发现结果不仅不对,而且两次结果不同,这是为什么呢:

结果不正确的原因:

因为GIL锁的存在,只有一个cpu来执行,所以多线程会有一个分片机制,分片机制会让我们的多个线程中来回切换,在切换的过程中可能会出现数据紊乱,所以我们的结果不是固定的,针对这个我们也有方法来解决。

2.线程安全

针对这个分片机制---导致我们的线程在操作同一个数据时会出先错误,我们会有线程安全来解决这个问题。就是创建一把锁

创建锁threading.RLock()
加锁.acquire()
解锁.release()

我们先用锁改装一下代码:

import threading

# 定义全局变量
number = 0
# 定义一把锁lock
lock = threading.RLock()


def _add():
    # 加锁
    lock.acquire()
    global number
    for i in range(100000000):
        number += 1
    # 解锁
    lock.release()


def _sub():
    lock.acquire()
    global number
    for i in range(100000000):
        number -= 1
    lock.release()


# 创建多线程(函数中参数target后面放的是多线程要执行的函数)
t1 = threading.Thread(target=_add)
t2 = threading.Thread(target=_sub)
# t1 t2准备完毕,等待cpu调动
t1.start()
t2.start()
# t1结束后再往下进行
t1.join()
# t2同理
t2.join()
print(number)

运行一下

 这时我们发现结果正确

3.线程锁:

我们通过上面的例子认识到了锁,然后我们聊一下锁的作用:

一把锁只能被申请也就是加一次锁,然后没申请到锁的线程就会卡着不动,直到申请锁的线程将锁结束,另一个线程才能得到锁从而运行,这就是线程安全---锁的作用

然后我们深入了解一下锁:

我们不仅有RLock(递归锁)还有Lock(同步锁)

创建锁(RLock)threading.RLock()
加锁.acquire()
解锁.release()
创建锁(Lock)threading.Lock()
加锁.acquire()
解锁.release()

        那么RLock和Lock有什么区别呢:

我们只需要知道一点RLock可以进行锁的嵌套,而Lock不能进行嵌套,在不进行锁的嵌套时,Lock的效率要比RLock的效率高,那么什么是锁的嵌套

还是在之前的例子上加上锁的嵌套

import threading

# 定义全局变量
number = 0
# 定义一把锁lock
lock = threading.RLock()


def _add():
    # 加锁
    lock.acquire()
    global number
    # 在锁的基础上再来申请一把锁
    lock.acquire()
    for i in range(100000000):
        number += 1
    # 解开第一把锁
    lock.release()
    # 解锁
    lock.release()


def _sub():
    lock.acquire()
    global number
    for i in range(100000000):
        number -= 1
    lock.release()


# 创建多线程(函数中参数target后面放的是多线程要执行的函数)
t1 = threading.Thread(target=_add)
t2 = threading.Thread(target=_sub)
# t1 t2准备完毕,等待cpu调动
t1.start()
t2.start()
# t1结束后再往下进行
t1.join()
# t2同理
t2.join()
print(number)

运行一下

 4.死锁:

之前我们了解到了RLock锁的嵌套,那为什么Lock锁不能嵌套呢,我们实践一下,将代码中的RLock改为Lock试试

import threading

# 定义全局变量
number = 0
# 定义一把锁lock
lock = threading.Lock()


def _add():
    # 加锁
    lock.acquire()
    global number
    # 在锁的基础上再来申请一把锁
    lock.acquire()
    for i in range(100000000):
        number += 1
    # 解开第一把锁
    lock.release()
    # 解锁
    lock.release()


def _sub():
    lock.acquire()
    global number
    for i in range(100000000):
        number -= 1
    lock.release()


# 创建多线程(函数中参数target后面放的是多线程要执行的函数)
t1 = threading.Thread(target=_add)
t2 = threading.Thread(target=_sub)
# t1 t2准备完毕,等待cpu调动
t1.start()
t2.start()
# t1结束后再往下进行
t1.join()
# t2同理
t2.join()
print(number)

运行一下

小编等了100年没有出现结果,这时就会出现死锁,这个代码就会一直卡在上第二把锁的地方,

这就是死锁,我们如果不修改代码的话是解不开的,所以小编推荐还是使用RLock锁吧。 

5.线程池(python3以后才会有)

这时跟大家说一个概念,不是线程开的越多,效率越高速度越快,如果开的太多反而会降低速率,所以在用线程开发时要有节制,这就引出了我们线程池的概念

我们先建立一个线程池,并使用

首先我们要导入线程池的函数

from concurrent.futures import ThreadPoolExecutor

建立我们的线程池

# 定义一个线程池,最大线程数为4
pool = ThreadPoolExecutor(4)

然后使用接着上代码

pool = ThreadPoolExecutor(4)
# 将任务放到线程池里帮我们执行
# 数字多少一次就可用几个线程
# 若任务大于数字会等前面线程结束后,还给线程池的时候再给别的调用
# 若有参数需要传入
pool.submit(函数名, 参数1, 参数2, .......)

这就是使用规则和原理,对于线程池就介绍到这里

对此:

我们对多线程的了解就到这里

结语:

由于篇幅过长,多进程和协程将在后续篇章内更新,希望多提宝贵意见,多互相交流,进步,我们下期见

  • 7
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值