线程


一、互斥锁

from multiprocessing import Process
import time
import json
import random


# 查票
def search(name):
    with open(r'data.json', 'r', encoding='utf8') as f:
        data = json.load(f)
    print('%s在查票 当前余票为:%s' % (name, data.get('ticket_num')))


# 买票
def buy(name):
    # 再次确认票
    with open(r'data.json', 'r', encoding='utf8') as f:
        data = json.load(f)
    # 模拟网络延迟
    time.sleep(random.randint(1, 3))
    # 判断是否有票 有就买
    if data.get('ticket_num') > 0:
        data['ticket_num'] -= 1
        with open(r'data.json', 'w', encoding='utf8') as f:
            json.dump(data, f)
        print('%s买票成功' % name)
    else:
        print('%s很倒霉 没有抢到票' % name)


def run(name):
    search(name)
    buy(name)


if __name__ == '__main__':
    for i in range(5):
        p = Process(target=run, args=('用户%s'%i, ))
        p.start()
"""
用户0在查票 当前余票为:1
用户3在查票 当前余票为:1
用户1在查票 当前余票为:1
用户2在查票 当前余票为:1
用户4在查票 当前余票为:1
用户1买票成功
用户0买票成功
用户3买票成功
用户2买票成功
用户4买票成功
现在我们可以发现余票只有一张 但是所有人都抢到了 那肯定是不符合常理的
问题其实是出现在了模拟网络延迟
因为每个人所经历的网络延迟是正常的现实生活中每个人都有网络延迟
但是在代码中每个进程的的余票都是1的话 那么if判断都是True 那么都会抢到票

这个时候我们就要运用到互斥锁了
"""

1.互斥锁的运用

'''
多个程序同时操作数据很容易造成数据的错乱!!
为了避免错乱 我们应该使用互斥锁
'''

# 互斥锁其实就是将并发变成并行 虽然牺牲了效率但是提高了数据的安全性


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


# 查票
def search(name):
    with open(r'data.json', 'r', encoding='utf8') as f:
        data = json.load(f)
    print('%s在查票 当前余票为:%s' % (name, data.get('ticket_num')))


# 买票
def buy(name):
    # 再次确认票
    with open(r'data.json', 'r', encoding='utf8') as f:
        data = json.load(f)
    # 模拟网络延迟
    time.sleep(random.randint(1, 3))
    # 判断是否有票 有就买
    if data.get('ticket_num') > 0:
        data['ticket_num'] -= 1
        with open(r'data.json', 'w', encoding='utf8') as f:
            json.dump(data, f)
        print('%s买票成功' % name)
    else:
        print('%s很倒霉 没有抢到票' % name)


def run(name, metex):
    search(name)
    metex.acquire()  # 抢锁
    buy(name)
    metex.release()  # 释放锁


'''
其实就是在买票的函数前加入互斥锁 
当有一个进程抢到了这个锁那么其他进程就不能在抢了
只能等这个进程结束之后再去抢 
抢到了才能继续执行buy函数买票
'''

if __name__ == '__main__':
    metex = Lock()
    for i in range(5):
        p = Process(target=run, args=('用户%s' % i, metex))
        p.start()
"""
用户1在查票 当前余票为:1
用户0在查票 当前余票为:1
用户3在查票 当前余票为:1
用户2在查票 当前余票为:1
用户4在查票 当前余票为:1
用户1买票成功
用户0很倒霉 没有抢到票
用户3很倒霉 没有抢到票
用户2很倒霉 没有抢到票
用户4很倒霉 没有抢到票

虽然互斥锁很好用 但是互斥锁只应该出现在多个程序操作数据的地方 其他位置尽量不要使用

"""

二、线程理论

# 进程是资源单位
    # 进程相当于是一个车间 进程负责给内部的线程提供相应的资源
# 线程是执行单位
    # 现场相当于是车间里的流水线 线程负责执行真正的功能
# 其实就是可以理解为内存在内存空间开了一个空间给进程运行 而进程有自己的空间 线程在进程的空间下运行代码

'''
1.一个进程至少含有一个线程
2.多进程和多线程的区别
    多进程
        需要申请内存空间  需要拷贝全部代码 资源消耗大  因为一个进程就需要一个内存空间
    多线程
        不需要申请内存空间  不需要拷贝全部代码  资源消耗小  因为线程就是在进程中运行的
3.同一个进程下的多线程之间数据是共享的
'''

 

 

三、创建线程的两种方式

from threading import Thread
import time

