多线程

一 threading模块介绍

multiprocess模块的完全模仿了threading模块的接口,二者在使用层面,有很大的相似性,因而不再详细介绍

官网链接:https://docs.python.org/3/library/threading.html?highlight=threading#

二 开启线程的两种方式

#方式一
from threading import Thread
import time

def task(name):
    print(f'{name}task is running')
    time.sleep(2)
    print(f'{name}task is done')

if __name__ == '__main__':
    t = Thread(target=task,args=('egon',))
    t.start() # 告诉操作系统开一个线程 开启线程无需申请内存空间会非常快。
    print('主')
# 注意:1 同一个进程下没有父子线程之分大家地位都一样。
#      2 右键运行发生了个什么事情? 开启了一个进程,开辟了一个内存空间,把代码都丢进去,然后运行代码(自带的主线程运行)
#      然后又开启了一个子线程。


# ps:主线程结束和子线程结束没有任何必然联系,比如主线程运行结束,子线程还在运行当中。不是主线程在等待子线程结束,是进程在等待自己的所有线程结束。
from threading import Thread
import time

class MyTread(Thread):
    def run(self):
        print('thread is running')
        time.sleep(2)
        print('thread is end')


if __name__ == '__main__':
    t = MyTread()
    t.start() #
    print('主线程')
    # 输出:
    # thread is running
    # 主线程
    # thread is end

img

三 进程vs线程

1 比起进程来说线程的开启速度快

from threading import Thread
from multiprocessing import Process
import time
def task(name):
    print(f"{name} is running")
    time.sleep(2)
    print(f"{name} is done")

if __name__ == '__main__':
    t = Thread(target=task,args=('子线程',))
    p = Process(target=task,args=('子进程',))
    t.start()
    # p.start()
    print('主')
    '''
    开启子进程打印的效果:
    
    >主
    >子进程 is running
    >子进程 is done
    
    开启子线程打印的效果:
    >子线程 is running
    >主
    >子线程 is done
    
    对比的结果:从打印效果就能看的出来,开启线程要快于进程。
    开启进程需要申请空间,复制代码到该空间,然后执行自带线程,非常慢。
    开启子线程 基本没有资源消耗所以非常快。
    '''

2 同一进程下的线程共享内存空间

from threading import Thread
import time

x = 100
def task():
    global x
    x = 20

if __name__ == '__main__':
    t = Thread(target=task)
    t.start()
    time.sleep(2)
    print(x) # 20
    # 首先子线程修改了全局变量为20,主线程等待子线程修改完毕后打印x为20。
    # 说明同一个进程下所有的线程共享同一份内存空间。

3 瞅瞅pid

from threading import Thread
import os

def task():
    print('子线程',os.getpid()) # 子线程 14576

if __name__ == '__main__':
    t = Thread(target=task)
    t.start()
    print('主线程',os.getpid())  #主线程 14576


四 线程对象join用法

主线程等待子线程运行结束。

from threading import Thread
import time

def task(name,n):
    print(f'{name} is running')
    time.sleep(n)
    print(f'{name} is done')

if __name__ == '__main__':
    t1 = Thread(target=task,args=('线程1',1))
    t2 = Thread(target=task,args=('线程2',2))
    t3 = Thread(target=task,args=('线程3',3))

    start_time = time.time()


    t1.start() #
    t2.start() #
    t3.start() #

    t1.join() # 等1s
    t2.join() # 等1s
    t3.join() # 等1s
    end_time = time.time()
    print(end_time-start_time) # 3.0023529529571533

    print('主')
    '''
    线程1 is running
    线程1 is done
    主  
    '''

ps:了解 对比进程的join,进程的join是当前线程在等子进程运行结束并不影响其他线程。

from multiprocessing import Process
from threading import Thread
import time

def threadtask(name):
    print(f'{name} start')
    time.sleep(5)
    print(f'{name} end')


def processtask(name):
    print(f'{name} start')
    time.sleep(20)
    print(f'{name} end')

if __name__ == '__main__':
    t = Thread(target=threadtask,args=('子线程',))
    p = Process(target=processtask,args=('子进程',))
    p.start()
    t.start()
    p.join() # 当前线程等待当前进程下的p子进程结束,然后往下运行。
from threading import Thread
from multiprocessing import Process
import os

def work():
    print('hello',os.getpid())

