线程 开启线程 开启多线程 线程类的参数和方法属性 守护线程 进程与线程的区别 GIL全局解释器锁 同步锁(互斥锁)

发布时间 2023-03-31 09:46:46作者: DRAMA-娜娜

今日内容概要

  • 线程,如何开启线程
  • 如何开启多线程
  • 线程类的参数和方法属性
  • 守护线程
  • 进程与线程的区别
  • GIL全局解释器锁
  • 同步锁(互斥锁)

今日内容详细

线程

概要

1.怎样理解线程和进程
    1.1 进程:进程是资源单位,进程相当于是车间,负责给内部的线程提供相应的资源
    1.2 线程: 线程是执行单位,线程相当于是车间里面的流水线,负责执行真正的功能
        ps.进程是做事的过程,它不会去实际的做事情,是由线程来做事情的
2.线程与进程关系:
	2.1.在进程里面开启线程,在进程里面可以开启多个线程
	2.2.进程是资源分配的基本单位,线程是CPU调度的最小单位
		ps:每一个进程里至少有一个线程
			当进程中只有一个线程,这个线程叫做主线程
	2.3.系统来调度进程和线程,程序员级别是不能调度他们的
		协程:是由程序员级别来调度的
	2.4.进程占用的资源是最多的,其次线程占用的资源,最后是协程
		ps:在python中,我们一般开多进程,而不开多线程,其他语言都是选择开多线程
3.同一个进程下,多线程数据是共享的,多进程下的不同线程要同信,只需要进程间通信就行了
4.多进程与多线程的区别
    4.1 多进程:需要申请内存空间,并且需要拷贝全部代码,资源消耗大
    4.2 多线程:不需要申请内存空间,也不需要拷贝全部代码,并且资源消耗小

如何开启线程

开设线程不需要完整拷贝代码,所以无论什么系统都不会出现反复操作的情况,也不需要在启动脚本中执行,但是为了兼容性和统一性,习惯在启动脚本中编写

1.线程类:Thread
2.步骤:
	2.1 导模块:from threading import Thread
	2.2 开线程:t = Thread()
				t.start()
		ps:在windows,开线程不需要写在启动脚本中,为了统一我们尽量写一下

代码

from threading import Thread

def task():
    print('hello')

if __name__ == '__main__':
    t  = Thread(target=task)
    t.start()
    print('主线程')
"""
hello
主线程
"""

开启多线程

1.
    from threading import Thread
    import time

    def task(i):

        print('子线程',i)

    if __name__ == '__main__':
        for i in range(1,5):
            t  = Thread(target=task,args=(i,))
            t.start()
        print('主线程')
    """
    子线程 1
    子线程 2
    子线程 3
    子线程 4
    主线程
    """

2.
    from threading import Thread
    import time

    def task(i):
        time.sleep(1)
        print('子线程',i)

    if __name__ == '__main__':
        for i in range(1,5):
            t  = Thread(target=task,args=(i,))
            t.start()
        print('主线程')
    """
    主线程
    子线程 2
    子线程 1
    子线程 4
    子线程 3
    """
  ps:线程占用的资源特别少,在等待1秒钟,在执行主线程代码(异步)  

多线程实现TCP服务端开发

服务端:
    import socket
    from threading import Thread
    server = socket.socket()
    server.bind(('127.0.0.1',8080))
    server.listen(5)

    def run(sock):
        while  True:
            try:
                data = sock.recv(1024)
                print(data.decode('utf8'))
                sock.send(data.upper())
            except ConnectionRefusedError as e:
                sock.close()
                break

    while  True:
        sock,addr = server.accept()
        p =  Thread(target=run,args=(sock,))
        p.start()
    
客服端:
    import socket

    client = socket.socket()
    client.connect(('127.0.0.1',8080))
    while True:
        client.send('hello'.encode('utf8'))
        data = client.recv(1024)
        print(data.decode('utf8'))

线程类的参数和方法

参数

1.Thread(group=None, target=None, name=None,args=(), kwargs=None, *, daemon=None) 默认值参数
	1.1 name = None  t.name  线程名
	1.2 args=()   kwargs = () 用于传参
    
