当前位置 : 主页 > 编程语言 > 其它开发 >

线程

来源:互联网 收集:自由互联 发布时间:2022-05-30
目录 GIL与普通互斥锁区别 验证多线程是否有用 死锁现象 信号量 event事件 进程池与线程池 协程 基于协程实现TCP服务端并发 内容GIL与普通互斥锁的区别先验证GIL的存在 from threading imp
目录
  • GIL与普通互斥锁区别
  • 验证多线程是否有用
  • 死锁现象
  • 信号量
  • event事件
  • 进程池与线程池
  • 协程
  • 基于协程实现TCP服务端并发
内容 GIL与普通互斥锁的区别 先验证GIL的存在
from threading import Thread,Lock
import time

money = 100
def task():
    global money
    money -= 1
for i in range(100): # 创建一百个进程
    t = Thread(target=task)
    t.start()
print(money)
View Code

结果是:0

但是基于网络都会有延迟的,所以添加了time模块之后:
from threading import Thread,Lock
import time

money = 100
mutex = Lock()

def task():
    global money
    tmp = money
    time.sleep(0.1)
    money = tmp - 1
t_list = []
for i in range(100): # 创建一百个进程
    t = Thread(target=task)
    t.start()
    t_list.append(t)
for t in t_list:
    t.join()
"""为了确保结构正确,应该等待所有的线程运行完毕再打印money"""
print(money)
View Code

结果是:99

那么如何保证数据安全,就需要自己加锁,这时候GIL的存在就无所谓了

再验证不同数据加不同锁
from threading import Thread,Lock
import time

money = 100
mutex = Lock()

def task():
    global money
    mutex.acquire() # 抢锁
    tmp = money
    time.sleep(0.1)
    money = tmp - 1
    mutex.release() # 放锁
t_list = []
for i in range(100): # 创建一百个进程
    t = Thread(target=task)
    t.start()
    t_list.append(t)
for t in t_list:
    t.join()
"""为了确保结构正确,应该等待所有的线程运行完毕再打印money"""
print(money)
View Code

结果是:0

虽然GIL是一把互斥锁,但是不会保证业务逻辑的安全,尤其是有网络延迟的情况下

针对不同的数据要加不同的锁,甚至正常的业务逻辑都不考虑GIL,所以我们想要保证数据的安全应该自己自定义互斥锁(使用别人封装好的工具)

抢锁放锁也有简便写法:with上下文管理

总结:GIL是一个纯理论知识,在实际工作中根本不需要考虑它的存在

验证多线程作用

CPU个数:多个;单个

任务的类型:IO密集型(经常需要进行IO操作,需要把CPU拿走);计算密集型(始终占着CPU)

单个CPU

多个IO密集型任务

多进程:浪费资源,无法利用多个CPU

多线程:节省资源,切换+保存状态

多个计算密集型任务

多进程:耗时更长,创建进程的消耗+切换消耗

多线程:耗时更短,切换消耗

多个CPU

多个IO密集型任务

多进程:浪费资源,多个CPU无用武之地

多线程:节省资源,切换+保存状态

多个计算密集型任务

多进程:利用多核,速度更快

多线程:速度较慢

结论:多进程和多线程都有具体的应用场景,尤其是多线程并不是没有用

代码验证

计算密集型

多进程:

from threading import Thread
from multiprocessing import Process
import os
import time

def work(): # 计算密集型任务
    res = 1
    for i in range(1,1000000000000):
        res *= 1

if __name__ == '__main__':
    # print(os.cpu_count()) # 查看当前计算机有几个CPU
    start_time = time.time()
    p_list = [] # 等8个进程结束之后拿上面计算的结果
    for i in range(8):
        p = Process(target=work)
        p.start()
    for p in p_list:
        p.join()
    print('总耗时:%s'%(time.time() - start_time))
View Code

总耗时:0.08106851577758789

多线程:

threading import Thread
from multiprocessing import Process
import os
import time

