Python | Python学习之多线程详解

简介: Python | Python学习之多线程详解

多进程详解

在Python中如何创建多线程?

  1. 通过Thread创建多线程
  2. 通过Thread子类创建多线程

python的threading模块是对thread做了一些包装的,可以更加方便的被使用,线程的方法和进程的基本相似,这里就不多赘述,下面举几个栗子:

#例一线程的基本用法
#coding=utf-8
import threading
import time
def xianyu():
    print("咸鱼普拉思")
    time.sleep(1)
if __name__ == "__main__":
    for i in range(5):
        t = threading.Thread(target=xianyu)
        t.start() #启动线程,即让线程开始执行
输出:
咸鱼普拉思
咸鱼普拉思
咸鱼普拉思
咸鱼普拉思
咸鱼普拉思
[Finished in 1.1s]
#例二使用Threading子类创建多线程
#coding=utf-8
import threading
import time
class MyThread(threading.Thread):
    def run(self):
        for i in range(3):
            time.sleep(1)
            msg = "I'm "+self.name+' @ '+str(i)
            print(msg)
def test():
    for i in range(5):
        t = MyThread()
        t.start()
if __name__ == '__main__':
    test()
输出:
I'm Thread-1 @ 0
I'm Thread-2 @ 0
I'm Thread-3 @ 0
I'm Thread-4 @ 0
I'm Thread-5 @ 0
I'm Thread-1 @ 1
I'm Thread-2 @ 1
I'm Thread-4 @ 1
I'm Thread-3 @ 1
I'm Thread-5 @ 1
I'm Thread-1 @ 2
I'm Thread-2 @ 2
I'm Thread-5 @ 2
I'm Thread-4 @ 2
I'm Thread-3 @ 2
[Finished in 3.2s]

多线程和多进程的执行有什么区别?

  1. 多进程是多份程序同时执行
  2. 多线程是在一份程序下多个执行指针同时执行
  3. 多线程并不需要线程间通信,线程间共享全局变量,进程间不共享全局变量
  4. 进程是系统进行资源分配和调度的一个独立单位,线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。
  5. 线程的划分尺度小于进程(资源比进程少),使得多线程程序的并发性高。
  6. 进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率
  7. 且线程不能够独立执行,必须依存在进程中
  8. 线程执行开销小,但不利于资源的管理和保护,而进程正相反

线程的几种状态

线程在执行过程中,如果中途执行sleep语句时,线程会进入到阻塞状态,当sleep结束之后,线程进入就绪状态,等待调度而线程调度将自行选择一个线程执行 。

具体线程状态变换参照下图:

线程状态变化图

线程之间共享全局变量

举个栗子:

from threading import Thread
import time
num = 100
def work1():
    global num
    for i in range(3):
        num += 1
    print("----in work1, num is %d---"%num)
def work2():
    global num
    print("----in work2, num is %d---"%num)
print("---线程创建之前g_num is %d---"%num)
t1 = Thread(target=work1)
t1.start()
#延时一会,保证t1线程中的事情做完
time.sleep(1)
t2 = Thread(target=work2)
t2.start()
输出:
---线程创建之前g_num is 100---
----in work1, num is 103---
----in work2, num is 103---
[Finished in 1.1s]

总结:

  1. 在一个进程内的所有线程共享全局变量,能够在不适用其他方式的前提下完成多线程之间的数据共享(这点要比多进程要好)
  2. 缺点就是,线程是对全局变量随意遂改可能造成多线程之间对全局变量的混乱(即线程非安全)

什么是线程不安全?

举个栗子:

from threading import Thread
import time
num = 0
def test1():
    global num
    for i in range(1000000):
        num += 1
    print("---test1---num=%d"%g_numnum
def test2():
    global num
    for i in range(1000000):
        num += 1
    print("---test2---num=%d"%num)
p1 = Thread(target=test1)
p1.start()
# time.sleep(3)
p2 = Thread(target=test2)
p2.start()
print("---num=%d---"%num)
输出:
当time.sleep(3),没有取消屏蔽时
---num=235159---
---test1---num=1172632
---test2---num=1334237
[Finished in 0.3s]
当time.sleep(3),取消屏蔽时
---test1---num=1000000
---num=1014670---
---test2---num=2000000
[Finished in 3.3s]

上面举的栗子就是线程不安全的现象,具体可以解释为,线程1对数据num进行自增的时候,获取的值是num=0,此时系统把线程1调度为”sleeping”状态 ,而线程2在做同样操作时获取的num值还是为0,同时做自增1的操作,这时在线程2中num的值为1,此时系统把线程2调度为”sleeping”状态,线程1再做自增操作时,num还是刚刚获取到的0,长此往复下去,最终的结果就不是我们所预期的了。

没有控制多个线程对同一资源的访问,对数据造成破坏,使得线程运行的结果不可预期 ,这种现象就是线程不安全。

如何避免线程不安全的现象发生?

当多个线程几乎同时修改某一个共享数据的时候,需要进行同步控制,线程同步能够保证多个线程安全访问竞争资源,最简单的同步机制是引入互斥锁。互斥锁为资源引入一个状态:锁定/非锁定。

某个线程要更改共享数据时,先将其锁定,此时资源的状态为“锁定”,其他线程不能更改;直到该线程释放资源,将资源的状态变成“非锁定”,其他的线程才能再次锁定该资源。互斥锁保证了每次只有一个线程进行写入操作,从而保证了多线程情况下数据的正确性。

举个栗子:

from threading import Thread, Lock
import time
num = 0
def test1():
    global num
    for i in range(1000000):
        #True表示堵塞 即如果这个锁在上锁之前已经被上锁了,那么这个线程会在这里一直等待到解锁为止 
        #False表示非堵塞,即不管本次调用能够成功上锁,都不会卡在这,而是继续执行下面的代码
        mutexFlag = mutex.acquire(True) 
        if mutexFlag:
            num += 1
            mutex.release()
    print("---test1---num=%d"%num)
def test2():
    global num
    for i in range(1000000):
        mutexFlag = mutex.acquire(True) #True表示堵塞
        if mutexFlag:
            num += 1
            mutex.release()
    print("---test2---num=%d"%num)
#创建一个互斥锁
#这个所默认是未上锁的状态
mutex = Lock()
p1 = Thread(target=test1)
p1.start()
p2 = Thread(target=test2)
p2.start()
print("---num=%d---"%num)
输出:
---num=61866---
---test1---num=1861180
---test2---num=2000000

当一个线程调用锁的acquire()方法获得锁时,锁就进入“locked”状态。每次只有一个线程可以获得锁。如果此时另一个线程试图获得这个锁,该线程就会变为“blocked”状态,称为“阻塞”,直到拥有锁的线程调用锁的release()方法释放锁之后,锁进入“unlocked”状态。线程调度程序从处于同步阻塞状态的线程中选择一个来获得锁,并使得该线程进入运行状态。

加锁确保了某段关键代码只能由一个线程从头到尾完整地执行,但是阻止了多线程并发执行,包含锁的某段代码实际上只能以单线程模式执行,效率就大大地下降了,由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对方持有的锁时,可能会造成死锁

什么是死锁?

在线程间共享多个资源的时候,如果两个线程分别占有一部分资源并且同时等待对方的资源,就会造成死锁。

举个栗子:

#coding=utf-8
import threading
import time
class MyThread1(threading.Thread):
    def run(self):
        if mutexA.acquire():
            print(self.name+'----do1---up----')
            time.sleep(1)
            if mutexB.acquire():
                print(self.name+'----do1---down----')
                mutexB.release()
            mutexA.release()
class MyThread2(threading.Thread):
    def run(self):
        if mutexB.acquire():
            print(self.name+'----do2---up----')
            time.sleep(1)
            if mutexA.acquire():
                print(self.name+'----do2---down----')
                mutexA.release()
            mutexB.release()
mutexA = threading.Lock()
mutexB = threading.Lock()
if __name__ == '__main__':
    t1 = MyThread1()
    t2 = MyThread2()
    t1.start()
    t2.start()

生产者与消费者模型

我们可以通过生产者和消费者模型来解决线程的同步,和线程安全。

Python的Queue模块中提供了同步的、线程安全的队列类,包括FIFO(先入先出)队列Queue,LIFO(后入先出)队列LifoQueue,和优先级队列PriorityQueue。这些队列都实现了锁原语(可以理解为原子操作,即要么不做,要么就做完),能够在多线程中直接使用,可以使用队列来实现线程间的同步。

举个栗子:

#encoding=utf-8
import threading
import time
#python2中
# from Queue import Queue
#python3中
from queue import Queue
class Producer(threading.Thread):
    def run(self):
        global queue
        count = 0
        while True:
            if queue.qsize() < 1000:
                for i in range(100):
                    count = count +1
                    msg = '生成产品'+str(count)
                    queue.put(msg)
                    print(msg)
            time.sleep(0.5)
class Consumer(threading.Thread):
    def run(self):
        global queue
        while True:
            if queue.qsize() > 100:
                for i in range(3):
                    msg = self.name + '消费了 '+queue.get()
                    print(msg)
            time.sleep(1)
if __name__ == '__main__':
    queue = Queue()
    for i in range(500):
        queue.put('初始产品'+str(i))
    for i in range(2):
        p = Producer()
        p.start(),
    for i in range(5):
        c = Consumer()
        c.start()

什么是生产者与消费者模型?

生产者消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给生产者和消费者解耦的。纵观大多数设计模式,都会找一个第三者出来进行解耦。

想使用全局变量有不想加锁怎么办?

在多线程环境下,每个线程都有自己的数据。一个线程使用自己的局部变量比使用全局变量好,因为局部变量只有线程自己能看见,不会影响其他线程,而全局变量的修改必须加锁。

举个栗子:

import threading
# 创建全局ThreadLocal对象:
local_school = threading.local()
def process_student():
    # 获取当前线程关联的student:
    std = local_school.student
    print('Hello, %s (in %s)' % (std, threading.current_thread().name))
def process_thread(name):
    # 绑定ThreadLocal的student:
    local_school.student = name
    process_student()
t1 = threading.Thread(target= process_thread, args=('咸鱼',), name='Thread-A')
t2 = threading.Thread(target= process_thread, args=('普拉思',), name='Thread-B')
t1.start()
t2.start()
t1.join()
t2.join()
输出:
Hello, 咸鱼 (in Thread-A)
Hello, 普拉思 (in Thread-B)

全局变量local_school就是一个ThreadLocal对象,每个Thread对它都可以读写student属性,但互不影响。你可以把local_school看成全局变量,但每个属性如local_school.student都是线程的局部变量,可以任意读写而互不干扰,也不用管理锁的问题,ThreadLocal内部会处理。

可以理解为全局变量local_school是一个dict,不但可以用local_school.student,还可以绑定其他变量,如local_school.teacher等等。

ThreadLocal最常用的地方就是为每个线程绑定一个数据库连接,HTTP请求,用户身份信息等,这样一个线程的所有调用到的处理函数都可以非常方便地访问这些资源。

一个ThreadLocal变量虽然是全局变量,但每个线程都只能读写自己线程的独立副本,互不干扰。ThreadLocal解决了参数在一个线程中各个函数之间互相传递的问题

同步调用和异步调用?

同步调用就是你喊你朋友吃饭,你朋友在忙,你就一直在那等,等你朋友忙完了 ,你们一起去。

异步调用就是你喊你朋友吃饭,你朋友说知道了,待会忙完去找你 ,你就去做别的了。

举个栗子:

from multiprocessing import Pool
import time
import os
def test():
    print("---进程池中的进程---pid=%d,ppid=%d--"%(os.getpid(),os.getppid()))
    for i in range(3):
        print("----%d---"%i)
        time.sleep(1)
    return "hahah"
def test2(args):
    print("---callback func--pid=%d"%os.getpid())
    print("---callback func--args=%s"%args)
pool = Pool(3)
pool.apply_async(func=test,callback=test2)
time.sleep(5)
print("----主进程-pid=%d----"%os.getpid())
输出:
---进程池中的进程---pid=9401,ppid=9400--
----0---
----1---
----2---
---callback func--pid=9400
---callback func--args=hahah
----主进程-pid=9400----

注意:这里的callback是由主进程执行的,当子进程死亡,主进程回调函数。

什么是GIL锁?

Python全局解释锁(GIL)简单来说就是一个互斥体(或者说锁),这样的机制只允许一个线程来控制Python解释器。这就意味着在任何一个时间点只有一个线程处于执行状态。

所以在python中多线程是假的,因为在执行过程中CPU中只有一个线程在执行。

当你使用多进程时,你的效率是高于多线程的。

Python GIL经常被认为是一个神秘而困难的话题,但是请记住作为一名Python支持者,只有当您正在编写C扩展或者您的程序中有计算密集型的多线程任务时才会被GIL影响。

写在后面

这是咸鱼的第三篇python学习笔记,上次的推文给大家送了实战的干货,接下来我会再整理下云盘中的资源,争取给大家再送波补贴。

相关文章
|
24天前
|
监控 Java 调度
【Java学习】多线程&JUC万字超详解
本文详细介绍了多线程的概念和三种实现方式,还有一些常见的成员方法,CPU的调动方式,多线程的生命周期,还有线程安全问题,锁和死锁的概念,以及等待唤醒机制,阻塞队列,多线程的六种状态,线程池等
101 6
【Java学习】多线程&JUC万字超详解
|
3天前
|
负载均衡 Java 调度
探索Python的并发编程:线程与进程的比较与应用
本文旨在深入探讨Python中的并发编程,重点比较线程与进程的异同、适用场景及实现方法。通过分析GIL对线程并发的影响,以及进程间通信的成本,我们将揭示何时选择线程或进程更为合理。同时,文章将提供实用的代码示例,帮助读者更好地理解并运用这些概念,以提升多任务处理的效率和性能。
|
1月前
|
数据采集 存储 安全
如何确保Python Queue的线程和进程安全性:使用锁的技巧
本文探讨了在Python爬虫技术中使用锁来保障Queue(队列)的线程和进程安全性。通过分析`queue.Queue`及`multiprocessing.Queue`的基本线程与进程安全特性,文章指出在特定场景下使用锁的重要性。文中还提供了一个综合示例,该示例利用亿牛云爬虫代理服务、多线程技术和锁机制,实现了高效且安全的网页数据采集流程。示例涵盖了代理IP、User-Agent和Cookie的设置,以及如何使用BeautifulSoup解析HTML内容并将其保存为文档。通过这种方式,不仅提高了数据采集效率,还有效避免了并发环境下的数据竞争问题。
如何确保Python Queue的线程和进程安全性:使用锁的技巧
|
19天前
|
API Python
探索Python中的多线程编程
探索Python中的多线程编程
37 5
|
2天前
|
存储 算法 Java
关于python3的一些理解(装饰器、垃圾回收、进程线程协程、全局解释器锁等)
该文章深入探讨了Python3中的多个重要概念,包括装饰器的工作原理、垃圾回收机制、进程与线程的区别及全局解释器锁(GIL)的影响等,并提供了详细的解释与示例代码。
10 0
|
2天前
|
安全 Java 调度
python3多线程实战(python3经典编程案例)
该文章提供了Python3中多线程的应用实例,展示了如何利用Python的threading模块来创建和管理线程,以实现并发执行任务。
8 0
|
29天前
|
调度 Python
Python 中如何实现多线程?
【8月更文挑战第29天】
46 6
|
6天前
|
并行计算 API 调度
探索Python中的并发编程:线程与进程的对比分析
【9月更文挑战第21天】本文深入探讨了Python中并发编程的核心概念,通过直观的代码示例和清晰的逻辑推理,引导读者理解线程与进程在解决并发问题时的不同应用场景。我们将从基础理论出发,逐步过渡到实际案例分析,旨在揭示Python并发模型的内在机制,并比较它们在执行效率、资源占用和适用场景方面的差异。文章不仅适合初学者构建并发编程的基础认识,同时也为有经验的开发者提供深度思考的视角。
|
1月前
|
API C语言 C++
C调用Python之多线程与traceback打印
C调用Python之多线程与traceback打印
27 2
|
1月前
|
程序员 测试技术 开发工具
豆瓣评分7.9!世界级讲师耗时5年整理出的Python学习手册!
Python是一门流行的开源编程语言,广泛用于各个领域的独立程序与脚本化应用中。它不仅免费、可移植、功能强大,同时相对简单,而且使用起来充满乐趣。从软件业界的任意一角到来的程序员,都会发现Python着眼于开发者的生产效率以及软件质量,因此无论你的项目是大还是小,选择Python都将带来战略性的优势。 今天给小伙伴们分享的这份手册讲述了完整的Python语言,力争满足“语言”和“原理”两个方面的需求,并拥有足够的深度以便实用。废话不多说,下面展示给大家。