【Python】从入门到上头— 多线程(9)

news2024/12/27 12:01:14

进程和线程的区别

详见【Java基础】多线程从入门到掌握第一节(一.多线程基础)

在这里插入图片描述

一. _thread模块和threading模块

Python的标准库提供了两个模块:_thread和threading,_thread是低级模块,threading是高级模块,对_thread进行了封装。绝大多数情况下,我们只需要使用threading这个高级模块。

  1. _thread 提供了低级别的、原始的线程以及一个简单的锁,它相比于 threading 模块的功能还是比较有限的。

    • threading 模块除了_thread 模块中的所有方法外,还提供的其他方法:

      • threading.currentThread(): 返回当前的线程变量。
      • threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。
      • threading.activeCount(): 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果。
  2. 除了使用方法外,线程模块同样提供了Thread类来处理线程,Thread类提供了以下方法:

    • run(): 用以表示具体执行的方法。
    • start(): 启动线程
    • join([time]): 等待至线程中止。这阻塞调用线程直至线程的join() 方法被调用中止-正常退出或者抛出未处理的异常-或者是可选的超时发生。
    • isAlive(): 返回线程是否运行的。
    • getName(): 返回线程名。
    • setName(): 设置线程名。

二.如何创建线程

Python中使用线程有两种方式:函数或者用来包装线程对象

1.函数式创建线程

  • _thread 模块函数式生成线程:调用中的start_new_thread()函数来产生新线程。

    如下::

    import _thread,threading
    import time
    
    
    # 为线程定义一个函数
    # 线程名,线程休眠时间s
    def loop(num):
        print('线程 %s 运行中...' % threading.current_thread().name)
        n = 0
        while n < 5:
            n = n + 1
            print('线程 %s >>> %s' % (threading.current_thread().name, n))
            time.sleep(1)
        print('线程 %s 运行结束.' % threading.current_thread().name)
    
    
    print('主线程 %s 运行中...' % threading.current_thread().name)
    # 创建两个线程
    try:
        _thread.start_new_thread(loop, (5,))
    except:
        print("Error: 无法启动线程")
    
    # 主线程一直保持运行
    while 1:
        pass
    print('主线程 %s 运行结束.' % threading.current_thread().name)
    

    在这里插入图片描述

  • threading 模块函数式生成线程:调用中的Thread函数来产生新线程

    import time, threading
    
    # 新线程执行的函数:
    def loop():
        print('线程 %s 运行中...' % threading.current_thread().name)
        n = 0
        while n < 5:
            n = n + 1
            print('线程 %s >>> %s' % (threading.current_thread().name, n))
            time.sleep(1)
        print('线程 %s 运行结束.' % threading.current_thread().name)
    
    
    # 主线程执行
    print('主线程 %s 运行中...' % threading.current_thread().name)
    #绑定要执行的函数以及设置函数名称
    t = threading.Thread(target=loop, name='LoopThread')
    t.start()
    t.join()
    print('主线程 %s 运行结束.' % threading.current_thread().name)
    

    在这里插入图片描述

2.使用 threading 模块用类包装形式创建线程

  • 我们可以通过直接从threading.Thread继承创建一个新的子类,并实例化后调用 start() 方法启动新线程,即它调用了线程的 run() 方法

    import threading
    import time
    
    exitFlag = 0
    # 1.定义线程方法
    def loop(threadName, delay, counter):
        print('线程 %s 开始运行.' % threadName)
        while counter:
            if exitFlag: threadName.exit()
            time.sleep(delay)
            print('线程%s>>> %s' % (threadName, time.ctime(time.time())))
    
            counter -= 1
        print('线程 %s 运行结束.' % threadName)
    
    
    # 2.线程类继承threading.Thread,实现run()和初始化线程变量
    class myThread(threading.Thread):
        def __init__(self, threadID, name, delay):
            threading.Thread.__init__(self)
            self.threadID = threadID
            self.name = name
            self.delay = delay
    
        def run(self):
            loop(self.name, self.delay, 5)
    
    
    if __name__ == "__main__":
        print('主线程 %s 运行中...' % threading.current_thread().name)
        # 创建新线程
        thread1 = myThread(1, "Thread-1", 1)
        thread2 = myThread(2, "Thread-2", 2)
    
        # 开启新线程
        thread1.start()
        thread1.join()
        print('主线程 %s 运行结束.' % threading.current_thread().name)
    

    在这里插入图片描述

    • 由于任何进程默认就会启动一个主线程基于主线程又可以启动新的线程
      • Python的threading模块有个current_thread()函数,用于返回当前线程的实例。
      • 主线程实例的名字叫MainThread子线程的名字在创建时指定