def work(): # 计算密集型任务
    res = 1
    for i in range(1,1000000000):
        res *= 1

if __name__ == '__main__':
    # print(os.cpu_count()) # 查看当前计算机有几个CPU
    start_time = time.time()
    p_list = [] # 等8个进程结束之后拿上面计算的结果
    for i in range(8):
        p = Thread(target=work)
        p.start()
        p_list.append(p)
    for p in p_list:
        p.join()
    print('总耗时:%s'%(time.time() - start_time))
View Code

总耗时:0.28725099563598633

结果:多进程更好

IO密集型

多线程:

from threading import Thread
from multiprocessing import Process
import time

def work():
    time.sleep(1)

if __name__ == '__main__':
    start_time = time.time()
    t_list = []
    for i in range(100):
        t = Thread(target=work)
        t.start()
    for t in t_list:
        t.join()
    print('总耗时:%s'%(time.time() - start_time))
View Code

总耗时:0.01210331916809082

多进程:

from threading import Thread
from multiprocessing import Process
import time

def work():
    time.sleep(1)

if __name__ == '__main__':
    start_time = time.time()
    t_list = []
    for i in range(100):
        t = Process(target=work)
        t.start()
    for t in t_list:
        t.join()
    print('总耗时:%s'%(time.time() - start_time))
View Code

总耗时:2.0086660385131836

结果:多线程更好

死锁现象

锁就算掌握了如何抢锁,放锁,也会产生死锁现象

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

# 产生两把锁
mutexA = Lock()
mutexB = Lock()

class MyThread(Thread):
    def run(self):
        self.f()
        self.f2()
    def f(self):
        mutexA.acquire()
        print(f'{self.name}抢到了A锁')
        mutexB.acquire()
        print(f'{self.name}抢到了B锁')
        mutexB.release()
        mutexA.release()
    def f2(self):
        mutexB.acquire()
        print(f'{self.name}抢到了B锁')
        time.sleep(2)
        mutexA.acquire()
        print(f'{self.name}抢到了A锁')
        mutexA.release()
        mutexB.release()

for i in range(20):
    t = MyThread()
    t.start()
View Code

锁不能轻易使用并且以后我们也不会自己去处理锁,都是使用别人封装好的工具

信号量

信号量在不同的知识体系中,展示出来的功能是不一样的

在并发编程中信号量是多把互斥锁

在django框架中信号量意思是达到某个条件自动触发特定功能

from threading import Thread,Semaphore
import time
import random

sp = Semaphore(5) # 创建一个有5个位置(带门的)的公共空间
def task(name):
    sp.acquire()
    print('%s正在占位置'%name)
    time.sleep(random.randint(1,5))
    sp.release()

for i in range(1,21):
    t = Thread(target=task,args=('伞兵%s号'%i,))
    t.start()
View Code

结果为:

伞兵1号正在占位置
伞兵2号正在占位置
伞兵3号正在占位置
伞兵4号正在占位置
伞兵5号正在占位置 # 瞬间来了五个人,占满五个位置

# 时间间隔


伞兵6号正在占位置
伞兵7号正在占位置伞兵8号正在占位置

伞兵9号正在占位置
伞兵10号正在占位置伞兵11号正在占位置

伞兵12号正在占位置
伞兵13号正在占位置
伞兵14号正在占位置
伞兵15号正在占位置
伞兵16号正在占位置
伞兵17号正在占位置
伞兵18号正在占位置
伞兵19号正在占位置
伞兵20号正在占位置

只要是跟锁相关的几乎不会让我们自己去写,后期还是使用模块

event事件 子进程运行可以取决于另外的子进程
from threading import Thread,Event
import time

event = Event() # 类似于红绿灯

def light():
    print('红灯停')
    time.sleep(3)
    print('绿灯行')
    event.set() # 让绿灯亮起来

def car(name):
    print('%s正在等红灯'%name)
    event.wait() # 等待发信号
    print('%s绿灯冲'%name)

