首页 > 其他分享 >29.并发编制【六】守护线程与锁

29.并发编制【六】守护线程与锁

时间:2024-05-28 20:23:40浏览次数:10  
标签:__ Thread work 29 并发 线程 time print

【一】守护线程

守护线程是在后台运行并依赖于主线程或非守护线程的存在

1)主线程死亡,子线程存活

  • 主线程结束后不会立马结束,而是等待其他子线程结束之后结束
from threading import Thread
import time

def work(name):
    print(f'{name}开始')
    time.sleep(2)
    print(f'{name}结束')

if __name__ == '__main__':
    print(f'主线程开始')
    t = Thread(target=work, args=('ST1',))
    t.start()
    print(f'主线程结束')
# 主线程开始
# ST1开始
# 主线程结束
# ST1结束

2)主线程死亡,子线程也死亡

  • 给子线程添加守护,便可在主进程结束后直接结束,无需等待子线程
from threading import Thread
import time

def work(name):
    print(f'{name}开始')
    time.sleep(2)
    print(f'{name}结束')

if __name__ == '__main__':
    print(f'主进程开始')
    t = Thread(target=work, args=('ST1',))
    t.daemon = True
    t.start()
    print(f'主进程结束')
# 主进程开始
# ST1开始
# 主进程结束

3)示例

  • work_1被守护,当主线程结束后,程序会等待work_2结束后直接关闭
  • 若work_1在work_2之后完成,程序将等不到work_1的输出便会结束
  • 若work_2在work_1之后完成,程序便能正常接收到work_1的输出
from threading import Thread
import time

def work_1():
    print('work_1 开始')
    time.sleep(2)
    print('work_1 结束')

def work_2():
    print('work_2 开始')
    time.sleep(1)
    print('work_2 结束')

if __name__ == '__main__':
    print('主线程开始')
    t1 = Thread(target=work_1)
    t1.daemon = True
    t2 = Thread(target=work_2)
    t1.start()
    t2.start()
    print('主线程结束')
# 主线程开始
# work_1 开始
# work_2 开始
# 主线程结束
# work_2 结束

【二】线程的互斥锁

1)无互斥锁

  • 所有子进程都会进行阻塞,导致最后的结果只改变了一次
  • 由于延迟的存在,每次赋值时原值都未来得及改变,才使得结果只改变了一次
from threading import Thread
import time
number = 0

def work():
    global number
    num = number
    time.sleep(0.03)
    number = num + 1

if __name__ == '__main__':
    task_list = []
    for i in range(100):
        t = Thread(target=work)
        task_list.append(t)
    [t.start() for t in task_list]
    [t.join() for t in task_list]
    print(number)
# 1

2)有互斥锁

  • 在数据发送变化前加锁,让上一个进程结束后在执行下一个进程
from threading import Thread, Lock
import time
number = 0
# 生成锁
lock = Lock()

def work():
    global number
    # 数据改变前加锁
    lock.acquire()
    num = number
    time.sleep(0.03)
    number = num + 1
    # 数据改变后解锁
    lock.release()

if __name__ == '__main__':
    task_list = []
    for i in range(100):
        t = Thread(target=work)
        task_list.append(t)
    [t.start() for t in task_list]
    [t.join() for t in task_list]
    print(number)
# 100

【三】GIL全局解释器锁

1)介绍

在Cpython中,全局解释器锁(GIL)是一个互斥锁,它可以防止多个本地线程同时执行Python字节码。这把锁主要是必要的
因为Cpython的内存管理不是线程安全的。(然而,由于GIL存在,其他特性已经发展到依赖于它所实施的保证。)

2)区别

from threading import Thread, Lock
import time

lock = Lock()
number = 0

# 所有线程都去抢那把 GIL 锁住的数据,当所有子线程都抢到后再去修改数据就变成了 99
def work_1():
    global number
    num = number
    time.sleep(0.1)
    number = num + 1