三.线程锁

多线程和多进程最大的不同在于,多进程中,同一个变量,各自有一份拷贝存在于每个进程中,互不影响

  • 多线程中,所有变量都由所有线程共享,所以,任何一个变量都可以被任何一个线程修改,因此,线程之间共享数据最大的危险在于多个线程同时修改一个变量。

多线程的优势在于可以同时运行多个任务(至少感觉起来是这样)。但是当线程需要存取共享数据时,可能存在数据不同步的问题。也就是线程安全问题,如以下实例:

import time, threading

#银行总存款:
balance = 0

def change_it(n):
    # 先存后取,结果应该为0:
    global balance
    #总存款存入n元
    balance = balance + n
    #总存款取出n元
    balance = balance - n

def run_thread(n):
    for i in range(2000000):
        change_it(n)

t1 = threading.Thread(target=run_thread, args=(5,))
t2 = threading.Thread(target=run_thread, args=(8,))
t1.start()
t2.start()
t1.join()
t2.join()
print(balance)

全局共享变量balance,初始值为0,并且启动2个线程,先存后取,理论上结果应该为0,由于线程调度是由操作系统决定的,当t1、t2交替执行时,只要循环次数足够多,balance的结果就不一定是0了。

  • 原因是因为高级语言的一条语句在CPU执行时会被解析成若干条指令,即使一个简单的计算:

    如: 
    balance = balance + n 
    也分2步:
    	1.计算balance + n,存入临时变量中;
    	2.将临时变量的值赋给balance。
    
  • 也就是可以看成:

    x = balance + n
    balance = x
    

由于x是局部变量,两个线程各自都有自己的x,当代码单线程正常执行时:

初始值 balance = 0

t1: x1 = balance + 5 # x1 = 0 + 5 = 5
t1: balance = x1     # balance = 5
t1: x1 = balance - 5 # x1 = 5 - 5 = 0
t1: balance = x1     # balance = 0

t2: x2 = balance + 8 # x2 = 0 + 8 = 8
t2: balance = x2     # balance = 8
t2: x2 = balance - 8 # x2 = 8 - 8 = 0
t2: balance = x2     # balance = 0
    
结果 balance = 0

但是t1和t2是多线程交替运行的,如果操作系统以下面的顺序执行t1、t2:


初始值 balance = 0

t1: x1 = balance + 5  # x1 = 0 + 5 = 5

t2: x2 = balance + 8  # x2 = 0 + 8 = 8
t2: balance = x2      # balance = 8

t1: balance = x1      # balance = 5
t1: x1 = balance - 5  # x1 = 5 - 5 = 0
t1: balance = x1      # balance = 0

t2: x2 = balance - 8  # x2 = 0 - 8 = -8
t2: balance = x2      # balance = -8

结果 balance = -8

究其原因,是因为修改balance需要操作系统执行多条指令,而执行这几条语句时,线程可能中断,从而导致多个线程把同一个对象的内容改乱了。

  • 因此需要给change_it()加上锁,当某个线程开始执行change_it()时获得了锁,其他线程不能同时执行change_it(),只能等待,直到锁被释放后,获得该锁以后才能改由于锁只有一个,无论多少线程,同一时刻最多只有一个线程持有该锁,所以不会造成修改的冲突。创建一个锁就是通过**threading.Lock()来**实现:

    balance = 0
    #获取锁实例
    lock = threading.Lock()
    
    def run_thread(n):
        for i in range(100000):
            # 先要获取锁:
            lock.acquire()
            try:
                # 放心地改吧:
                change_it(n)
            finally:
                # 改完了一定要释放锁:
                lock.release()
    

