文章目录

  • 一、python多线程
    • 1. GIL
  • 二、threading库使用介绍
    • 1. 创建多线程
    • 2. 线程合并
    • 3. 线程同步与互斥锁Lock
    • 4. 死锁与可重入锁(递归锁)RLock
    • 5. 守护线程
    • 6. 定时器
    • 7. Thread类的其他方法
  • 三、常见问题
    • 1. 我们有了GIL锁为什么还要自己在代码中加锁呢?
    • 2. python因为CPython解释器中GIL的原因,多线程还能用吗?
    • 3. 多线程中加锁和使用join的区别

一、python多线程

前言:为什么有人说 Python 的多线程是鸡肋,不是真正意义上的多线程?

关于多线程,多进程的详细介绍 可阅读此篇blog

这里自己用白话简单概括一下(非准确):

  • 一个要执行的程序(应用)可以看作一个进程
  • 改程序里的一串代码指令(或一个函数)看作是进程里的一个线程。(main函数就是主线程)

1. GIL

首先需要明确的一点是GIL并不是Python的特性,它是在实现Python解析器(CPython)时所引入的一个概念。

其他语言,CPU是多核时 是支持多个线程同时执行。但在Python中,无论是单核还是多核,同时只能由一个线程在执行。

其根源是GIL的存在。GIL的全称是Global Interpreter Lock(全局解释器锁),来源是Python设计之初的考虑,为了数据安全所做的决定。

某个线程想要执行,必须先拿到GIL,我们可以把GIL看作是“通行证”,并且在一个Python进程中,GIL只有一个。拿不到通行证的线程,就不允许进入CPU执行。

而目前Python的解释器有多种,例如:

  • CPython: CPython是用C语言实现的Python解释器。 作为官方实现,它是最广泛使用的Python解释器。

  • PyPy: PyPy是用RPython实现的解释器。RPython是Python的子集, 具有静态类型。这个解释器的特点是即时编译,支持多重后端(C, CLI, JVM)。PyPy旨在提高性能,同时保持最大兼容性(参考CPython的实现)。

  • Jython: Jython是一个将Python代码编译成Java字节码的实现,运行在JVM (Java Virtual Machine) 上。另外,它可以像是用Python模块一样,导入并使用任何Java类。

  • IronPython: IronPython是一个针对 .NET 框架的Python实现。它可以用Python和 .NET framework的库,也能将Python代码暴露给 .NET框架中的其他语言。

GIL只在CPython中才有,而在PyPy和Jython中是没有GIL的

每次释放GIL锁,线程进行锁竞争、切换线程,会消耗资源。这就导致打印线程执行时长,会发现耗时更长的原因。

并且由于GIL锁存在,Python里一个进程永远只能同时执行一个线程(拿到GIL的线程才能执行),这就是为什么在多核CPU上,Python 的多线程效率并不高的根本原因。

二、threading库使用介绍

multiprocess模块(python多进程库)完全模仿了threading模块的接口,二者在使用层面,有很大的相似性。

1. 创建多线程

Python提供两个模块进行多线程的操作,分别是thread和threading,前者是比较低级的模块,用于更底层的操作,一般应用级别的开发不常用。

  • 方法1:直接使用threading.Thread()
import threading# 这个函数名可随便定义
def run(n):print("current task:", n)if __name__ == "__main__":t1 = threading.Thread(target=run, args=("thread 1",))t2 = threading.Thread(target=run, args=("thread 2",))t1.start()t2.start()
from threading import Thread
import time
def sayhi(name):time.sleep(2)print('%s say hello' %name)if __name__ == '__main__':t=Thread(target=sayhi,args=('egon',))t.start()print('主线程')
  • 方法2:继承threading.Thread来自定义线程类,重写run方法
import threadingclass MyThread(threading.Thread):def __init__(self, n):super(MyThread, self).__init__()  # 重构run函数必须要写self.n = ndef run(self):print("current task:", n)if __name__ == "__main__":t1 = MyThread("thread 1")t2 = MyThread("thread 2")t1.start()t2.start()
from threading import Thread
import time
class Sayhi(Thread):def __init__(self,name):super().__init__()self.name=namedef run(self):time.sleep(2)print('%s say hello' % self.name)if __name__ == '__main__':t = Sayhi('egon')t.start()print('主线程')