if __name__ == '__main__':
    #part1:在主进程下开启多个线程,每个线程都跟主进程的pid一样
    t1=Thread(target=work)
    t2=Thread(target=work)
    t1.start()
    t2.start()
    print('主线程/主进程pid',os.getpid())

    #part2:开多个进程,每个进程都有不同的pid
    p1=Process(target=work)
    p2=Process(target=work)
    p1.start()
    p2.start()
    print('主线程/主进程pid',os.getpid())
from  threading import Thread
from multiprocessing import Process
import os
def work():
    global n
    n=0

if __name__ == '__main__':
    # n=100
    # p=Process(target=work)
    # p.start()
    # p.join()
    # print('主',n) #毫无疑问子进程p已经将自己的全局的n改成了0,但改的仅仅是它自己的,查看父进程的n仍然为100


    n=1
    t=Thread(target=work)
    t.start()
    t.join()
    print('主',n) #查看结果为0,因为同一进程内的线程之间共享进程内的数据

五 思考主线程是否会等子线程运行结束

import time

def task(name):
    print(f'线程 start {name}')
    time.sleep(3)
    print('线程 end')

if __name__ == '__main__':
    t = Thread(target=task,args=('egon',))
    t.start() #非常快
    print('主')

分析

1 其实是进程在等
貌似是主线程在原地等着,主线程已经运行完。
原来没有子线程的情况下,其实就一个主线程这一条流水线工作完了,这个进程就结束了。
那现在的情况是当前进程有其他的子线程,是进程等待自己所有的子线程运行完。

# 主进程等子进程是因为主进程要给子进程收尸
# 现在看到的等是进程必须等待其内部所有线程都运行完毕才结束。

六 线程相关的其他方法(了解)



Thread实例对象的方法
  # isAlive(): 返回线程是否活动的。
  # getName(): 返回线程名。
  # setName(): 设置线程名。

threading模块提供的一些方法:
  # threading.currentThread(): 返回当前的线程变量。
  # threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。
  # threading.activeCount(): 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果。
from threading import Thread
import threading

'''以后只有周日!!!'''

def task():
    import time
    time.sleep(3)
    print(threading.current_thread().getName())

if __name__ == '__main__':
    t = Thread(target=task)
    t1 = Thread(target=task)
    print(threading.current_thread().setName('张三')) # None
    print(threading.current_thread().getName()) # 张三
    t.start()
    t1.start()
    #  返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。
    print(threading.enumerate()) # [<_MainThread(MainThread, started 17944)>, <Thread(Thread-1, started 19320)>, <Thread(Thread-2, started 19308)>]
    print(len(threading.enumerate())) # 3
    print(threading.activeCount()) # 返回正在运行的线程数量。与len(threading.enumerate())有相同的结果。
    

七 守护线程(了解)

# 守护线程首先是一个线程。
	# 守护线程守护到当前进程运行结束。
    # ps:比如有未完成的子进程阶段会守护,比如有未完成的其他子线程也均会守护。
# 守护进程首先是一个进程。
	# 守护进程守护到当前进程的最后一行代码结束。

代码演示:

# 守护线程守护到当前进程结束
from threading import Thread
import threading
import time

def threadtask(name):
    print(f' {name}  start')
    print(time.sleep(20))
    print(f' {name}  end')
    # print(time.sleep(6))

def threadtask2(name):
    print(f'{name} start')
    time.sleep(10)
    print(threading.enumerate()) # [<_MainThread(MainThread, stopped 14544)>, <Thread(Thread-1, started daemon 13676)>, <Thread(Thread-2, started 13148)>]
    print(f'{name} end')

if __name__ == '__main__':
    t = Thread(target=threadtask,args=('守护线程',))
    t2 = Thread(target=threadtask2,args=('子线程',))
    t.daemon = True
    t.start()
    t2.start()
    print('主')
    '''
    守护线程  start
    子线程 start
    主
    [<_MainThread(MainThread, stopped 15448)>, <Thread(Thread-1, started daemon 17520)>, <Thread(Thread-2, started 10356)>]
    子线程 end

    '''
    可以看到当主线程已经结束的时候,其他子线程没有结束的时候打印当前的活跃的线程发现有守护线程。

八 同步锁(线程的互斥锁)

0 前言:

x = 1
def func1():
    global x
    print(x) # 1
    func2()
    print(x) # 10000  每次拿x的时候都会去全局拿到那个最新的x
    x  = x -1
def func2():
    global x
    x = 10000

func1()
# func2()
print(x) # 9999

1 多线程修改数据会造成混乱

from threading import Thread,current_thread,Lock
import time
x = 0