考虑这样一种情况:一个列表里所有元素都是0,线程"set"从后向前把所有元素改成1,而线程"print"负责从前往后读取列表并打印。

  • 那么,可能线程"set"开始改的时候,线程"print"便来打印列表了,输出就成了一半0一半1,这就是数据的不同步。为了避免这种情况,引入了锁的概念。
import threading
import time


class myThread(threading.Thread):
    def __init__(self, threadID, name, delay):
        threading.Thread.__init__(self)
        self.threadID = threadID
        self.name = name
        self.delay = delay

    def run(self):
        print("开启线程: " + self.name)
        # 获取锁,用于线程同步
        threadLock.acquire()
        try:
            # 放心地改吧:
            print_time(self.name, self.delay, 3)
        finally:
       	 	# 释放锁,开启下一个线程
        	threadLock.release()


def print_time(threadName, delay, counter):
    while counter:
        time.sleep(delay)
        print("%s: %s" % (threadName, time.ctime(time.time())))
        counter -= 1


if __name__ == "__main__":
    threadLock = threading.Lock()
    threads = []

    # 创建新线程
    thread1 = myThread(1, "Thread-1", 1)
    thread2 = myThread(2, "Thread-2", 2)

    # 开启新线程
    thread1.start()
    thread2.start()

    # 添加线程到线程列表
    threads.append(thread1)
    threads.append(thread2)

    # 等待所有线程完成
    for t in threads:
        t.join()
    print("退出主线程")

总结:

  • 当多个线程同时执行lock.acquire()时,只有一个线程能成功地获取锁,然后继续执行代码,其他线程就继续等待直到获得锁为止。

  • 获得锁的线程用完后一定要释放锁,否则等待锁的线程将永远等待下去,成为死线程。所以我们用try...finally来确保锁一定会被释放。

  • 锁的好处就是确保了某段关键代码只能由一个线程从头到尾完整地执行,坏处当然也很多,首先是阻止了多线程并发执行,包含锁的某段代码实际上只能以单线程模式执行,效率就大大地下降了。

  • 如果存在多个锁,不同的线程持有不同的锁,并试图获取对方持有的锁时,可能会造成死锁,导致多个线程全部挂起,既不能执行,也无法结束,只能靠操作系统强制终止

四.队列( Queue)(新手了解即可)

Queue 模块中提供了同步的、线程安全的队列类,包括FIFO(先入先出)队列QueueLIFO(后入先出)队列LifoQueue,和优先级队列 PriorityQueue

  • 这些队列都在多线程中直接使用,可以使用队列来实现线程间的同步。

Queue 模块中的常用方法:

  • Queue.qsize() 返回队列的大小
  • Queue.empty() 如果队列为空,返回True,反之False
  • Queue.full() 如果队列满了,返回True,反之False
  • Queue.full 与 maxsize 大小对应
  • Queue.get([block[, timeout]])获取队列,timeout等待时间
  • Queue.get_nowait() 相当Queue.get(False)
  • Queue.put(item) 写入队列,timeout等待时间
  • Queue.put_nowait(item) 相当Queue.put(item, False)
  • Queue.task_done() 在完成一项工作之后,Queue.task_done()函数向任务已经完成的队列发送一个信号
  • Queue.join() 实际上意味着等到队列为空,再执行别的操作
import queue
import threading
import time

exitFlag = 0


class myThread(threading.Thread):
    def __init__(self, threadID, name, q):
        threading.Thread.__init__(self)
        self.threadID = threadID
        self.name = name
        self.q = q

    def run(self):
        print("开启线程:" + self.name)
        process_data(self.name, self.q)
        print("退出线程:" + self.name)