2.守护线程
    daemon = None   默认 
    daemon = True  守护线程(主线程执行完毕,子线程立马结束)
    ps:daemon = True 写在t.start() 上面
    daemon = True  ====>  t.setDaemon
	注意:进程下所有的非守护线程结束,主线程(主进程)才能真正的结束
    ps:
        from threading import Thread

        def task(a,name):
            print(a,name)

        if __name__ == '__main__':
            t  = Thread(target=task,args=(1,),kwargs={'name':'nana'})
            daemon = True
            t.start()
            print('主线程')
        """
        1 nana
        主线程
        """

方法

1.t.join()使主线程等到子线程运行结束之后再运行
2.t.is_alive() 线程是否存活
3.t.getName()  线程名
4.t.setName()  设置线程名
5.threding.currentThread() 返回当前的线程变量
6.threding.currentThread().getName 返回当前的线程变量的名字
7.threding. active_count()查看进程下的线程数

进程和线程的比较

1.pid的比较

1.进程:开多个进程,每个进程都有不同的pid
    from multiprocessing import Process
    import os
    def task():
        pass

    if __name__ == '__main__':
        p = Process(target=task)
        p.start()
        print('子进程:',p.pid)
        print('主进程:',os.getpid())
    """
    子进程: 13672
    主进程: 6768
    """
    
2.线程:在主进程下开启多个线程,每个线程都跟主进程的pid一样
    from threading import Thread
    import os

    def task():
        print('子进程',os.getpid())

    if __name__ == '__main__':
        for i in range(1,4):
            t = Thread(target=task)
            t.start()
        print('主线程/主进程:',os.getpid())
    """
    子进程 9840
    子进程 9840
    子进程 9840
    主线程/主进程: 9840
    """

2.开启效率的较量

进程的开销要远远大于线程的开销

1.进程
    from multiprocessing import Process
    import time
    def task(i):
        time.sleep(1)
        print('子进程',i)

    if __name__ == '__main__':
        l = []
        ctime = time.time()
        for i in range(1,4):
            p = Process(target=task,args=(i,))
            p.start()
            l.append(p)
        for j in l:
            j.join()
        print('进程运行时间:',time.time()-ctime)

    """
    子进程 1
    子进程 2
    子进程 3
    进程运行时间: 1.8508541584014893
    """
2.线程
    from threading import Thread
    import time

    def task(i):
        time.sleep(1)
        print('子进程',i)

    if __name__ == '__main__':
        l = []
        ctime = time.time()
        for i in range(1,4):
            t = Thread(target=task,args=(i,))
            t.start()
            l.append(t)
        for j in l:
            j.join()
        print('主线程运行时间:',time.time()-ctime)
    """
    子进程 1
    子进程 3
    子进程 2
    主线程运行时间: 1.0114858150482178
    """

3.内存数据的共享问题

1.进程:进程之间的数据是隔离的
from multiprocessing import Process

n = 100

def task():
    global n
    n = 10

if __name__ == '__main__':
    p = Process(target=task)
    p.start()
    print('主进程n:',n)  # 主进程n: 100
    
2.线程:线程之间的数据是相互通信的,严格来说,是同一个进程下的所有线程数据共享
    from threading import Thread

    n = 100

    def task():
        global n
        n = 10

    if __name__ == '__main__':
        t = Thread(target=task)
        t.start()
        print('主线程:',n)  # 主线程: 10
ps:不同进程下的线程之间数据是不共享的--->如何让不同进程下的线程数据之间数据共享---->其实就是进程间通信

GIL全局解释器锁((Global Interpreter Lock) )

image