def task():
    global x
    for i in range(100000): # 最少10万级别才能看出来
        x = x+1   # 有可能右边的x刚拿到了0,
        # 发生线程不安全的原因:
        # t1 x+1 阶段 x = 0 保存了状态 cpu被切走  t2 x+1 x = 0 保存了状态 cpu被切走
        # 下次t1 继续运行代码 x = 0+1  下次t2 再运行代码的时候也是 x = 0+1
        #  也就说修改了两次 x 只加了一次1 。
        # time.sleep()
    # lock.release()
if __name__ == '__main__':
    t_list = []
    for i in range(3):
        t = Thread(target=task)
        t_list.append(t)
        t.start()
    for i in t_list:
        i.join()

    print(x) # 99

2 使用线程锁解决线程修改数据混乱问题。

from threading import Thread,current_thread,Lock
import time
x = 0
lock = Lock()
def task():
    global x
    lock.acquire()
    for i in range(100000): # 最少10万级别才能看出来  
        x = x+1   # 有可能右边的x刚拿到了0,
    lock.release()
if __name__ == '__main__':
    t_list = []
    for i in range(3):
        t = Thread(target=task)
        t_list.append(t)
        t.start()
    for i in t_list:
        i.join()

    print(x) # 99

3 死锁问题

from threading import Thread,Lock
import time
mutexA=Lock()
mutexB=Lock()

class MyThread(Thread):
    def run(self):
        self.func1()
        self.func2()
    def func1(self):
        mutexA.acquire()
        print('\033[41m%s 拿到A锁\033[0m' %self.name)

        mutexB.acquire()
        print('\033[42m%s 拿到B锁\033[0m' %self.name)
        mutexB.release()

        mutexA.release()

    def func2(self):
        mutexB.acquire()
        print('\033[43m%s 拿到B锁\033[0m' %self.name)
        time.sleep(2)

        mutexA.acquire()
        print('\033[44m%s 拿到A锁\033[0m' %self.name)
        mutexA.release()

        mutexB.release()

if __name__ == '__main__':
    for i in range(10):
        t=MyThread()
        t.start()

'''
Thread-1 拿到A锁
Thread-1 拿到B锁
Thread-1 拿到B锁
Thread-2 拿到A锁
然后就卡住,死锁了
'''

**解决死锁问题 **

解决方法,递归锁,在Python中为了支持在同一线程中多次请求同一资源,python提供了可重入锁RLock。

这个RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源可以被多次require。直到一个线程所有的acquire都被release,其他的线程才能获得资源。上面的例子如果使用RLock代替Lock,则不会发生死锁:

mutexA=mutexB=threading.RLock() #一个线程拿到锁,counter加1,该线程内又碰到加锁的情况,则counter继续加1,这期间所有其他线程都只能等待,等待该线程释放所有锁,即counter递减到0为止

九 信号量Semaphore

同进程的一样

Semaphore管理一个内置的计数器,
每当调用acquire()时内置计数器-1;
调用release() 时内置计数器+1;
计数器不能小于0;当计数器为0时,acquire()将阻塞线程直到其他线程调用release()。

实例:(同时只有5个线程可以获得semaphore,即可以限制最大连接数为5):

from threading import Thread,Semaphore
import threading
import time


def task():
    sm.acquire()
    print(f"{threading.current_thread().name} get sm")
    time.sleep(3)
    sm.release()

if __name__ == '__main__':
    sm = Semaphore(5) # 同一时间只有5个进程可以执行。
    for i in range(20):
        t = Thread(target=task)
        t.start()

与进程池是完全不同的概念,进程池Pool(4),最大只能产生4个进程,而且从头到尾都只是这四个进程,不会产生新的,而信号量是产生一堆线程/进程

二 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
在Cpython解释器中有一把GIL锁,GIl锁本质是一把互斥锁。
因为GIL锁:同一个进程下开启的多线程,同一时刻只能有一个线程执行,无法利用多核优势

# 2
GIL本质就是一把互斥锁,那既然是互斥锁,原理都一样,都是让多个并发线程同一时间只能
    有一个执行
    即:有了GIL的存在,同一进程内的多个线程同一时刻只能有一个在运行,意味着在Cpython中
        一个进程下的多个线程无法实现并行===》意味着无法利用多核优势
        多个线程只能并发不能并行
# 3
GIL可以被比喻成执行权限,同一进程下的所以线程 要想执行都需要先抢执行权限。