# 跟进程一样代码创建按线程有两种方式:

# 1.
def task(name):
    print(f'{name}正在运行')
    time.sleep(3)
    print(f'{name}运行结束')


'''
因为开设多线程是不用拷贝代码的 所以无论什么系统都不会出现反复状况
也不需要启动脚本执行 但是为了兼容性 习惯在启动脚本中编写
'''

if __name__ == '__main__':
    t = Thread(target=task, args=('jason',))
    t.start()
    print('主线程')
'''
运行结果:
jason正在运行
主线程
jason运行结束

两个线程都是在一个进程下运行
可以理解为两个线程在一个进程下是并行运行的 但是线程之间的数据是共享的 因为线程是不需要复制代码的所以 运行的时候会直接运行子线程的代码 但是t.start()是异步操作 所以提交完任务之后会直接运行接下来的代码 所以函数运行的时候主线程也在运行
''' # 2. class MyThread(Thread): def __init__(self, name): super().__init__() self.name = name def run(self): print(f'{self.name}正在运行') time.sleep(3) print(f'{self.name}执行结束') if __name__ == '__main__': t = MyThread('jason') t.start() print('主线程') ''' jason正在运行 主线程 jason执行结束 '''

 

四、多线程实现TCP服务端并发

# 比多进程更简单方便 消耗资源更少 因为线程都是在一个进程下运行的

# 服务端
import socket
from threading import Thread

server = socket.socket()

server.bind(('127.0.0.1', 8080))
server.listen(5)


def task(sock):
    while True:
        data = sock.recv(1024)
        print(data.decode('utf8'))
        sock.send(data.upper())


while True:
    sock, address = server.accept()
    t = Thread(target=task, args=(sock, ))
    t.start()


# 客户端
import socket

client = socket.socket()

client.connect(('127.0.0.1', 8080))

while True:
    client.send(b'hello baby')
    data = client.recv(1024)
    print(data.decode('utf8'))

五、join方法

# 线程的join方法其实跟进程的方法是一样的都是把start从异步操作变成同步操作
from threading import Thread
import time

def task(name):
    print(f'{name}正在运行')
    time.sleep(3)
    print(f'{name}结束运行')


if __name__ == '__main__':
    t = Thread(target=task, args=('jason', ))
    t.start()
    print('主线程')
"""
jason正在运行
主线程
jason结束运行

如果不加join方法那么start就是一个异步操作 
那么start在提交完任务之后
就会直接运行接下来的代码
所以子线程在执行代码的时候 主线程也在执行代码

如果我们加上join方法的话让start变成同步操作
"""

def task(name):
    print(f'{name}正在运行')
    time.sleep(3)
    print(f'{name}结束运行')


if __name__ == '__main__':
    t = Thread(target=task, args=('jason', ))
    t.start()
    t.join()
    print('主线程')
"""
jason正在运行
jason结束运行
主线程

如果加上了join方法那么start就会变成同步操作 
所以主线程就会等子线程运行结束的时候
才会执行主线程
"""

 

 

六、同一个进程下线程的数据共享

from threading import Thread

money = 100


def task():
    global money
    money = 666


if __name__ == '__main__':
    t = Thread(target=task)
    t.start()
    print(f'子线程的money:{money}')  # 子线程的money:666
    money = 120
    print(f'子线程的money:{money}')  # 子线程的money:120

print(money)  # 120
'''
因为同一个进程下所有的线程的数据都是共享的所以
不管是在主线程下修改数据的值 还是子线程下修改数据值 
都是能够修改全局变量的值的
'''

七、线程对象的相关方法

1.线程的名称

from threading import Thread, current_thread
import time


def task(name):
    print(f'{name}正在被运行')
    time.sleep(2)
    print(f'{name}终止运行')


if __name__ == '__main__':
    t = Thread(target=task, args=('jason',))
    t.start()
    print(current_thread().name)  # MainThread



def task(name):
    print(f'{name}正在被运行')
    time.sleep(2)
    print(f'{name}终止运行')
    print('子线程的名字:%s' % current_thread().name)  # 子线程的名字:Thread-8


if __name__ == '__main__':
    for i in range(10):
        t = Thread(target=task, args=('用户%s' % i,))
        t.start()

# 主线程的名字是:MainThread  子线程的名字: Thread-n

2.线程的进程号

# 同一个进程下的多个线程的进程号是同一个

from threading import Thread, current_thread
import time
import os

def task(name):
    print(f'{name}正在被运行')
    time.sleep(2)
    print(f'{name}终止运行')
    print('子:', os.getpid())  # 子: 14608