1.GIL全局解释器锁定义
	官方解释:
    In CPython, the global interpreter lock, or GIL, is a mutex that prevents multiple native threads from executing Python bytecodes at once. This lock is necessary mainly  because CPython’s memory management is not thread-safe. (However, since the GIL exists, other features have grown to depend on the guarantees that it enforces.)
    
	解读:
    1.GIL的研究是Cpython解释器的特点,不是python语言的特点
    2.GIL本质也是一把互斥锁
    3.GIL的存在使得同一个进程下的多个线程不能同时执行(单进程下的多进程无法利用多核优势,使效率低)
    4.GIL的存在主要是因为cpthon解释器中垃圾回收机制不是线程安全的
  
	结论:
    在Cpython解释器中,同一个进程下开启的多线程,同一时刻只能有一个线程执行,无法利用多核优势
    
2.	
2.1 python代码运行在解释器上,由解释器来翻译执行
2.2 python解释器的种类有哪些?
	CPython IPython PyPy IronPython... 
    python解释器是由编程语言写出来的,Cpython:是用C语言写出来的,
	Jpthon:是用java语言写出来的,Pypython:是用python语言写出来的
    市面上目前绝大多数都是用的是CPython解释器
2.3.GIL锁存在与Cpyhton解释器中,在其他解释器中不存在
2.4 起一个垃圾回收的线程,起一个正常执行代码的线程,当垃圾回收线程还没有把垃圾回收完毕的时候,会出现抢占资源的情况。
	"""
	Python解释器帮你自动定期进行内存回收,你可以理解为python解释器里有一个独立的线程,每过一段时间它起wake up做一次全局轮询看看哪些内存数据是可以被清空的,此时你自己的程序里的线程和py解释器自己的线程是并发运行的,假设你的线程删除了一个变量,py解释器的垃圾回收线程在清空这个变量的过程中的clearing时刻,可能一个其它线程正好又重新给这个还没来及得清空的内存空间赋值了,结果就有可能新赋值的数据被删除了,为了解决类似的问题,python解释器简单粗暴的加了锁,即当一个线程运行时,其它人都不能动,这样就解决了上述的问题,  这可以说是Python早期版本的遗留问题
	"""
2.5.python设计之初,就在python解释器上加了一把锁(GIL锁)
	我们就需要拿到GIL锁,要想让线程能够正常执行,那么,线程就必须要拿到这把锁(GIL锁)
	加这个锁的目的:同一时刻只能有一个线程执行,不能同时有多个线程执行,如果开了多个线程,那么,线程想要有执行权限,必须先拿到这把锁(GIL锁)
    
    总结:统一时刻,多个线程只能有一个线程在运行,其他线程都处于等待状态
        
3.理解记忆
3.1. python有GIL锁的原因:同一个进程下多个线程实际上同一时刻,只有一个线程在执行
3.2. 只有在python上开进程用的多,其他语言一般不开多进程,只开多线程就够了
3.3. cpython解释器开多线程不能利用多核优势,只有开多进程才能利用多核优势,其他语言不存在这个问题
3.4. 8核cpu电脑,充分利用起我这个8核,至少起8个线程,8条线程全是计算--->计算机cpu使用率是100%,
3.5. 如果不存在GIL锁,一个进程下,开启8个线程,它就能够充分利用cpu资源,跑满cpu
3.6 cpython解释器中好多代码,模块都是基于GIL锁机制写起来的,改不了了,(我们不能有8个核,但我现在只能用1核)可以开启多进程,再在每个进程下开启的线程,可以被多个cpu调度执行
3.7. cpython解释器:io密集型使用多线程,计算密集型使用多进程
	1.io密集型,遇到io操作会切换cpu,假设你开了8个线程,8个线程都有io操作,但io操作不消耗cpu,一段时间内看上去,其实8个线程都执行了
	2.计算密集型,消耗cpu,如果开了8个线程,第一个线程会一直占着cpu,而不会调度到其他线程执行,其他7个线程根本没执行,所以我们开8个进程,每个进程有一个线程,8个进程下的线程会被8个cpu执行,从而效率高
3.8 进程:可以利用多核
	线程:没办法利用多核   
网址:https://www.yuque.com/liyangqit/lb35ya/htscyi#a865f478

GIL的理解

1.判断:python的多线程就是垃圾,利用不到多核优势
    错误,python的多线程确实无法使用多核优势,但是在IO密集的任务下是有用的
