首页 > 其他分享 >28. 多线程、互斥锁

28. 多线程、互斥锁

时间:2024-09-23 22:46:09浏览次数:1  
标签:__ money print 28 互斥 线程 进程 ticket 多线程

1. 多线程理论

1.1 什么是线程

(1)概念

在操作系统中,每个进程都有一个内存空间地址。

而且默认每个进程都有一个控制线程,即自带一个主线程。

进程是用来把资源集中到一起(进程是一个资源单位,或者称资源集合),线程是CPU上的执行单位。

多线程(即多个控制线程)的概念:一个进程中存在多个控制线程,多个控制线程共享该进程的地址空间。

相当于一个车间内有多条流水线,都共用一个车间的资源。

例如:上海地铁和广州地铁是不同的进程,而广州地铁的13号线是一个线程,广州地铁所有的线路共享广州地铁的资源。

(2)总结

进程:资源单位  

   启一个进程的含义是在内存中获得一块独立的空间

线程:执行单位  

   真正被CPU执行的是进程里面的线程

   线程指的就是代码的执行过程,执行代码过程所需要的资源都找所在的进程获取

1.2 线程的创建开销

每一个进程都要划分内存中的地址,并且相互独立

线程的创建在进程之内,线程创建的开销更小

1.3 进程与进程、线程与线程之间的关系

不同的进程是竞争关系     例如:不同的程序抢占网速

同一个进程内的线程之间是合作关系

 1.4 线程和进程的区别

线程共享创建它的进程的地址空间;进程具有自己的地址空间。

线程可以直接访问其进程的数据段;进程具有其父进程数据段的副本。

线程可以直接与其进程中的其它线程通信;进程必须使用进程间通信与同级进程进行通信。

新线程很容易创建;新进程需要复制父进程。

线程可以对同一进程的线程行使相当大的控制权;进程只能控制子进程。

对主线程的更改(取消、优先级更改等)可能会影响该进程其它线程的行为;对父进程的更改不会影响子进程。

2. 开启线程的两种方式

开启线程不需要在主程序入口下面执行代码,直接书写即可

一般习惯性地将启动命令写在主程序入口下

2.1 方式一:直接调用Thread函数

from threading import Thread

# 定义子线程功能函数
def work(name):
    print(f'子线程{name}开始运行')
    print(f'子线程{name}结束运行')

# 定义产生子线程函数
def create_thread():
    st_list = [Thread(target=work, args=(i,)) for i in range(1, 4)]
    [st.start() for st in st_list]  # 将每一个子线程启动
    [st.join() for st in st_list]  # 主进程等子线程结束再结束

if __name__ == '__main__':
    print('主进程启动')
    create_thread()
    print('主进程结束')

2.2 方式二:重写Thread的run方法

from threading import Thread

# 继承Thread重写run方法
class NewThread(Thread):
    def __init__(self, name):
        super().__init__()
        self.name = name

    def run(self):
        print(f'子线程{self.name}运行开始')
        print(f'子线程{self.name}运行结束')

# 定义产生子线程函数
def create_thread():
    st_list = [NewThread(name=f'{i}') for i in range(1, 4)]
    [st.start() for st in st_list]  # 将每一个子线程启动
    [st.join() for st in st_list]  # 主进程等子线程结束再结束

if __name__ == '__main__':
    print('主进程启动')
    create_thread()
    print('主进程结束')

 2.3 线程调用join函数与进程调用join函数的区别

 (1)子线程不调用join无休眠

from threading import Thread

# 定义子线程功能函数
def work(name):
    print(f'子线程{name}开始运行')
    print(f'子线程{name}结束运行')

# 定义产生子线程函数
def create_thread():
    st_list = [Thread(target=work, args=(i,)) for i in range(1, 4)]
    [st.start() for st in st_list]  # 将每一个子线程启动

if __name__ == '__main__':
    print('主进程启动')
    create_thread()
    print('主进程结束')

 分析:

子线程不调用join与子进程调用join几乎无区别,都是主进程等待子进程(子线程)结束再结束

(2)子线程不调用join但有休眠

import time
from threading import Thread

# 定义子线程功能函数
def work(name):
    print(f'子线程{name}开始运行')
    time.sleep(0.01)
    print(f'子线程{name}结束运行')