2. 线程合并

join函数执行顺序是逐个执行每个线程,执行完毕后继续往下执行。主线程结束后,子线程还在运行,join函数使得主线程等到子线程结束时才退出

import threadingdef count(n):while n > 0:print(n)n -= 1if __name__ == "__main__":t1 = threading.Thread(target=count, args=(10",))t2 = threading.Thread(target=count, args=(10,))t1.start()t2.start()# 将 t1 和 t2 加入到主线程中t1.join()t2.join()

3. 线程同步与互斥锁Lock

线程之间数据共享的。当多个线程对某一个共享数据进行操作时,就需要考虑到线程安全问题。threading模块中定义了Lock 类,提供了互斥锁的功能来保证多线程情况下数据的正确性。

用法的基本步骤:

#创建锁
mutex = threading.Lock()
#锁定
mutex.acquire([timeout])
#释放
mutex.release()------模板:--------
R=threading.Lock()
R.acquire()
'''
对公共数据的操作
'''
R.release()

解释:

一把锁有两个状态:locked 和 unlocked 状态。锁刚被创建的时候是处于 unlocked 状态。

  • lock.acquire() 会让锁从 unlocked -> locked。

  • lock.release() 会让锁从 locked -> unlocked。

如果锁已经处于 locked 状态,对它使用 acquire() 的线程会被阻塞,直到另一个线程调用了 release() 使该锁解锁。

release()方法只能在锁locked时调用,并释放锁。否则会抛出RuntimeError错误。

其中,锁定方法acquire可以有一个超时时间的可选参数timeout。如果设定了timeout,则在超时后通过返回值可以判断是否得到了锁,从而可以进行一些其他的处理。具体用法见示例代码:

import threading
import timenum = 0
mutex = threading.Lock()class MyThread(threading.Thread):def run(self):global num time.sleep(1)if mutex.acquire(1):  num = num + 1msg = self.name + ': num value is ' + str(num)print(msg)mutex.release()if __name__ == '__main__':for i in range(5):t = MyThread()t.start()

另一个示例:

import threading
import time
from queue import Queuedef a():global A,locklock.acquire()for i in range(10):A+=1print("a",A)lock.release()def b():global A,locklock.acquire()for i in range(10):A+=10print("b",A)lock.release()if __name__ == '__main__':lock = threading.Lock()A=0t1=threading.Thread(target=a,)t2=threading.Thread(target=b,)t1.start()t2.start()输出结果:
a 1
a 2
a 3
a 4
a 5
a 6
a 7
a 8
a 9
a 10
b 20
b 30
b 40
b 50
b 60
b 70
b 80
b 90
b 100
b 110

4. 死锁与可重入锁(递归锁)RLock

进程也有死锁与可重入锁,使用方法都是一样的,所以放到这里一起说:

所谓死锁: 是指两个或两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程,如下就是死锁。
示例1.

from threading import Lock as Lock
import timemutexA=Lock()
mutexA.acquire()
mutexA.acquire()
print(123)
mutexA.release()
mutexA.release()

示例2. 科学家吃面

import time
from threading import Thread,Lock
noodle_lock = Lock()
fork_lock = Lock()
def eat1(name):noodle_lock.acquire()print('%s 抢到了面条'%name)fork_lock.acquire()print('%s 抢到了叉子'%name)print('%s 吃面'%name)fork_lock.release()noodle_lock.release()def eat2(name):fork_lock.acquire()print('%s 抢到了叉子' % name)time.sleep(1)noodle_lock.acquire()print('%s 抢到了面条' % name)print('%s 吃面' % name)noodle_lock.release()fork_lock.release()for name in ['哪吒','egon','yuan']:t1 = Thread(target=eat1,args=(name,))t2 = Thread(target=eat2,args=(name,))t1.start()t2.start()

解决方法:使用可重入锁

为了满足在同一线程中多次请求同一资源的需求,Python提供了可重入锁(RLock)。

RLock内部维护着一个Lock和一个counter变量,counter记录了acquire
的次数,从而使得资源可以被多次require。直到一个线程所有的acquire都被release,其他的线程才能获得资源。

具体用法如下:

#创建 RLock
mutex = threading.RLock()class MyThread(threading.Thread):def run(self):if mutex.acquire(1):print("thread " + self.name + " get mutex")time.sleep(1)mutex.acquire()mutex.release()mutex.release()

对应上面两个死锁问题,可以按照如下写法解决:
示例1.

from threading import RLock
import time
mutexA=RLock()
mutexA.acquire()
mutexA.acquire()
print(123)
mutexA.release()
mutexA.release()

示例2. 科学家吃面

import time
from threading import Thread,RLock
fork_lock = noodle_lock = RLock()
def eat1(name):noodle_lock.acquire()print('%s 抢到了面条'%name)fork_lock.acquire()print('%s 抢到了叉子'%name)print('%s 吃面'%name)fork_lock.release()noodle_lock.release()def eat2(name):fork_lock.acquire()print('%s 抢到了叉子' % name)time.sleep(1)noodle_lock.acquire()print('%s 抢到了面条' % name)print('%s 吃面' % name)noodle_lock.release()fork_lock.release()for name in ['哪吒','egon','yuan']:t1 = Thread(target=eat1,args=(name,))t2 = Thread(target=eat2,args=(name,))t1.start()t2.start()

5. 守护线程

无论是进程还是线程,都遵循:守护xx会等待主xx运行完毕后被销毁。 需要强调的是:运行完毕并非终止运行

  • #1.对主进程来说,运行完毕指的是主进程代码运行完毕
  • #2.对主线程来说,运行完毕指的是主线程所在的进程内所有非守护线程统统运行完毕,主线程才算运行完毕

#1 主进程在其代码结束后就已经算运行完毕了(守护进程在此时就被回收),然后主进程会一直等非守护的子进程都运行完毕后回收子进程的资源(否则会产生僵尸进程),才会结束,
#2 主线程在其他非守护线程运行完毕后才算运行完毕(守护线程在此时就被回收)。因为主线程的结束意味着进程的结束,进程整体的资源都将被回收,而进程必须保证非守护线程都运行完毕后才能结束。

如果希望主线程执行完毕之后,不管子线程是否执行完毕都随着主线程一起结束。我们可以使用setDaemon(bool)函数,它跟join函数是相反的。

它的作用是设置子线程是否随主线程一起结束,必须在start() 之前调用,默认为False。

看下示例

from threading import Thread
import time
def sayhi(name):time.sleep(2)print('%s say hello' %name)if __name__ == '__main__':t=Thread(target=sayhi,args=('egon',))t.setDaemon(True) #必须在t.start()之前设置t.start()print('主线程')print(t.is_alive())'''主线程True'''
from threading import Thread
import time
def foo():print(123)time.sleep(1)print("end123")def bar():print(456)time.sleep(3)print("end456")t1=Thread(target=foo)
t2=Thread(target=bar)t1.daemon=True
t1.start()
t2.start()
print("main-------")

6. 定时器

如果需要规定函数在多少秒后执行某个操作,需要用到Timer类。具体用法如下:

from threading import Timerdef show():print("Pyhton")# 指定一秒钟之后执行 show 函数
t = Timer(1, hello)
t.start()

7. Thread类的其他方法

Thread实例对象的方法# isAlive(): 返回线程是否活动的。# getName(): 返回线程名。# setName(): 设置线程名。threading模块提供的一些方法:# threading.currentThread(): 返回当前的线程变量。# threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。# threading.activeCount(): 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果。
from threading import Thread
import threading
from multiprocessing import Process
import osdef work():import timetime.sleep(3)print(threading.current_thread().getName())if __name__ == '__main__':#在主进程下开启线程t=Thread(target=work)t.start()print(threading.current_thread().getName())print(threading.current_thread()) #主线程print(threading.enumerate()) #连同主线程在内有两个运行的线程print(threading.active_count())print('主线程/主进程')'''打印结果:MainThread<_MainThread(MainThread, started 140735268892672)>[<_MainThread(MainThread, started 140735268892672)>, <Thread(Thread-1, started 123145307557888)>]主线程/主进程Thread-1'''代码示例

三、常见问题

1. 我们有了GIL锁为什么还要自己在代码中加锁呢?

GIL保护的是Python解释器级别的数据资源,自己代码中的数据资源就需要自己加锁防止竞争。如下图:

2. python因为CPython解释器中GIL的原因,多线程还能用吗?

有了GIL的存在,同一时刻同一进程中只有一个线程被执行,进程可以利用多核,但是开销大,而Python的多线程开销小,但却无法利用多核优势,也就是说Python这语言难堪大用。

其实编程所解决的现实问题大致分为IO密集型和计算密集型。

对于IO密集型的场景,Python的多线程编程完全OK
而对于计算密集型的场景,Python中有很多成熟的模块或框架如Pandas等能够提高计算效率。

3. 多线程中加锁和使用join的区别

#不加锁:并发执行,速度快,数据不安全
from threading import current_thread,Thread,Lock
import os,time
def task():global nprint('%s is running' %current_thread().getName())temp=ntime.sleep(0.5)n=temp-1if __name__ == '__main__':n=100lock=Lock()threads=[]start_time=time.time()for i in range(100):t=Thread(target=task)threads.append(t)t.start()for t in threads:t.join()stop_time=time.time()print('主:%s n:%s' %(stop_time-start_time,n))'''
Thread-1 is running
Thread-2 is running
......
Thread-100 is running
主:0.5216062068939209 n:99
'''#不加锁:未加锁部分并发执行,加锁部分串行执行,速度慢,数据安全
from threading import current_thread,Thread,Lock
import os,time
def task():#未加锁的代码并发运行time.sleep(3)print('%s start to run' %current_thread().getName())global n#加锁的代码串行运行lock.acquire()temp=ntime.sleep(0.5)n=temp-1lock.release()if __name__ == '__main__':n=100lock=Lock()threads=[]start_time=time.time()for i in range(100):t=Thread(target=task)threads.append(t)t.start()for t in threads:t.join()stop_time=time.time()print('主:%s n:%s' %(stop_time-start_time,n))'''
Thread-1 is running
Thread-2 is running
......
Thread-100 is running
主:53.294203758239746 n:0
'''#有的同学可能有疑问:既然加锁会让运行变成串行,那么我在start之后立即使用join,就不用加锁了啊,也是串行的效果啊#没错:在start之后立刻使用jion,肯定会将100个任务的执行变成串行,毫无疑问,最终n的结果也肯定是0,是安全的,但问题是#start后立即join:任务内的所有代码都是串行执行的,而加锁,只是加锁的部分即修改共享数据的部分是串行的
#单从保证数据安全方面,二者都可以实现,但很明显是加锁的效率更高.
from threading import current_thread,Thread,Lock
import os,time
def task():time.sleep(3)print('%s start to run' %current_thread().getName())global ntemp=ntime.sleep(0.5)n=temp-1if __name__ == '__main__':n=100lock=Lock()start_time=time.time()for i in range(100):t=Thread(target=task)t.start()t.join()stop_time=time.time()print('主:%s n:%s' %(stop_time-start_time,n))'''
Thread-1 start to run
Thread-2 start to run
......
Thread-100 start to run
主:350.6937336921692 n:0 #耗时是多么的恐怖
''')

参考链接:
https://blog.csdn.net/zong596568821xp/article/details/99678390
https://www.cnblogs.com/haitaoli/articles/10302508.html

【python第三方库】python多线程编程---threading库相关推荐

  1. python进阶 多线程编程 —— threading和queue库实现多线程编程

    python进阶 多线程编程 -- threading和queue库实现多线程编程) 摘要 多线程实现逻辑封装 模型参数选择实例 摘要 本文主要介绍了利用python的 threading和queue ...

  2. Python实战之多线程编程threading.Thread

    Python实战之多线程编程threading.Thread 在Python中可以使用继承threading.Thread类来实现多线程编程,其中子类可以重写父类的__init__和run方法来实现用 ...

  3. pthread库进行多线程编程 - 组件工厂 - C++博客

    pthread库进行多线程编程 - 组件工厂 - C++博客 11 Threads 1 Introduction 不用介绍了吧- 2 Thread Concepts 1.     Thread由下面部 ...

  4. python 线程锁_python多线程编程(3): 使用互斥锁同步线程

    问题的提出 上一节的例子中,每个线程互相独立,相互之间没有任何关系.现在假设这样一个例子:有一个全局的计数num,每个线程获取这个全局的计数,根据num进行一些处理,然后将num加1.很容易写出这样的 ...

  5. python线程创建对象_Python多线程编程基础:如何创建线程?

    Python标准库threading中的Thread类用来创建和管理线程对象,支持使用两种方法来创建线程: 1)直接使用Thread类实例化一个线程对象并传递一个可调用对象作为参数: 2)继承Thre ...

  6. Python爬虫下一代网络请求库httpx和parsel解析库测评

    Python网络爬虫领域两个最新的比较火的工具莫过于httpx和parsel了.httpx号称下一代的新一代的网络请求库,不仅支持requests库的所有操作,还能发送异步请求,为编写异步爬虫提供了便 ...

  7. 进程、线程及python的多线程编程

    目录 一.进程.线程和并行执行 1.什么是进程.线程 注意 2.什么是并行执行 二.python的多线程编程 threading模块 语法 多线程编程的传参 演示 三.总结 一.进程.线程和并行执行 ...

  8. 多线程编程指南 part 2

    多线程编程指南 Sun Microsystems, Inc. 4150 Network Circle Santa Clara, CA95054 U.S.A. 文件号码819–7051–10 2006 ...

  9. 多线程编程之两阶段终止模式

    2019独角兽企业重金招聘Python工程师标准>>> 对于多线程编程,如何优雅的终止子线程,始终是一个值得考究的问题.如果直接终止线程,可能会产生三个问题: 子线程当前执行的任务可 ...

最新文章

  1. MongoDB是什么以及它如何满足您的应用需求
  2. jQuery -- touch事件之滑动判断(左右上下方向)
  3. 用shp制作geoJson格式地图数据(shp convert to geoJson)
  4. Java线程之Synchronized用法
  5. java使用Executor(执行器)管理线程
  6. 什么是存储过程,存储过程的作用及好处
  7. numpy.linalg——线性代数运算
  8. hdu 2553(N皇后)
  9. vue json对象转数组_vue面试题汇总
  10. Excel表格中如何批量删除工作表
  11. qq聊天/msn聊天/发送邮件
  12. Sublime Merge中文版
  13. 等保安全计算环境之Windows(身份鉴别+访问控制)(二级)
  14. 02_Python算法+数据结构笔记-冒泡排序-选择排序-插入排序-快排-二叉树
  15. 计算机网络术语中rt是什么意思?今天就来给你解答
  16. X86_64汇编与IA32比较
  17. 【网页实战】html+css+js超简易书籍购买网站实现(FindMyBook)
  18. 漂亮实用的万年历带中文节日农历
  19. 寻路者华为云:在产业AI迷宫里走直线
  20. 服务器源码用哪种打包软件,市面上同城外卖配送系统有哪几种?看完你就知道啦...

热门文章

  1. SQL注释怎么写以及SQL分类
  2. 【Java位运算】Java中整数取反(位操作)
  3. 电脑连接电视以后只有桌面没有图标?快捷键一步搞定
  4. java drawstring 乱码_Linux环境下BufferedImage Graphics drawString 中文乱码解决方法
  5. git小乌龟pull报错 You asked to pull from the remote ‘origin‘...
  6. Python实现关联规则推荐
  7. OSI七层模型简单理解
  8. 解除移动/联通网络锁定的方法
  9. java发送邮件-模板
  10. 最新虎年送祝福小程序源码下载+UI很不错