简介

一个应用程序由多个进程组成,一个进程有多个线程,一个线程则是操作系统调度的最小单位,当应用程序运行时,操作系统根据优先级和时间片调度线程(决定此时此刻执行哪个线程)。

python的线程

python存在多个解释器,cpython、jpython等,但目前主流常用的则是cpython,其存在GIL锁,从而导致无论你启多少个线程,你有多少个cpu, Python在执行的时候会淡定的在同一时刻只允许一个线程运行,因此适用于io密集型程序,由于读写io需要时间且python线程只能在一个cpu运行,可以在io的时间中来回切换线程,进一步提高效率

python为什么要加锁

进行定期自动内存回收

加入GIL主要的原因是为了降低程序的开发的复杂度,比如现在的你写python不需要关心内存回收的问题,因为Python解释器帮你自动定期进行内存回收,你可以理解为python解释器里有一个独立的线程,每过一段时间它起wake up做一次全局轮询看看哪些内存数据是可以被清空的,此时你自己的程序 里的线程和 py解释器自己的线程是并发运行的,假设你的线程删除了一个变量,py解释器的垃圾回收线程在清空这个变量的过程中的clearing时刻,可能一个其它线程正好又重新给这个还没来及得清空的内存空间赋值了,结果就有可能新赋值的数据被删除了,为了解决类似的问题,python解释器简单粗暴的加了锁,即当一个线程运行时,其它人都不能动,这样就解决了上述的问题, 这可以说是Python早期版本的遗留问题。

python线程的常用创建方式

1.类方式创建,需要继承threading.Thread类,必须要在构造函数中初始化thread的init方法,线程方法需要重写run方法

import threading, time, os

class MyThread(threading.Thread):

    def __init__(self, name):
threading.Thread.__init__(self, name=name)
self.name = name def run(self):
time.sleep(10)
print(f"MyThread run:{self.name}") print(f'pid:{os.getpid()}') t = MyThread(name='test')
t2 = MyThread(name='test2')
t.run()
t2.run()

2.函数式创建线程,需要注意的是在函数传参时如果只有一个值,需要在后面添加逗号

import threading, time

def run(name):
time.sleep(10)
print(f"Thread run:{name}") t1 = threading.Thread(target=run, args=('test1',))
t2 = threading.Thread(target=run, args=('test2',))
t1.start()
t2.start()

python 线程间通信

1.全局变量,一个线程写入,一个线程读取

from typing import List

MSG_LIST:List = []

import threading

def send(msg):
global MSG_LIST
MSG_LIST.append(msg) def get():
global MSG_LIST
for msg in MSG_LIST:
print(f'recvmsg:{msg}')
MSG_LIST.remove(msg) t_list = []
for i in range(5):
t = threading.Thread(target=send, args=('msg'+str(i),))
t2 = threading.Thread(target=get)
t.start()
t2.start()
t.join()
t2.join()

2.队列

与全局变量类似,不过方便了编写

import threading, queue

q = queue.Queue()

def send(msg):
global q
q.put(msg) def get():
global q
print(f'msg:{q.get_nowait()}') t_list = []
for i in range(5):
t = threading.Thread(target=send, args=('msg'+str(i),))
t2 = threading.Thread(target=get)
t.start()
t2.start()
t.join()
t2.join()

python线程数据不一致问题

在多线程操作数据时,会导致脏数据、数据不一致的情况,如下所示:

count = 0

import threading, time

def run(name):
global count
time.sleep(5)
count += 1
print(f'name:{name}, count:{count}') t_list = []
for i in range(5):
t:threading.Thread = threading.Thread(target=run, args=('thread' + str(i+1), ))
t_list.append(t)
t.start() for t in t_list:
t.join()



为了解决上述问题,需要采取加锁的方式,方式如下:

1.互斥锁(Lock)

使用threading中的Lock模块,只需要将函数或方法中的操作逻辑加上对应的锁即可,此方法可以解决一般的数据不一致问题,但是如果锁发生嵌套,则可能会出现死锁问题

count = 0

import threading, time

lock = threading.Lock()
def run(name):
global count
lock.acquire()
time.sleep(5)
count += 1
print(f'name:{name}, count:{count}')
lock.release() t_list = []
for i in range(5):
t:threading.Thread = threading.Thread(target=run, args=('thread' + str(i+1), ))
t_list.append(t)
t.start() for t in t_list:
t.join()

2.如上Lock所写,Lock嵌套可能会出现死锁的问题,如下由于Lock使用了嵌套,则会导致线程发生阻塞,导致死锁问题

count = 0

import threading, time

lock = threading.Lock()
def run(name):
global count
lock.acquire()
time.sleep(5)
count += 1
lock.acquire()
count -= 1
lock.release()
print(f'name:{name}, count:{count}')
lock.release() t_list = []
for i in range(5):
t:threading.Thread = threading.Thread(target=run, args=('thread' + str(i+1), ))
t_list.append(t)
t.start() for t in t_list:
t.join()

因此需要使用嵌套锁RLock

count = 0

import threading, time