if __name__ == '__main__':
    for i in range(10):
        t = Thread(target=task, args=('用户%s' % i,))
        t.start()
    print('', os.getpid())  # 主 14608

# 我们可以看到不管是主线程还是子线程 的进程号都是一样的 因为他们都是同一个进程下运行

3.active_count()

# 计算线程的数量

from threading import Thread, active_count
import time


def task(name):
    print(f'{name}正在被运行')
    time.sleep(2)
    print(f'{name}终止运行')


if __name__ == '__main__':
    for i in range(10):
        t = Thread(target=task, args=('用户%s' % i,))
        t.start()
    print('线程的个数:', active_count())  # 线程的个数: 11

# 为什么我只产生了是个线程 但是返回的结果是11呢?
    因为它本身的主线程也要算一个线程

八、守护线程

from threading import Thread
import time


def task(name):
    print(f'{name}正在执行')
    time.sleep(3)
    print(f'{name}结束运行')


if __name__ == '__main__':
    t = Thread(target=task, args=('jason',))
    t.start()
    print('主线程')

'''
当子线程不是守护线程时 主线程和子线程哥执行各的
jason正在执行
主线程
jason结束运行
'''
# 守护线程跟守护进程的概念时一样的
def task(name):
    print(f'{name}正在执行')
    time.sleep(3)
    print(f'{name}结束运行')


if __name__ == '__main__':
    t = Thread(target=task, args=('jason',))
    t.daemon = True
    t.start()
    print('主线程')
'''
jason正在执行
主线程

但是当子线程是主线程的守护线程时 那么主线程执行结束的时候子线程就会立马结束运行
'''

九、GIL解释器锁

'''
储备知识
    1.python解释器也是由编程语言写出来的
        Cpython  用C写出来的
         Jpython  用Java写出来的
        Pypython 用python写出来的
ps:最常用的就是Cpython(默认)
'''
'''
1.GIL的研究是Cpython解释器的特点 不是python语言的特点
2.GIL本质也是一把互斥锁
3.GIL的存在使得同一个进程下的多个线程无法同时执行(关键)
    言外之意:单进程下的多线程无法利用多核优势 效率低!!!
4.GIL的存在主要是因为cpython解释器中垃圾回收机制不是线程安全的
'''
'''
其实我的理解就是 
当我们运行一个进程的时候如果有多个线程运行 
然后我们可以把垃圾回收机制看成是一个线程
每个进程中都有一个垃圾回收机制
然后在同一个进程中多线程的数据共享的

如果我们这个时候定义了一个变量
然后在变量值和变量名绑定之前 
如果垃圾回收机制检测到这个变量值没有被绑定(就是没有被引用)
就会被垃圾回收机制回收 这样的话那我们定义了变量跟没有定义一样
其他线程都用不了这个变量了
所以我们需要一个GIl解释器锁
'''
"""
每个线程运行的时候都需要一个解释器帮忙运行
那么现在我们只需要用GIL解释器锁把python解释器锁上
然后让每个线程来抢这个锁 谁抢到就运行谁
就算被垃圾回收机制抢到也没有关系 因为垃圾回收之后 就会释放锁
然后每个线程继续抢 谁抢到谁就运行谁
这样就能确保了每个线程的数据安全了

而线程怎么释放锁呢?
线程代码运行结束就会释放锁
线程遇到IO操作也会释放锁
"""
'''
而既然有了GIL解释器锁了为什么还需要互斥锁呢?
因为GIL解释器锁只是针对垃圾回收机制的
为了不让整个python解释器这个进程的数据不错乱
而向我们之前的抢票系统的互斥锁就算是让GIL锁住买票的数据
但是我们在买票前是有个模拟网络延迟的 
一当进入到网络延迟那么就会进入IO操作 这样GIL的锁就会释放让别的线程继续抢
这样对于GIL解释器锁由它没它都一样了 因为进入IO操作让其他线程抢到那么其他线程拿到的余票也相当于是1了
这样也就都可以买票了 那数据又会错乱了
所以我们需要用到别的互斥锁
而我们用到的互斥锁相当与专门作用与数据上 
而GIL解释器只是让整个python解释器这个进程的数据不会错乱 针对进程中的线程的数据它是不管的

所以python解释器在一个进程下的多个线程同一时间只能运行一个 所以无法利用多核优势
'''

 

posted @ 2022-08-10 22:16  stephen_hao  阅读(39)  评论(0编辑  收藏  举报