# 谁先抢到谁就先处理数据
def work_2():
    global number
    num = number
    number = num + 1

# 自动加锁并解锁
# 子线程启动,后先去抢GIL锁,进入IO自动释放GIL锁,但是自己加的锁还没解开,其他线程资源能抢到GIL锁,但是抢不到互斥锁,最终GIL回到互斥锁的那个进程上,处理数据
def work_3():
    global number
    with lock:
        num = number
        time.sleep(0.01)
        number = num + 1

if __name__ == '__main__':
    task_list = []
    for i in range(100):
        t = Thread(target=work_)
        task_list.append(t)
    [t.start() for t in task_list]
    [t.join() for t in task_list]
    print(number)
#work_1:1
#work_2:100
#work_3:100

3)GIL导致多线程无法利用多核优势

1.Cpython解释器中GIL

  • 在Cpython解释器中GIL是一把互斥锁,用来阻止同一个进程下的多个线程的同时进行

2.Python的多线程

  • 多线程是否有用看情况
    • 单核
      • 四个任务(IO密集型/计算密集型)
    • 多核
      • 四个任务(IO密集型/计算密集型)
计算密集型

一种处在计算运行中

  • 每个任务都需要 10s

    • 单核
      • 多进程:额外消耗资源
      • 多线程:减少开销
    • 多核
      • 多进程:总耗时 10s
      • 多线程:总耗时 40s+
from multiprocessing import Process
from threading import Thread
import time, os

def work():
    res = 0
    for i in range(1, 100000000):
        res *= i

def main_t():
    t_list = []
    # 获取当前CPU运行的个数
    print(os.cpu_count())
    start_time = time.time()
    for i in range(12):
        t = Thread(target=work)
        t.start()
        t_list.append(t)
    for t in t_list:
        t.join()
    print(f'总耗时:>>>{time.time() - start_time}')
    
def main_p():
    p_list = []
    # 获取当前CPU运行的个数
    print(os.cpu_count())
    start_time = time.time()
    for i in range(12):
        p = Process(target=work)
        p.start()
        p_list.append(p)
    for p in p_list:
        p.join()
    print(f'总耗时:>>>{time.time() - start_time}')

if __name__ == '__main__':
    main_t()  # 十二个核一致在跑 12 个进程就需要开设 12块内存空间 ---> 耗费资源和时间
    # 12
    # 总耗时:>>>5.368584394454956
    main_p()  # 只需要开设一个进程 在进程内部开设 12个线程就可以了
    # 12
    # 总耗时:>>>27.101927280426025
    
IO密集型

存在多个 IO 阻塞切换操作

  • 每个任务都需要 10s

    • 多核
      • 多进程:相对浪费资源
      • 多线程:更加节省资源
from multiprocessing import Process
from threading import Thread
import time, os

def work():
    res = 0
    for i in range(1, 100000000):
        res *= i

def main_t():
    t_list = []
    # 获取当前CPU运行的个数
    print(os.cpu_count())
    start_time = time.time()
    for i in range(12):
        t = Thread(target=work)
        t.start()
        t_list.append(t)
    for t in t_list:
        t.join()
    print(f'总耗时:>>>{time.time() - start_time}')

def main_p():
    p_list = []
    # 获取当前CPU运行的个数
    print(os.cpu_count())
    start_time = time.time()
    for i in range(12):
        p = Process(target=work)
        p.start()
        p_list.append(p)
    for p in p_list:
        p.join()
    print(f'总耗时:>>>{time.time() - start_time}')

if __name__ == '__main__':
    main_t()  # 十二个核一致在跑 12 个进程就需要开设 12块内存空间 ---> 耗费资源和时间
    # 12
    # 总耗时:>>>27.46139693260193
    main_p()  # 只需要开设一个进程 在进程内部开设 12个线程就可以了
    # 12
    # 总耗时:>>>5.451528549194336