t = Thread(target=light)
t.start()
for i in range(20): # 看成是一瞬间启动了20个线程
    t = Thread(target=car,args=('大奔%s'%i,))
    t.start()
View Code

结果为:

红灯停
大奔0正在等红灯
大奔1正在等红灯
大奔2正在等红灯
大奔3正在等红灯
大奔4正在等红灯
大奔5正在等红灯
大奔6正在等红灯
大奔7正在等红灯
大奔8正在等红灯
大奔9正在等红灯
大奔10正在等红灯
大奔11正在等红灯
大奔12正在等红灯
大奔13正在等红灯
大奔14正在等红灯
大奔15正在等红灯
大奔16正在等红灯
大奔17正在等红灯
大奔18正在等红灯
大奔19正在等红灯
绿灯行
大奔0绿灯冲大奔1绿灯冲
大奔5绿灯冲
大奔6绿灯冲大奔8绿灯冲
大奔10绿灯冲
大奔14绿灯冲
大奔18绿灯冲

大奔4绿灯冲
大奔3绿灯冲
大奔11绿灯冲
大奔12绿灯冲
大奔17绿灯冲
大奔19绿灯冲
大奔9绿灯冲
大奔16绿灯冲
大奔13绿灯冲
大奔15绿灯冲
大奔2绿灯冲

大奔7绿灯冲

这种效果其实也可以通过其他手段实现,比如队列,只不过没有event简便

进程池与线程池 TCP服务端实现并发
  1. 多进程:来一个客户端就开设一个进程
  2. 多线程:来一个客户端就开设一个线程
服务端必备的三要素
  1. 24小时不间断提供服务
  2. 固定的ip和port
  3. 支持高并发

问题:计算机硬件有物理极限,不可能无限制创建进程和线程,如何解决

措施:;保证计算机硬件安全的情况下提升程序的运行效率

进程池:提前创建好固定数量的进程,后续反复使用这些进程

线程池:提前创建好固定数量的线程,后续反复使用这些线程

如果任务超出了池子里面的最大进程或者进程数,则原地等待

进程池和线程池其实降低了程序的运行效率,但是保证了硬件的安全

代码演示

线程池:

线程池线程数:max_workers = min(32, (os.cpu_count() or 1) + 4)

线程池线程数默认是CPU个数的五倍,也可以自定义

from concurrent.futures import ProcessPoolExecutor,ThreadPoolExecutor
import time
from threading import current_thread

pool = ThreadPoolExecutor(5) # 线程池线程数默认是CPU个数的五倍,也可以自定义
"""创建5个等待工作的线程"""

def task(n):
    time.sleep(2)
    print(n)
    # print(current_thread().name)
    return 'hello' # 获取人物的返回值,否则就是None

for i in range(20):
    res = pool.submit(task,i) # 朝线程池中提交任务(异步提交)
    print(res.result()) # 获取当前异步提交任务的结果,但这是一个同步提交(打印结果会很慢)
View Code

由于.result()方法是同步提交,导致打印结果很慢

任务的提交方式:

同步:提交任务之后原地等待任务结果

异步:提交任务之后不原地等待任务结果,结果通过反馈机制自动获取

基于上述代码,不应该自己主动等待结果,应该让异步提交自动提醒>>>:异步回调机制(反馈机制)

from concurrent.futures import ProcessPoolExecutor,ThreadPoolExecutor
import time
from threading import current_thread

pool = ThreadPoolExecutor(5) # 线程池线程数默认是CPU个数的五倍,也可以自定义
"""创建5个等待工作的线程"""

def task(n):
    time.sleep(2)
    print(n)
    # print(current_thread().name)
    return 'hello' # 获取人物的返回值,否则就是None

def func(*args,**kwargs):
    print(args,kwargs)

for i in range(20):
    pool.submit(task,i).add_done_callback(func) # 括号随便写一个函数
View Code