2.判断:既然有GIL,那么以后我们写代码就不需要加互斥锁
    错误,GIL只确保解释器层面数据不会错乱(垃圾回收机制),针对程序中自己的数据应该自己加锁处理
3.所有的解释型编程语言都没办法做到同一个进程下多个线程同时执行
ps:我们平时写代码的时候,不需要考虑GIL

验证GIL的存在

GIL的存在使得同一个进程下的多个线程无法同时执行

from threading import Thread

money = 100
def task():
    global money
    money -= 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()
print(money)  # 0

ps:加入t_list列表添加线程对象,是为了保证所有的线程执行完了之后,才执行查看最后的money

验证GIL的特点

GIL不会影响程序层面的数据,也不会保证它的修改是安全的,要想保证自己程序的数据安全,得自己加锁

from threading import Thread
import time

money = 100
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()
    
print(money)  # 99
from threading import Thread,Lock
import time

money = 100
mutex = Lock()

def task():
    mutex.acquire()
    global money
    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()
    
print(money)  # 0

验证python多线程是否有用

python的多线程确实无法使用多核优势 但是在IO密集型的任务下是有用的

验证python多线程是否有用,需要分两种情况:第一种:是单个cpu还是多个CPU;第二种:是IO密集型(代码有IO操作),计算密集型(代码没有IO操作);根据这两种情况,可得出以下结论

1.单个CPU
    1.1 IO密集型:多线程有优势
        多进程:申请额外的空间,消耗更多的资源
        多线程:消耗资源相对较少,可以通过多道技术,提升CPU的效率
    1.2 计算密集型:多线程有优势
        多进程:申请额外的空间,消耗更多的资源(总耗时+申请空间+拷贝代码+切换)
        多线程:消耗资源相对较少,通过多道技术(总耗时+切换)

2.多个CPU
    2.1 IO密集型:多线程有优势
        多进程:总耗时(单个进程耗时+IO操作+申请空间+拷贝代码)
        多线程:总耗时(单个进程的耗时+IO)
    2.2 计算密集型:多进程有优势
        多进程:总耗时(单个进程耗时+IO操作+申请空间+拷贝代码)
        多线程:总耗时(多个进程的综合)

验证:在多个CPU且IO密集型的情况

多进程:
from multiprocessing import Process
import time

def work():
    time.sleep(2)  # 模拟纯IO操作
if __name__ == '__main__':
    start_time = time.time()
    p_list = []
    for i in range(100):
        p = Process(target=work)
        p.start()
        p_list.append(p)
    for p in p_list:
        p.join()
    print('总耗时:%s'%(time.time()-start_time))  # 总耗时:5.521599054336548
多线程:
from threading import Thread
import time

def work():
    time.sleep(2)  # 模拟纯IO操作
if __name__ == '__main__':
    start_time = time.time()
    t_list = []
    for i in range(100):
        t = Thread(target=work)
        t.start()
        t_list.append(t)
    for t in t_list:
        t.join()
    print('总耗时:%s'%(time.time()-start_time))  # 总耗时:2.03155326843261

验证:在多个CPU且计算密集型的的情况

在多个CPU且计算密集型的的情况的情况下,多进程有优势

多进程:
from multiprocessing import Process
import os
import time
​
def work():
    res = 1
    for i in range(1,100000):
        res *= i
​
if __name__ == '__main__':
    print(os.cpu_count())  # 8 查看当前计算机cpu的个数
    start_time = time.time()
    p_list = []
    for i in range(12):
        p = Process(target=work)
        p.start()
        p_list.append(p)
    for p in p_list:
        p.join()
    print('总耗时:%s'%(time.time()-start_time))  # 总耗时:9.805726766586304
from threading import Thread
import os
import time

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

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

同步锁(互斥锁)

多程序同时操作一份数据的时候,很容易产生数据的错乱,为了避免数据错乱,我们需要使用互斥锁;互斥锁是将并发变成串行,虽然牺牲了程序的执行效率但是保证了数据的安全

1.开多进程或者多线程一定好吗?
	不一定,虽然可以提高效率,但是,会出现安全问题