lock = threading.RLock()
def run(name):
global count
lock.acquire()
time.sleep(5)
count += 1
lock.acquire()
count += 1
lock.release()
print(f'name:{name}, count:{count}')
lock.release() t_list = []
for i in range(5):
t:threading.Thread = threading.Thread(target=run, args=('thread' + str(i+1), ))
t_list.append(t)
t.start() for t in t_list:
t.join()

3.信号量(Semaphore)

互斥锁 同时只允许一个线程更改数据,而Semaphore是同时允许一定数量的线程更改数据

count = 0

import threading, time

lock = threading.Semaphore(4)
def run(name):
global count
lock.acquire()
time.sleep(5)
count += 1
print(f'name:{name}, count:{count}')
lock.release() t_list = []
for i in range(5):
t:threading.Thread = threading.Thread(target=run, args=('thread' + str(i+1), ))
t_list.append(t)
t.start() for t in t_list:
t.join()

结果虽然都是一样的,但是在实际运行过程中,则会4个线程‘同时’运行,此处同时起始只是人眼可见的,实际上还是依次运行的,不过运行较快



Semaphore原理:相当于早晚班倒

内部存在一个计数器,acquire时-1 release时+1, 当计数器为0时则阻塞, 当有线程释放时则会继续执行相关信息,不需要全部释放后才可以继续执行

计数器的数量取决于设置的n的数量, 默认是1

4.条件(Condition)

import threading, time

count = 0
con = threading.Condition()
def producer():
global count
con.acquire()
time.sleep(5)
count += 1
con.notify()
con.release() def customer():
global count
con.acquire()
con.wait(timeout=10)
print(f'count:{count}')
con.release() c = threading.Thread(target=customer)
c.start()
p = threading.Thread(target=producer)
p.start()

Condition原理: 类似与生产消费者模式

python 条件变量Condition也需要关联互斥锁,同时Condition自身提供了wait/notify/notifyAll方法,用于阻塞/通知其他并行线程,可以访问共享资源了。

Condition提供了一种多线程通信机制,假如线程1需要数据,那么线程1就阻塞等待,这时线程2就去制造数据,线程2制造好数据后,通知线程1可以去取数据了,然后线程1去获取数据。

内部使用互斥锁,双层锁结构

5.event(事件):适用于两个线程间通信

import threading, time

count = 0

event = threading.Event()

def run1():
global count
count += 1
print(f'event:flag:before:{event.isSet()}')
time.sleep(10)
event.set()
print(f'event:flag:after:{event.isSet()}') def run2():
global count
event.wait()
print(f'count:{count}') t1 = threading.Thread(target=run2)
t2 = threading.Thread(target=run1)
t1.start()
t2.start()

event原理:

python线程的事件用于主线程控制其他线程的执行,事件主要提供了三个方法 set、wait、clear。

默认 event flag为false

事件处理的机制:全局定义了一个“Flag”,如果“Flag”值为 False,那么当程序执行 event.wait 方法时就会阻塞,如果“Flag”值为True,那么event.wait 方法时便不再阻塞。

clear:将“Flag”设置为False

set:将“Flag”设置为True

线程池

使用submit创建单个线程

from concurrent.futures import ThreadPoolExecutor
import time def run(n, m):
print(f'{n} thread is starting')
time.sleep(3)
print(f'{n} thread is finish')
return n, m t = ThreadPoolExecutor(4)
ret_list = []
for i in range(5):
# 创建单个线程
ret = t.submit(run, n=i, m=i+1)
ret_list.append(ret) for ret in ret_list:
print(ret.result())

使用map批量创建线程

需要使用args中间传递值,结果会在最后返回

from concurrent.futures import ThreadPoolExecutor
import time def run(n, m):
print(f'{n} thread is starting')
time.sleep(3)
print(f'{n} thread is finish')
return n, m def run_map(args):
print(args)
print(*args)
return run(*args) t = ThreadPoolExecutor(4)
ret = t.map(run_map, [(1,2)])
print(list(ret))

死锁问题

1.什么是死锁?

死锁是由于两个或以上的线程互相持有对方需要的资源,且都不释放占有的资源,导致这些线程处于等待状态,程序无法执行。

2.产生死锁的四个必要条件

1.互斥性:线程对资源的占有是排他性的,一个资源只能被一个线程占有,直到释放。

2.请求和保持条件:一个线程对请求被占有资源发生阻塞时,对已经获得的资源不释放。

3.不剥夺:一个线程在释放资源之前,其他的线程无法剥夺占用。

4.循环等待:发生死锁时,线程进入死循环,永久阻塞。

3.产生死锁的原因

在多线程的场景,比如线程A持有独占锁资源a,并尝试去获取独占锁资源b同时,线程B持有独占锁资源b,并尝试去获取独占锁资源a。

这样线程A和线程B相互持有对方需要的锁,从而发生阻塞,最终变为死锁。

造成死锁的原因可以概括成三句话:

1.不同线程同时占用自己锁资源

2.这些线程都需要对方的锁资源

3.这些线程都不放弃自己拥有的资源

注意事项

1.建议创建线程时对每一个线程起一个名称,方便后期出现问题后定位问题