# 定义产生子线程函数
def create_thread():
    st_list = [Thread(target=work, args=(i,)) for i in range(1, 4)]
    [st.start() for st in st_list]  # 将每一个子线程启动

if __name__ == '__main__':
    print('主进程启动')
    create_thread()
    print('主进程结束')

分析:

子线程不调用join且有休眠与子进程不调用join有很大区别

子进程不调用join:主进程开始---主进程结束---子进程依次开始---子进程依次结束

子线程不调用join且有休眠:主进程开始---子线程依次开始---主进程结束---子线程依次结束

3. 多线程属性

查看线程名称:current_thread( ).name

统计存活的线程数:active_count( )

import time
import os
from threading import Thread, current_thread, active_count

# 定义子线程功能函数
def work(name):
    print(f'子线程{current_thread().name}开始运行, 进程号为{os.getpid()},存活的线程数为{active_count()}')
    time.sleep(0.01)
    print(f'子线程{current_thread().name}结束运行, 进程号为{os.getpid()},子线程结束前存活的线程数为{active_count()}')

# 定义产生子线程函数
def create_thread():
    st_list = [Thread(target=work, args=(i,)) for i in range(1, 4)]
    [st.start() for st in st_list]  # 将每一个子线程启动

if __name__ == '__main__':
    print('主进程启动')
    create_thread()
    print('主进程结束')

 每一个子线程的进程号一致,证明了所有的子线程都是同一个进程开设的

为什么第一次启动子线程时存活的线程数是2?

每个进程都有一个控制线程,即自带一个主线程。

4. 多线程实现TCP服务端并发

# 服务端
from socket import socket, AF_INET, SOCK_STREAM, SOL_SOCKET, SO_REUSEADDR, SOL_SOCKET
from threading import Thread

# 定义子线程功能函数
def work(conn, addr):
    while True:
        info_from_client = conn.recv(1024)
        print(f'客户端{addr}发来的信息为:{info_from_client.decode("utf-8")}')
        conn.send(info_from_client.decode("utf-8").upper().encode('utf-8'))


# 定义产生子线程函数
def create_thread(cls, conn, addr):
    task = cls(target=work, args=(conn, addr))
    task.start()
    # 如果这里调用join函数多进程和多线程都无法实现多个客户端与服务端通信,只有第一个客户端可以,之后的会产生阻塞


# 定义主函数
def main():
    server = socket(AF_INET, SOCK_STREAM)
    server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
    server.bind(('127.0.0.1', 9002))
    server.listen(3)
    while True:
        conn, addr = server.accept()
        # 开启多线程
        create_thread(cls=Thread, conn=conn, addr=addr)


if __name__ == '__main__':
    main()
# 客户端
from socket import socket, AF_INET, SOCK_STREAM

client = socket(AF_INET, SOCK_STREAM)
client.connect(('127.0.0.1', 9002))
while True:
    info_to_server = input('请输入发给服务端的信息:')
    client.send(info_to_server.encode('utf-8'))
    info_from_server = client.recv(1024)
    print(f"客户端返回的信息为:{info_from_server.decode('utf-8')}")

 5. 互斥锁

5.1 理论

互斥锁是一种用于多线程编程中控制对共享资源访问的机制。

其作用是保证在同一时刻只有一个线程在访问共享资源,从而避免多个线程同时读写数据造成的问题。

互斥锁的基本原理是在对共享资源进行访问前加锁,使得其它线程无法访问该资源,当访问完成后再解锁,其它线程可以进行访问。

通过这种方式,可以保证同一时间只有一个线程在执行关键代码段,从而保证了数据的安全性。

需要注意的是,互斥锁会带来一些额外的开销。

5.2 子线程与子进程加锁代码示例

子线程加锁

import time
from threading import Thread, Lock

# 定义子线程功能函数
def work(name, lock):
    lock.acquire()  # 在数据改变之前加锁
    print(f'子线程{name}开始运行')
    time.sleep(1)
    print(f'子线程{name}结束运行')
    lock.release()  # 结束释放锁