3.小结

  • 计算是消耗cpu的:

    • 代码执行,算术,for都是计算
  • io不消耗cpu:

    • 打开文件,写入文件,网络操作都是io
    • 如果遇到io,该线程会释放cpu的执行权限,cpu转而去执行别的线程
  • 计算密集型任务使用多进程可以充分利用多核CPU的优势,

  • IO密集型任务使用多线程能够更好地处理IO操作,避免频繁的进程切换开销。

4.GIL特点总结

  • GIL 不是python的特点而是Cpython解释器的特点
  • GIL 保证解释器级别的数据的安全
  • GIL会导致同一个进程下的多个线程的无法同时进行即无法利用多核优势
  • 针对不同的数据还是需要加不同的锁处理
  • 解释型语言的通病:同一个进程下的多个线程无法利用多核优势

【四】死锁

1)介绍

  • 指两个或多个进行,在执行过程中因争夺资源而造成了互相等待的一种现象

  • 即两个或多个进程持有各自的锁并试图获取对方持有的锁,从而导致阻塞,不能向前执行,最终形成僵局。

2)决绝方案

  • 锁不要有多个,一个足够
  • 如果真的发生了死锁的问题,必须迫使一方先交出锁

3)示例

from threading import Thread, Lock
import time

lock_1 = Lock()
lock_2 = Lock()

class MyThread(Thread):
    def run(self):
        self.work1()
        self.work2()
    def work1(self):
        lock_1.acquire()
        print(f'{self.name}抢到了A锁')
        lock_2.acquire()
        print(f'{self.name}抢到了B锁')
        lock_1.release()
        lock_2.release()
    def work2(self):
        lock_2.acquire()
        print(f'{self.name}抢到了A锁')
        time.sleep(1)
        lock_1.acquire()
        print(f'{self.name}抢到了B锁')
        lock_1.release()
        lock_2.release()

if __name__ == '__main__':
    for i in range(5):
        t = MyThread()
        t.start()
# Thread-1抢到了A锁
# Thread-1抢到了B锁
# Thread-1抢到了A锁
# Thread-2抢到了A锁
# 进程卡死,无法继续

【五】递归锁

1)介绍

  • 一种特殊的锁,允许一个线程多次请求同一个锁
  • 在该线程释放锁之前,会对锁计数器进行累加操作,线程每成功获得一次锁时,都要进行相应的解锁操作,直到锁计数器清零才能完全释放
  • 其能保证同一线程在持有锁时能够再次获取该所,而不是被自己所持有的锁阻塞
  • 但过多的获取锁会导致性能下降

3)示例

from threading import Thread, Lock, RLock
import time

lock_1 = lock_2 = RLock()

class MyThread(Thread):
    def run(self):
        self.work1()
        self.work2()
    def work1(self):
        lock_1.acquire()
        print(f'1-{self.name}抢到了A锁')
        lock_2.acquire()
        print(f'1-{self.name}抢到了B锁')
        lock_2.release()
        lock_1.release()
    def work2(self):
        lock_2.acquire()
        print(f'2-{self.name}抢到了A锁')
        time.sleep(1)
        lock_1.acquire()
        print(f'2-{self.name}抢到了B锁')
        lock_1.release()
        lock_2.release()

if __name__ == '__main__':
    for i in range(3):
        t = MyThread()
        t.start()
# 1-Thread-1抢到了A锁
# 1-Thread-1抢到了B锁
# 2-Thread-1抢到了A锁
# 2-Thread-1抢到了B锁
# 1-Thread-2抢到了A锁
# 1-Thread-2抢到了B锁
# 2-Thread-2抢到了A锁
# 2-Thread-2抢到了B锁
# 1-Thread-3抢到了A锁
# 1-Thread-3抢到了B锁
# 2-Thread-3抢到了A锁
# 2-Thread-3抢到了B锁