add_done_callback只要任务有结果了,就会自动调用括号内的函数处理

处理拿到的是对象,只要在add_done_callbac()括号内的函数里面使用.result()即可

from concurrent.futures import ProcessPoolExecutor,ThreadPoolExecutor
import time
from threading import current_thread

pool = ThreadPoolExecutor(5) # 线程池线程数默认是CPU个数的五倍,也可以自定义
"""创建5个等待工作的线程"""

def task(n):
    time.sleep(2)
    print(n)
    # print(current_thread().name)
    return '任务的执行结果:%s'% (n+2) # 获取任务的返回值,否则就是None

def func(*args,**kwargs):
    # print(args,kwargs)
    print(args[0].result())

for i in range(20):
    pool.submit(task,i).add_done_callback(func) # 括号随便写一个函数

进程池:

进程池进程数:默认是CPU个数

if max_workers is None:

         self._max_workers = os.cpu_count() or 1

from concurrent.futures import ProcessPoolExecutor,ThreadPoolExecutor
import time
from multiprocessing import Process
import os

pool = ProcessPoolExecutor(5)

def task(n):
    time.sleep(2)
    print(n)
    print(os.getpid())
    # print(current_thread().name)
    return '任务的执行结果:%s'% (n**2) # 获取任务的返回值,否则就是None

def func(*args,**kwargs):
    # print(args,kwargs)
    print(args[0].result())

if __name__ == '__main__':
    for i in range(20):
         pool.submit(task,i).add_done_callback(func)

通过获取进程号查看只有五个进程号,与ProcessPoolExecutor(5)一致

协程

进程:资源单位;线程:执行单位;协程:单线程下实现并发

并发:切换+保存状态

协程:能够在多个任务之间切换+保存状态来节省IO操作,完全是程序员自己意淫出来的名词

对于操作系统而言只认识进程和线程,协程就是自己通过代码来检测程序的IO操作并自己处理,让CPU感觉不到IO的存在从而最大幅度的占用CPU

保存数据的功能,我们接触过yield,但是无法检测到IO操作切换

from gevent import spawn
from gevent import monkey,monkey.patch_call() # 固定操作,用于检测IO操作

import time

def play(name):
    print('%s play 1'%name)
    time.sleep(5)
    print('%s play 2'%name)
def eat(name):
    print('%s eat 1'%name)
    time.sleep(3)
    print('%s eat 2'%name)

start_time = time.time()
g1 = spawn(play,'zhou')
g2 = spawn(eat,'zhou')
g1.join() # 等待检测任务执行完毕
g2.join()
print('总耗时:',time.time() - start_time)
View Code 基于协程实现TCP并发

服务端:

from gevent import monkey;monkey.patch_all()
from gevent import spawn
import socket

def conmunication(sock):
    while True:
        data = sock.recv(1024) # IO操作
        print(data.decode('utf8'))
        sock.send(data.upper())

def get_server():
    server = socket.socket()
    server.bind(('127.0.0.1',9999))
    server.listen(5)
    while True:
        sock, add = server.accept()  # IO操作
        spawn(conmunication,sock)

g1 = spawn(get_server)
g1.join()
View Code

客户端:

from threading import Thread,current_thread
import socket

def get_client():
    client = socket.socket()
    client.connect(('127.0.0.1',9999))
    count = 0
    while True:
        msg = '%s say hello %s'%(current_thread().name,count)
        count += 1
        client.send(msg.encode('utf8'))
        data = client.recv(1024)
        print(data.decode('utf8'))

for i in range(200):
    t = Thread(target=get_client)
    t.start()
View Code

spawn消耗资源,对CPU有一定的负荷

总结:

python可以通过开设多进程,在多进程下开设多线程,在多线程使用协程,从而让程序执行的效率达到极致

但是实际业务中很少需要如此之高的效率(一直占着CPU),因为大部分程序都是IO密集型

所有协程知道其存在即可,几乎不会真正自己去编写

网友评论