# 定义产生子线程函数
def create_thread(cls):
    lock_obj = Lock()  # 生成锁对象
    task_list = []
    for i in range(1, 4):
        task = cls(target=work, args=(i, lock_obj))
        task_list.append(task)
    [task.start() for task in task_list]  # 将每一个子线程启动
    [task.join() for task in task_list]  # 主进程等子线程结束再结束
    # 加锁必须开启join,否则会报错

if __name__ == '__main__':
    print('主进程开始')
    create_thread(cls=Thread)
    print('主进程结束')

分析:

如果不加锁,子进程功能函数在有休眠的情况下,代码运行顺序为:主进程开始---子进程依次开始---子进程依次结束---主进程结束

加了锁之后,有休眠,多线程也会变成串行,由原来的子线程依次开始、子线程依次结束,变成了等待上一个子线程结束下一个子线程才能开始

 

子进程加锁将以上代码中的cls=Thread改为cls=Process,Lock从multiprocessing中导入即可

5.3 多进程抢票(不加锁)

import json
import os
import time
from multiprocessing import Process

# 将余票数据存放到本地文件中
db_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'ticket.json')

# 定义产生票数初始化的函数
def init_ticket():
    ticket_dict = {'ticket_num': 2}
    save_ticket(data=ticket_dict)
    print('票数初始化完成,余票为2')

# 定义保存余票数量的函数
def save_ticket(data):
    with open(db_path, 'w', encoding='utf-8') as f1:
        json.dump(obj=data, fp=f1)

# 定义获取余票数量的函数
def read_ticket():
    with open(db_path, 'r', encoding='utf-8') as f2:
        ticket_dict = json.load(f2)
    return ticket_dict

# 定义子进程功能函数
def buy_ticket(username):
    ticket_dict = read_ticket()  # 查票
    print(f'用户{username}查看余票,余票数量为{ticket_dict.get("ticket_num")}')
    time.sleep(0.1)  # 模拟买票延迟,在进程与线程中,休眠的作用是切换进程/线程
    rest = ticket_dict.get('ticket_num')  # 获取数据库中的余票
    if rest > 0:
        ticket_dict['ticket_num'] -= 1
        save_ticket(data=ticket_dict)
        print(f'用户{username}购票成功,余票为:{ticket_dict.get("ticket_num")}')
    else:
        print('余票不足')

# 定义产生子进程函数
def create_process():
    sp_list = [Process(target=buy_ticket, args=(f'{i}',)) for i in range(1, 4)]
    [sp.start() for sp in sp_list]
    [sp.join() for sp in sp_list]

if __name__ == '__main__':
    init_ticket()
    create_process()

 

5.4 多进程抢票(加锁)

import json
import os
import time
from multiprocessing import Process, Lock

# 将余票数据存放到本地文件中
db_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'ticket.json')

# 定义产生票数初始化的函数
def init_ticket():
    ticket_dict = {'ticket_num': 2}
    save_ticket(data=ticket_dict)
    print('票数初始化完成,余票为2')

# 定义保存余票数量的函数
def save_ticket(data):
    with open(db_path, 'w', encoding='utf-8') as f1:
        json.dump(obj=data, fp=f1)

# 定义获取余票数量的函数
def read_ticket():
    with open(db_path, 'r', encoding='utf-8') as f2:
        ticket_dict = json.load(f2)
    return ticket_dict

# 定义子进程功能函数
def buy_ticket(username, lock):
    ticket_dict = read_ticket()  # 查票
    print(f'用户{username}查看余票,余票数量为{ticket_dict.get("ticket_num")}')
    time.sleep(0.1)  # 模拟买票延迟,在进程与线程中,延迟的作用是切换进程/线程
    lock.acquire()
    ticket_dict = read_ticket()  # 查票
    rest = ticket_dict.get('ticket_num')  # 获取数据库中的余票
    if rest > 0:
        ticket_dict['ticket_num'] -= 1
        save_ticket(data=ticket_dict)
        print(f'用户{username}购票成功')
    else:
        print('购票失败,余票不足')
    lock.release()

# 定义产生子进程函数
def create_process():
    lock = Lock()  # 生成锁对象
    sp_list = [Process(target=buy_ticket, args=(f'{i}', lock)) for i in range(1, 4)]
    [sp.start() for sp in sp_list]
    [sp.join() for sp in sp_list]


if __name__ == '__main__':
    init_ticket()
    create_process()

 分析:

与5.3中的主要区别是查票的代码增加了一次,买票(数据改变)的过程加上了锁

程序第一个子进程运行到sleep时会休眠,切换到另一个子进程,再运行到sleep时再切换子进程

因此,第一次查票代码的作用是每个子进程查看系统余票(任何进程都没有购票)

子进程3休眠切换到子进程1,子进程1执行休眠之后的代码,再一次进行查票,并且购票,余票数量减少

子进程1购票完成,自动切换到子进程2,此时第二次查票的作用为每个子进程查看之前子进程购票之后的余票(已经有子进程购票,数据库中数量已发生变化)

子进程2购票完成,自动切换到子进程3,此时第二次查票发现已无余票,因此购票失败。

 

6. 线程的互斥锁

import time
from threading import Thread, Lock
from multiprocessing import Process, Lock

money = 99

def work(lock):
    global money
    lock.acquire()
    temp = money
    # time.sleep(0.01)  # 进行进程/线程切换
    money = temp - 1
    lock.release()

def create_spst(cls):
    lock = Lock()
    print(f'修改之前{money}')
    task_list = [cls(target=work, args=(i, lock)) for i in range(99)]
    [task.start() for task in task_list]
    [task.join() for task in task_list]
    print(f'修改之后{money}')

if __name__ == '__main__':
    create_spst(cls=Thread)

分析:

多线程共享的是同一个资源

给子线程加上锁之后sleep已经起不到切换线程的作用,数字会递减到0

7. 进程与线程其它不加锁的情况

(1)多线程不加锁运行顺序

import time
from threading import Thread

money = 99

def work1():
    global money
    temp = money
    print(f'work1 sleep之前 :>>> {money}')
    time.sleep(0.01)
    print(f"work 1修改之前 :>>> {money}")
    money = temp - 1
    print(f'work1 sleep之后 :>>> {money}')
    print(f'work1 修改之后 :>>> {money}')

def work2():
    global money
    temp = money
    print(f'work2 修改之前 :>>> {money}')

    money = temp + 2
    print(f'work2 修改之后:>>> {money}')

def create_thread():
    print(f"修改之前 :>>>> {money}")
    task1 = Thread(target=work1)
    task2 = Thread(target=work2)
    task1.start()
    task2.start()
    task1.join()
    task2.join()
    print(f"修改之后 :>>>> {money}")


if __name__ == '__main__':
    create_thread()

 分析:

# 修改之前 :>>>> 99             主进程开始
# work1 sleep之前 :>>> 99 首先运行子线程1,此数据为全局的99
# work2 修改之前 :>>> 99 子线程1运行到sleep休眠,切换到子线程2,此数据为全局的99
# work2 修改之后:>>> 101 子线程2无休眠,继续运行,money为temp(99)+2=101,全局也为101;子线程2运行结束又去运行子线程1休眠后剩下的代码,因为join要等所有子线程结束主进程才能结束
# work 1修改之前 :>>> 101 接上一步全局为101
# work1 sleep之后 :>>> 98 money为temp-1,temp为最开始全局的99没变化,此时money提到全局
# work1 修改之后 :>>> 98 为上一步全局的值
# 修改之后 :>>>> 98 为上一步/上上一步全局的值

(2)多进程不加锁运行顺序

import time
from multiprocessing import Process

money = 99

def work1():
    global money
    temp = money
    print(f'work1 sleep之前 :>>> {money}')
    time.sleep(0.01)
    print(f"work 1修改之前 :>>> {money}")
    money = temp - 1
    print(f'work1 sleep之后 :>>> {money}')
    print(f'work1 修改之后 :>>> {money}')

def work2():
    global money
    temp = money
    print(f'work2 修改之前 :>>> {money}')

    money = temp + 2
    print(f'work2 修改之后:>>> {money}')

def create_process():
    print(f"修改之前 :>>>> {money}")
    task1 = Process(target=work1)
    task2 = Process(target=work2)
    task1.start()
    task2.start()
    task1.join()
    task2.join()
    print(f"修改之后 :>>>> {money}")

if __name__ == '__main__':
    create_process()

 

标签:__,money,print,28,互斥,线程,进程,ticket,多线程
From: https://www.cnblogs.com/hbutmeng/p/18425951