标签:__,Thread,work,29,并发,线程,time,print
From: https://www.cnblogs.com/Mist-/p/18218776

相关文章

  • 28.并发编制【五】管道与多线程
    【一】管道1)介绍frommultiprocessingimportPipe#创建管道left_pipe,right_pipe=Pipe()#返回管道两端的连接对象,需在产生Process对象之前产生管道#默认参数dumplex:默认管道是全双工的#若为False,left_pipe只能用于接收,right_pipe只能用于发送2)主要方法#接收数......
  • 30.并发编制【七】
    【一】信号量(Semahpore)1)概念互斥锁:允许在同一时刻只能有一个线程或进程同资源进行修改信号量:允许指定数量的进程或线程对资源进行修改2)示例frommultiprocessingimportProcess,Semaphoreimporttimeimportrandomdefeg(sem,user):#对信号量加锁sem.acq......
  • 【代码之髓】研究编程语言的核心点——结构化编程、函数、异常、作用域、类型、容器、
    写在前面本文基于人民邮电出版社发行的西尾泰和先生所著《代码之髓》有一定读书笔记性质,算是精简版改写。目录写在前面如何深入高效地学习语言从比较中学习从历史中学习程序设计语言诞生史语法的诞生程序的流程控制if诞生以前whilefor函数为什么要有函数函数的返回......
  • 高并发多高才算高?
    高并发是指系统能够同时处理大量并发请求的能力。在现代互联网应用中,高并发是常见的挑战之一,尤其是在电商促销、金融交易、社交媒体和流媒体服务等场景中。那么,到底多高的并发量才算是高并发呢?本文将从多个角度进行探讨。高并发的衡量标准高并发的具体标准因应用场景而异,通常使......
  • python+threading,实现简单的接口并发测试
    #-*-coding:utf-8-*-importthreadingfromutilsimporthttpUtilbody={"claimId":10179599,"protocols":[{"protocolUrl":None,"protocolContent":"<spanclass='c_......
  • 【go从入门到精通】精通并发编程-使用atomic管理状态和同步的无锁技术
    了解原子计数器        在Go中,原子计数器是多个goroutine可以同时访问的共享变量。术语“原子”是指在计数器上执行的操作的不可分割的性质。在Go中,原子计数器允许多个goroutine安全地更改共享变量,而无需使用锁或任何其他显式同步,这可确保数据完整性并避免竞......
  • kafka多线程顺序消费
    一、单线程顺序消费为了避免有的小伙伴第一次接触顺序消费的概念,我还是先介绍一下顺序消费是个什么东西。双十一,大量的用户抢在0点下订单。为了用户的友好体验,我们把订单生成逻辑与支付逻辑包装成一个个的MQ消息发送到Kafka中,让kafka积压部分消息,防止瞬间的流量压垮服务。那么......
  • 基于linux下c实现的简单版线程池
    #include<iostream>#include<unistd.h>#include<pthread.h>#include<string>#include<signal.h>#include<stdlib.h>#include<string.h>#include<errno.h>#defineDEFAULT_TIME10#defineDEFAULT_STEP15using......
  • SpringBoot系列---【线程池优雅停机,避免消费数据丢数的问题】
    1.问题项目中通过kafka来对接上游,在项目中写一个listener监听topTopic队列,for循环消费records,在for循环中处理成存储到es的对象,一次拉50条,使用自定义线程池esThreadPool异步推送到es中,但是每次停机就会丢数据,例:kafka消费了1000条,但是往es中存储比较慢,优雅停机的时候,esThreadPool......
  • C129 并查集+01背包 P1455 搭配购买
    视频链接:C129并查集+01背包P1455搭配购买_哔哩哔哩_bilibili  E08【模板】背包DP01背包_哔哩哔哩_bilibiliP1455搭配购买-洛谷|计算机科学教育新生态(luogu.com.cn)//并查集+01背包#include<iostream>#include<cstring>#include<algorithm>usingname......