首先需要明确的一点是GIL并不是Python的特性,它是在实现Python解析器(CPython)时所引入的一个概念。就好比C++是一套语言(语法)标准,但是可以用不同的编译器来编译成可执行代码。有名的编译器例如GCC,INTEL C++,Visual C++等。Python也一样,同样一段代码可以通过CPython,PyPy,Psyco等不同的Python执行环境来执行。像其中的JPython就没有GIL。然而因为CPython是大部分环境下默认的Python执行环境。所以在很多人的概念里CPython就是Python,也就想当然的把GIL归结为Python语言的缺陷。所以这里要先明确一点:GIL并不是Python的特性,Python完全可以不依赖于GIL

二 GIL介绍

GIL本质就是一把互斥锁,既然是互斥锁,所有互斥锁的本质都一样,都是将并发运行变成串行,以此来控制同一时间内共享数据只能被一个任务所修改,进而保证数据安全。

# 为何要有GIL?
    因为Cpython解释器自带垃圾回收机制不是线程安全的。
# 如果不对垃圾回收机制线程做任何处理,也没有GIL锁行不行?
	提示:如果是问题的这种情况,多线程和垃圾回收线程就会并发了。

可以肯定的一点是:保护不同的数据的安全,就应该加不同的锁。

要想了解GIL,首先确定一点:每次执行python程序,都会产生一个独立的进程。例如python test.py,python aaa.py,python bbb.py会产生3个不同的python进程

'''
#验证python test.py只会产生一个进程
#test.py内容
import os,time
print(os.getpid())
time.sleep(1000)
'''
python3 test.py 
#在windows下
tasklist |findstr python
#在linux下
ps aux |grep python

在一个python的进程内,不仅有test.py的主线程或者由该主线程开启的其他线程,还有解释器开启的垃圾回收等解释器级别的线程,总之,所有线程都运行在这一个进程内,毫无疑问

#1 所有数据都是共享的,这其中,代码作为一种数据也是被所有线程共享的(test.py的所有代码以及Cpython解释器的所有代码)
例如:test.py定义一个函数work(代码内容如下图),在进程内所有线程都能访问到work的代码,于是我们可以开启三个线程然后target都指向该代码,能访问到意味着就是可以执行。

#2 所有线程的任务,都需要将任务的代码当做参数传给解释器的代码去执行,即所有的线程要想运行自己的任务,首先需要解决的是能够访问到解释器的代码。

综上:

如果多个线程的target=work,那么执行流程是

多个线程先访问到解释器的代码,即拿到执行权限,然后将target的代码交给解释器的代码去执行

解释器的代码是所有线程共享的,所以垃圾回收线程也可能访问到解释器的代码而去执行,这就导致了一个问题:对于同一个数据100,可能线程1执行x=100的同时,而垃圾回收执行的是回收100的操作,解决这种问题没有什么高明的方法,就是加锁处理,如下图的GIL,保证python解释器同一时间只能执行一个任务的代码

三 GIL与Lock

GIL保护的是解释器级的数据,保护用户自己的数据则需要自己加锁处理,如下图

GIL是导致python慢的元凶吗?

单核cpu同一个时刻只能执行一个线程。
而python因为有GIL的存在无法利用多核优势,即线程1在cpu核心1运行的同时不允许cpu在核心2上运行,也就是单进程下(同一个解释器)只能有同一个线程在运行。
那么如果开多进程的话跟GIL有啥关系吗还?答案是完全没有。
比如你有四个核心,你开了四个进程,每个进程里只有一个线程在执行。完全没有问题,同样能实现多核优势的效果(这跟无法利用多核优势毫无关系)。
所以说多线程更多的是为了显示出来并发的效果,而不是提升执行效率。

所以如果开多进程的话,那GIL解释器锁一点都不会影响效率。
参考:https://python3-cookbook.readthedocs.io/zh_CN/latest/c12/p09_dealing_with_gil_stop_worring_about_it.html

那么影响效率的是什么?

“它是解释型语言而非编译语言”
参考:https://www.jiqizhixin.com/articles/2018-08-14-11
cpython执行python文件第一次也会编译成为pychach字节码文件,然后解释执行。
但是他的字节码执行的效率不如java的高,java编译的时候用到里JIT,JIT允许在运行时进行优化,即执行某些代码次数越多运行该代码越快。
“它是动态类型语言”
不必声明类型不是使Python变慢的原因。Python语言的设计使我们几乎可以创建任何动态变量。我们可以在运行时替换对象中的方法,也可以胡乱地把低级系统调用赋给一个值。几乎怎么修改都可以。
正是这种设计使得优化Python变得异常困难。

posted @ 2019-09-16 05:20  张明岩  阅读(383)  评论(0编辑  收藏  举报