相关文章

  • 多线程之手撕生产者-消费者
    要点维护一个资源(在生产者-消费者中即流水线的位置)池,实现put()/get()两个函数。由于对信号量的操作是互斥的,要引入条件变量和信号量。实现资源池类Pool,成员变量:mtx:mutexcv:condition_variableque:queuecapacity:int实现资源池类Pool,成员函数:Tget():获取......
  • 828华为云征文|华为云 Flexus 云服务器 X 实例:在openEuler系统下搭建MySQL主从复制
    828华为云征文|华为云Flexus云服务器X实例:在openEuler系统下搭建MySQL主从复制前言一、Flexus云服务器X实例介绍1.1Flexus云服务器X实例简介1.2Flexus云服务器X实例特点1.3Flexus云服务器X实例使用场景二、MySQL数据库介绍2.1MySQL简介2.2MySQL主要特点2.3MySQ......
  • JAVA多线程
    一、并发和并行    并发:同一时刻,多个指令在单个CPU上交替执行。    并行:同一时刻,多个指令在多个CPU上同时执行。二、多线程的实现方式1.继承Thread类的方式进行实现。publicclassThreadDemo{publicstaticvoidmain(String[]args){MyT......
  • STM32 通过 SPI 驱动 W25Q128
    目录一、STM32SPI框图1、通讯引脚2、时钟控制3、数据控制逻辑4、整体控制逻辑5、主模式收发流程及事件说明如下:二、程序编写1、SPI初始化2、W25Q128驱动代码2.1读写厂商ID和设备ID2.2读数据2.3写使能/写禁止2.4读/写状态寄存器2.5擦除扇区2.6擦除整个芯片2......
  • 多线程问题:异常处理,单例
    1)多线程异常处理多线程中如何捕获抛出异常到主线程a)catch中使用std::current_exception();获得正在处理的异常b)通过引用类型std::exception_ptr&_ex_ptr传出c)std::rethrow_exception(ex_ptr);重新抛出异常usingnamespacestd; try{ std::exception_ptrex_ptr;......
  • Adobe Illustrator AI v28.6 解锁版下载及安装教程 (矢量图形设计软件)
    前言AdobeIllustrator(简称AI)专业矢量图形设计软件,矢量绘图设计工具,设计师常用的矢量绘制软件。该软件广泛应用于广告设计、印刷出版、海报书籍、插画绘制、图像处理、PDF文档设计、WEB页面等设计,借助这款矢量绘图工具,可以制作适用于印刷,Web,视频和移动设备的徽标、图标、绘图、......
  • 南沙C++信奥老师解一本通题 1281:最长上升子序列
    ​ 【题目描述】一个数的序列bibi,当b1<b2<...<bS的时候,我们称这个序列是上升的。对于给定的一个序列(a1,a2,...,aN),我们可以得到一些上升的子序列(ai1,ai2,...,aiK),这里1≤i1<i2<...<iK≤N。比如,对于序列(1,7,3,5,9,4,8),有它的一些上升子序列,如(1,7),(3,4,8)等等。这些子序列......
  • 2024金三银四苦刷算法28天!成功收获字节Offer
    **毕竟现在大厂里用的都是算法,所以这块内容不吃透肯定是不行的。**目录如下:图文并茂,附有刷题答案源码。第一份:LeetCode算法收割机=================由于篇幅原因,为了避免影响到大家的阅读体验,在此只以截图展示部分内容,详细完整版的看文末有免费的获取方式!部分目录展示:......
  • 面试:多线程顺序打印
            在多线程中有多种方法让线程按特定顺序执行,可以用线程类的join()方法在一个线程中启动另一个线程,另外一个线程完成该线程继续执行。比如说:使用join方法,T3调用T2,T2调用T1,这样就能确保T1就会先完成而T3最后完成。        定义三个类,分别实现Runnable接......
  • 28岁的程序员,如何转型做AI项目经理?
    最近收到很多网友发给我的私信,说自己在AI领域做了5-6年开发,现在想转型做AI项目经理,但不知道从何下手,有没有什么好的建议?今天,我就来聊一聊这个问题。1转型的可能性首先,需要明确的是,无论你是程序员、产品经理还是技术主管,其实都是可以往AI项目经理这个方向去转型。但是,这......