python 线程理解的更多相关文章

  1. PYTHON线程知识再研习A

    前段时间看完LINUX的线程,同步,信息号之类的知识之后,再在理解PYTHON线程感觉又不一样了. 作一些测试吧. thread:模块提供了基本的线程和锁的支持 threading:提供了更高级别,功 ...

  2. Python 线程(threading) 进程(multiprocessing)

    *:first-child { margin-top: 0 !important; } body>*:last-child { margin-bottom: 0 !important; } /* ...

  3. python线程同步原语--源码阅读

    前面两篇文章,写了python线程同步原语的基本应用.下面这篇文章主要是通过阅读源码来了解这几个类的内部原理和是怎么协同一起工作来实现python多线程的. 相关文章链接:python同步原语--线程 ...

  4. python——线程相关

    使用python的threading中的Thread 下面是两种基本的实现线程的方式: 第一种方式———— #coding=utf-8 """ thread的第一种声明及 ...

  5. python线程入门

    目录 python线程入门 线程与进程 线程 总结 参考 python线程入门 正常情况下,我们在启动一个程序的时候.这个程序会先启动一个进程,启动之后这个进程会启动起来一个线程.这个线程再去处理事务 ...

  6. [ python ] 线程的操作

    目录 (见右侧目录栏导航) - 1. 前言    - 1.1 进程    - 1.2 有了进程为什么要有线程    - 1.3 线程的出现    - 1.4 进程和线程的关系    - 1.5 线程的 ...

  7. python多线程理解

    在发送网络请求的过程中,单个请求的速度总是有着很大的限制,而任务往往需要以更快的速度去执行,这时多线程就是一个很好地选择.python已经给我们封装好了多线程库thread和threading. th ...

  8. python线程条件变量Condition(31)

    对于线程与线程之间的交互我们在前面的文章已经介绍了 python 互斥锁Lock / python事件Event , 今天继续介绍一种线程交互方式 – 线程条件变量Condition. 一.线程条件变 ...

  9. python 线程创建和传参(28)

    在以前的文章中虽然我们没有介绍过线程这个概念,但是实际上前面所有代码都是线程,只不过是单线程,代码由上而下依次执行或者进入main函数执行,这样的单线程也称为主线程. 有了单线程的话,什么又是多线程? ...

随机推荐

  1. Python3获取5000个元素的单字符表

    技术背景 此前考虑过一个问题,有没有办法获取到python里面所有定义好的单字符的表,比如我们获取5000个不一样的单字符,但是常用的chr(number)的方法里面包含了太多的非字母条目,比如缩进换 ...

  2. sklearn机器学习实战-简单线性回归

    记录下学习使用sklearn,将使用sklearn实现机器学习大部分内容 基于scikit-learn机器学习(第2版)这本书,和scikit-learn中文社区 简单线性回归 首先,最简单的线性回归 ...

  3. Numpy的一些操作

    1.什么是Numpy 简单来说: Numpy(Numerical Python)是一个开源的Python科学计算库,用于快速处理任意维度的数组. Numpy支持常见的数组和矩阵操作.对于同样的数值计算 ...

  4. 题解 P3831 [SHOI2012]回家的路

    什么叫分层图最短路,我不会/kk 感觉自己做法和其他题解不大一样所以过来发篇题解了. 未刻意卡常拿下最优解 题目大意 就是说给你一个 \(n \times n\) 的网格图和 \(m\) 个可换乘点, ...

  5. 简历应该怎么写,HR看一篇简历仅需要5秒吗,简历模板大全

    哈喽!大家好,我是小奇,一位热爱分享的程序员 小奇打算以轻松幽默的对话方式来分享一些技术,如果你觉得通过小奇的文章学到了东西,那就给小奇一个赞吧 文章持续更新 一.前言 最近有很多小伙伴问奇哥,说奇哥 ...

  6. Python 空间名称与闭包函数

    空间名称与闭包函数 名称空间 名称空间 namespaces:存放名字的地方,是对栈区的划分 名称空间在栈区中分为三种,详细的划分不同的空间,不同空间可以存放相同名字的名字 内置名称空间 存放的名字: ...

  7. npm run serve修改为npm run dev

    找到package.json文件,打开文件找到  "serve": "vue-cli-service serve"  这一行,把前面的 serve 修改 dev ...

  8. OAuth2学习中的一些高频问题的QA

    关于OAuth2相信很多初学者都有一些疑问,胖哥将这些疑问一一收集了起来做成了QA,或许能帮助学习者. OAuth2相关的QA Q:OAuth2 的一些常用场景? A: OAuth2主要用于API授权 ...

  9. 修改SQL Server用户的密码-使用SSMS

    更新日志 2022年6月13日 发布文章. 2022年5月21日 开始文章. 打开软件Microsoft SQL Server Management Studio(简写:SSMS). 登录连接具体的数 ...

  10. 一文搞懂Kafka的基本原理及使用

    Kafka的基本原理及使用 一.基本概念及原理 1.Kafka特点 Kafka 是一个分布式的流式平台,流式平台包括以下三个特点: 发布和订阅消息(流),类似于一个消息队列或企业消息系统 持久化收到的 ...