2.多个线程去操作同一个数据,会出现并发安全问题
	解决1: 加锁,让原本并发的操作,变成串行,牺牲效率,保证安全
     解决2:通过线程queue也可以避免并发安全的问题,所有queue的本质就是锁
3.互斥锁只应该在出现多个程序操作数据的地方,其他位置尽量不要加 。ps:以后我们自己处理的情况很少,只需要知道锁的功能即可
4.补充
    1.行锁:访问数据库的时候,锁定整个行数据,他人不能在哪一行操作数据。
    2.表锁:访问数据库的时候,锁定整个表数据,他人不能在哪一个表格操作数据。
    3.乐观锁:每次去拿数据的时候都认为别人不会修改,所以不会上锁
    4.悲观锁:每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁

代码

from threading import Thread

n = 10

def task():
    global n
    n -= 1

if __name__ == '__main__':
    l = []
    for i in range(10):
        t = Thread(target=task)
        t.start()
        l.append(t)

    for j in l:
        j.join()
    print('n:', n)  # n: 0
    
from threading import Thread

n = 10

def task():
    global n
    temp = n
    n = temp - 1  # n=n-1

if __name__ == '__main__':
    l = []
    for i in range(10):
        t = Thread(target=task)
        t.start()
        l.append(t)

    for j in l:
        j.join()
    print('n:', n)  # n: 0
from threading import Thread, Lock
import time
n = 10


def task():
    global n
    temp = n
    time.sleep(1)
    n = temp - 1  

if __name__ == '__main__':
    l = []
    for i in range(10):
        t = Thread(target=task)
        t.start()
        l.append(t)

    for j in l:
        j.join()
    print('n:', n)  # n: 9
ps:创建的多线程(10个)几乎接近同一时间去执行task函数,拿到n=10(在1秒内,还来不及减1) ,最后就n=9  ,引发了数据安全问题
from threading import Thread, Lock
import time
n = 10

def task(lock):
    lock.acquire()  # 上锁
    global n
    temp = n
    time.sleep(1)
    n = temp - 1
    lock.release() # 释放锁

if __name__ == '__main__':
    lock = Lock()  # 创建锁
    l = []
    for i in range(10):
        t = Thread(target=task,args=(lock,))
        t.start()
        l.append(t)
    for j in l:
        j.join()
    print('n:', n)  # n: 0

GIL VS Lock

1.Python已经有一个GIL来保证同一时间只能有一个线程来执行了,为什么这里还需要lock?
    1.首先我们需要达成共识:锁的目的是为了保护共享的数据,同一时间只能有一个线程来修改共享的数据
	2.然后,我们可以得出结论:保护不同的数据就应该加不同的锁。
    3.最后,GIL 与Lock是两把锁,保护的数据不一样
    	GIL是解释器级别的(当然保护的就是解释器级别的数据,比如垃圾回收的数据),
        Lock是保护用户自己开发的应用程序的数据,很明显GIL不负责这件事,只能用户自定义加锁处理,即Lock

2.过程分析:所有线程抢的是GIL锁,或者说所有线程抢的是执行权限
	线程1抢到GIL锁,拿到执行权限,开始执行,然后加了一把Lock,还没有执行完毕,即线程1还未释放Lock,有可能线程2抢到GIL锁,开始执行,执行过程中发现Lock还没有被线程1释放,于是线程2进入阻塞,被夺走执行权限,有可能线程1拿到GIL,然后正常执行到释放Lock。。。这就导致了串行运行的效果

    既然是串行,那我们执行
	t1.start()
    t1.join
    t2.start()
    t2.join()
    这也是串行执行啊,为何还要加Lock呢,需知join是等待t1所有的代码执行完,相当于锁住了t1的所有代码,而Lock只是锁住一部分操作共享数据的代码。
    ps:
    在start之后立刻使用join,肯定会将多任务的执行变成串行,毫无疑问,但问题是
    1.start后立即join:任务内的所有代码都是串行执行的,而加锁,只是加锁的部分即修改共享数据的部分是串行的
   2.单从保证数据安全方面,二者都可以实现,但很明显是加锁的效率更高.