def process_data(threadName, q):
    while not exitFlag:
        #加锁
        queueLock.acquire()
        #如果工作队列不为空
        if not workQueue.empty():
            #获取队头
            data = q.get()
            #释放锁
            queueLock.release()
            print("%s processing %s" % (threadName, data))
        else:
            #释放锁
            queueLock.release()
        #休眠1s    
        time.sleep(1)


if __name__ == "__main__":
    # 线程集合
    threadList = ["Thread-1", "Thread-2", "Thread-3"]
    # 名称集合
    nameList = ["One", "Two", "Three", "Four", "Five"]
    # 线程锁
    queueLock = threading.Lock()
    # 生成一个长度为10的队列
    workQueue = queue.Queue(10)
    # 保存线程数组
    threads = []
    # 线程编号
    threadID = 1

    # 1.循环创建新线程
    for tName in threadList:
        thread = myThread(threadID, tName, workQueue)
        thread.start()
        threads.append(thread)
        threadID += 1

    # 2.填充队列
    queueLock.acquire()
    for word in nameList:
        workQueue.put(word)
    queueLock.release()

    # 等待队列清空
    while not workQueue.empty():
        pass

    # 通知线程是时候退出
    exitFlag = 1

    # 等待所有线程完成
    for t in threads:
        t.join()
    print("退出主线程")

在这里插入图片描述

五.ThreadLocal(新手了解即可)

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

  • ThreadLocal应运而生,每个线程都只能读写自己线程的独立副本,互不干扰。ThreadLocal解决了参数在一个线程中各个函数之间互相传递的问题。
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=('Alice',), name='Thread-A')
t2 = threading.Thread(target= process_thread, args=('Bob',), name='Thread-B')
t1.start()
t2.start()
t1.join()
t2.join()

执行结果:

Hello, Alice (in Thread-A)
Hello, Bob (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请求,用户身份信息等,这样一个线程的所有调用到的处理函数都可以非常方便地访问这些资源。

六.多核CPU

多核CPU该可以同时执行多个线程。 如果写一个死循环的话,会出现什么情况呢?

  • 打开Mac OS X的Activity Monitor,或者Windows的Task Manager,可以监控到一个死循环线程会100%占用一个CPU。

    • 如果有两个死循环线程,在多核CPU中,可以监控到会占用200%的CPU,也就是占用两个CPU核心。

    • 要想把N核CPU的核心全部跑满,就必须启动N个死循环线程。

试用Python启动CPU的核心N个死循环线程。:

import threading, multiprocessing

def loop():
    x = 0
    while True:
        x = x ^ 1

for i in range(multiprocessing.cpu_count()):
    t = threading.Thread(target=loop)
    t.start()
  • 启动与CPU核心数量相同的N个线程,在4核CPU上可以监控到CPU占用率仅有102%,也就是仅使用了一核

    • 但是用C、C++或Java来写相同的死循环,直接可以把全部核心跑满,4核就跑到400%,8核就跑到800%,为什么Python不行呢?

Python的线程虽然是真正的线程,但解释器执行代码时,有一个GIL锁:Global Interpreter Lock,任何Python线程执行前,必须先获得GIL锁,然后每执行100条字节码,解释器就自动释放GIL锁,让别的线程有机会执行。

  • GIL全局锁实际上把所有线程的执行代码都给上了锁,所以多线程在Python中只能交替执行,即使100个线程跑在100核CPU上,也只能用到1个核。**

  • GIL是Python解释器设计的历史遗留问题,通常我们用的解释器是官方实现的CPython,要真正利用多核,除非重写一个不带GIL的解释器

    • 所以Python多线程不能有效利用多核。如果一定要通过多线程利用多核,那只能通过C扩展来实现,不过这样就失去了Python简单易用的特点。

      • 不过,也不用过于担心,Python虽然不能利用多线程实现多核任务,但可以通过多进程实现多核任务多个Python进程有各自独立的GIL锁,互不影响。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1003219.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

菜单组件Menu

前面讲解了如果构建GUI界面&#xff0c;其实就是把一些GUI的组件&#xff0c;按照一定的布局放入到容器中展示就可以了。在实际开发中&#xff0c;除了主界面&#xff0c;还有一类比较重要的内容就是菜单相关组件&#xff0c;可以通过菜单相关组件很方便的使用特定的功能&#…

在历史长河中,这个震撼了我!

在一个人的一生中&#xff0c;很容易低估世界发生的变化。 科技能够用我们无法想象的方式改变世界&#xff0c;直到它们真的发生。 咱们回顾这个世界的技术史&#xff0c;有助于看清楚未来几年甚至几十年内可能发生的改变。 我们的祖辈在童年时代&#xff0c;很难想象出会有一个…

从9.10拼多多笔试第四题产生的01背包感悟

文章目录 题面基本的01背包问题本题变式 本文参考&#xff1a; 9.10拼多多笔试ak_牛客网 (nowcoder.com) 拼多多 秋招 2023.09.10 编程题目与题解 (xiaohongshu.com) 题面 拼多多9.10笔试的最后一题&#xff0c;是一道比较好的01背包变式问题&#xff0c;可以学习其解法加深对…

手搓消息队列【RabbitMQ版】

什么是消息队列&#xff1f; 阻塞队列&#xff08;Blocking Queue&#xff09;-> 生产者消费者模型 &#xff08;是在一个进程内&#xff09;所谓的消息队列&#xff0c;就是把阻塞队列这样的数据结构&#xff0c;单独提取成了一个程序&#xff0c;进行独立部署~ --------&…

工具教程【甜心转译】-双语字幕、中文语音生成(配音),打破信息差

甜心转译 &#xff08;主站&#xff09;是一款AI加持的音/视频生成双语字幕、中文语音的软件。帮助人们更容易的获取外语信息、不管是学习、还是娱乐&#xff0c;快人一步。 主要功能 字幕生成&#xff1a;只需几个简单的步骤&#xff0c;轻松生成字幕。字幕翻译&#xff1a;…

【校招VIP】java语言考点之异常

考点介绍&#xff1a; 导致程序的正常流程被中断的事件&#xff0c;叫做异常。异常是程序中的一些错误&#xff0c;但并不是所有的错误都是异常&#xff0c;并且错误有时候是可以避免的。异常发生的原因有很多&#xff0c;通常包含以下几大类: 1.用户输入了非法数据。2.要打开的…

【Python】爬虫基础

爬虫是一种模拟浏览器实现&#xff0c;用以抓取网站信息的程序或者脚本。常见的爬虫有三大类&#xff1a; 通用式爬虫&#xff1a;通用式爬虫用以爬取一整个网页的信息。 聚焦式爬虫&#xff1a;聚焦式爬虫可以在通用式爬虫爬取到的一整个网页的信息基础上只选取一部分所需的…

首家!亚信科技AntDB数据库完成中国信通院数据库迁移工具专项测试

近日&#xff0c;在中国信通院“可信数据库”数据库迁移工具专项测试中&#xff0c;湖南亚信安慧科技有限公司&#xff08;简称&#xff1a;亚信安慧科技&#xff09;数据库数据同步平台V2.1产品依据《数据库迁移工具能力要求》、结合亚信科技AntDB分布式关系型数据库产品&…

pinduoduo(商品详情)API接口

为了进行电商平台 的API开发&#xff0c;首先我们需要做下面几件事情。 1&#xff09;开发者注册一个账号 2&#xff09;然后为每个pinduoduo应用注册一个应用程序键&#xff08;App Key) 。 3&#xff09;下载pinduoduo API的SDK并掌握基本的API基础知识和调用 4&#xff…

冠达管理:etf怎样购买?

ETF是一种指数基金&#xff0c;与传统的自动办理型基金不同&#xff0c;相比之下&#xff0c;ETF是一种被动出资东西&#xff0c;因为它们的方针是跟从某个特定的指数&#xff0c;而不是企图在市场上打败其他出资者。ETF通常具有较低的办理费用、较高的流动性和灵敏的买卖方法&…

CANoe的工作模式之争:模拟总线的两种运行方式

我们在文章《CANoe中的工作模式之争:由一段简单的代码引出的问题》中,介绍了模拟总线模式下的三种工作方式: animated with factoras fast as possibleslave mode由于模拟总线模式不需要连接真实ECU,无需和真实ECU保持时间同步,那么就可以在模拟总线上加速或放缓程序的运行…

pytorch从0开始安装

文章目录 一. 安装anaconda1.安装pytorch前需要先安装anaonda&#xff0c;首先进入官网&#xff08;Anaconda | The Worlds Most Popular Data Science Platform&#xff09;进行安装相应的版本。2.接着按如图所示安装,遇到下面这个选项时&#xff0c;选择all users.3.选择自己…

如何与 PGA Tour Superstore 建立 EDI 连接?

PGA Tour Superstore 是一家专业的高尔夫用品卖场&#xff0c;以销售高品质高尔夫球具、装备和配件为主要经营范围。其使命是为高尔夫爱好者提供一站式购物体验&#xff0c;帮助他们在球场上取得更优越的成绩。多年来&#xff0c;PGA Tour Superstore 凭借其卓越的产品选择、专…

EFCore 基于Code First开发的配置

EFCore版本&#xff1a;7.0.10 Visual Studio版本&#xff1a;2022 1、首先新建一个项目(无论是.net framework还是.net core都可以)&#xff0c;在项目中添加EFCore的程序包&#xff1a; 2、新建一个文件夹Models&#xff0c;把表模型都放在这个文件夹里 3、新建表模型&…

Java笔记:线程池

一. 正确使用ThreadPoolExecutor创建线程池 1.1、基础知识 Executors创建线程池便捷方法列表&#xff1a;下面三个是使用ThreadPoolExecutor的构造方法创建的 方法名功能newFixedThreadPool(int nThreads)创建固定大小的线程池newSingleThreadExecutor()创建只有一个线程的线…

三维模型3DTile格式轻量化压缩模型变形浅析

三维模型3DTile格式轻量化压缩模型变形浅析 在对三维模型进行轻量化压缩处理的过程中&#xff0c;常常会出现模型变形的现象。这种变形现象多数源于模型压缩过程中信息丢失或误差累积等因素。以下将对此现象进行详细分析。 首先&#xff0c;我们需要了解三维模型轻量化压缩的…

前端设计模式基础笔记

前端设计模式是指在前端开发中经常使用的一些解决问题的模式或思想。它们是经过实践证明的最佳实践&#xff0c;可以帮助我们更好地组织和管理我们的代码。 一、单例模式&#xff08;Singleton Pattern&#xff09; 单例模式是一种创建型模式&#xff0c;它保证一个类只有一个…

2023最新轻松升级、安装和试用Navicat Premium 16.2.10 教程详解

&#x1f337;&#x1f341; 博主猫头虎&#xff08;&#x1f405;&#x1f43e;&#xff09;带您 Go to New World✨&#x1f341; &#x1f405;&#x1f43e;猫头虎建议程序员必备技术栈一览表&#x1f4d6;&#xff1a; &#x1f6e0;️ 全栈技术 Full Stack: &#x1f4da…

ArcGIS标注的各种用法和示例

标注是将描述性文本放置在地图中的要素上或要素旁的过程。 本文整理了ArcGIS中的各种标注方法、可能遇到的问题和细节,内容比较杂,想到哪写到哪。 一、正常标注某一字段值的内容 右键点击【属性】,在【标注】选项卡下勾选【标注此图层中的的要素】,在【文本字符串】栏中…

HTTPS/HTTP2

HTTPS HTTPS(HyperText Transfer Protocol Secure)&#xff0c;译为&#xff1a;超文本传输安全协议 常称为HTTP over TLS、HTTP over SSL、HTTP Secure由网景公司于1994年首次提出 HTTPS的默认端口号443&#xff08;HTTP是80&#xff09; SSL/TLS HTTPS是在HTTP的基础上使…