欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

python基础(34):线程(二)

程序员文章站 2024-01-23 13:03:10
1. python线程 1.1 全局解释器锁GIL Python代码的执行由Python虚拟机(也叫解释器主循环)来控制。Python在设计之初就考虑到要在主循环中,同时只有一个线程在执行。虽然 Python 解释器中可以“运行”多个线程,但在任意时刻只有一个线程在解释器中运行。对Python虚拟机 ......

1. python线程

1.1 全局解释器锁gil

python代码的执行由python虚拟机(也叫解释器主循环)来控制。python在设计之初就考虑到要在主循环中,同时只有一个线程在执行。虽然 python 解释器中可以“运行”多个线程,但在任意时刻只有一个线程在解释器中运行。
对python虚拟机的访问由全局解释器锁(gil)来控制,正是这个锁能保证同一时刻只有一个线程在运行。

在多线程环境中,python 虚拟机按以下方式执行:

设置 gil

切换到一个线程去运行

运行指定数量的字节码指令或者线程主动让出控制(可以调用 time.sleep(0))

把线程设置为睡眠状态

解锁 gil

再次重复以上所有步骤

在调用外部代码(如 c/c++扩展函数)的时候,gil将会被锁定,直到这个函数结束为止(由于在这期间没有python的字节码被运行,所以不会做线程切换)编写扩展的程序员可以主动解锁gil。

1.2 python线程模块的选择

python提供了几个用于多线程编程的模块,包括thread、threading和queue等。thread和threading模块允许程序员创建和管理线程。thread模块提供了基本的线程和锁的支持,threading提供了更高级别、功能更强的线程管理的功能。queue模块允许用户创建一个可以用于多个线程之间共享数据的队列数据结构。

避免使用thread模块,因为更高级别的threading模块更为先进,对线程的支持更为完善,而且使用thread模块里的属性有可能会与threading出现冲突;其次低级别的thread模块的同步原语很少(实际上只有一个),而threading模块则有很多;再者,thread模块中当主线程结束时,所有的线程都会被强制结束掉,没有警告也不会有正常的清除工作,至少threading模块能确保重要的子线程退出后进程才退出。 

thread模块不支持守护线程,当主线程退出时,所有的子线程不论它们是否还在工作,都会被强行退出。而threading模块支持守护线程,守护线程一般是一个等待客户请求的服务器,如果没有客户提出请求它就在那等着,如果设定一个线程为守护线程,就表示这个线程是不重要的,在进程退出的时候,不用等待这个线程退出。

2. threading模块

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

2.1 线程的创建threading.thread类

2.1.1 线程的创建

创建线程的方式1:

from threading import thread
import time
def sayhi(name):
    time.sleep(2)
    print('%s say hello' %name)

if __name__ == '__main__':
    t=thread(target=sayhi,args=('egon',))
    t.start()
    print('主线程')

创建线程的方式2:

from threading import thread
import time
class sayhi(thread):
    def __init__(self,name):
        super().__init__()
        self.name=name
    def run(self):
        time.sleep(2)
        print('%s say hello' % self.name)

if __name__ == '__main__':
    t = sayhi('egon')
    t.start()
    print('主线程')

2.1.2 多线程与多进程

pid的比较:

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():
    print('hello')

if __name__ == '__main__':
    #在主进程下开启线程
    t=thread(target=work)
    t.start()
    print('主线程/主进程')
    '''
    打印结果:
    hello
    主线程/主进程
    '''

    #在主进程下开启子进程
    t=process(target=work)
    t.start()
    print('主线程/主进程')
    '''
    打印结果:
    主线程/主进程
    hello
    '''

内存数据的共享问题:

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,因为同一进程内的线程之间共享进程内的数据
同一进程内的线程共享该进程的数据?

2.1.3 thread类的其他方法

thread实例对象的方法
  # isalive(): 返回线程是否活动的。
  # getname(): 返回线程名。
  # setname(): 设置线程名。

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

代码示例:

from threading import thread
import threading
from multiprocessing import process
import os

def work():
    import time
    time.sleep(3)
    print(threading.current_thread().getname())

if __name__ == '__main__':
    #在主进程下开启线程
    t=thread(target=work)
    t.start()

    print(threading.current_thread().getname())
    print(threading.current_thread()) #主线程
    print(threading.enumerate()) #连同主线程在内有两个运行的线程
    print(threading.active_count())
    print('主线程/主进程')

    '''
    打印结果:
    mainthread
    <_mainthread(mainthread, started 140735268892672)>
    [<_mainthread(mainthread, started 140735268892672)>, <thread(thread-1, started 123145307557888)>]
    主线程/主进程
    thread-1
    '''

join方法:

from threading import thread
import time
def sayhi(name):
    time.sleep(2)
    print('%s say hello' %name)

if __name__ == '__main__':
    t=thread(target=sayhi,args=('egon',))
    t.start()
    t.join()
    print('主线程')
    print(t.is_alive())
    '''
    egon say hello
    主线程
    false
    '''

2.1.4 守护线程

无论是进程还是线程,都遵循:守护xx会等待主xx运行完毕后被销毁。需要强调的是:运行完毕并非终止运行。

对主进程来说,运行完毕指的是主进程代码运行完毕。

对主线程来说,运行完毕指的是主线程所在的进程内所有非守护线程统统运行完毕,主线程才算运行完毕。

主进程在其代码结束后就已经算运行完毕了(守护进程在此时就被回收),然后主进程会一直等非守护的子进程都运行完毕后回收子进程的资源(否则会产生僵尸进程),才会结束。

主线程在其他非守护线程运行完毕后才算运行完毕(守护线程在此时就被回收)。因为主线程的结束意味着进程的结束,进程整体的资源都将被回收,而进程必须保证非守护线程都运行完毕后才能结束。

守护线程实例1:

from threading import thread
import time
def sayhi(name):
    time.sleep(2)
    print('%s say hello' %name)

if __name__ == '__main__':
    t=thread(target=sayhi,args=('egon',))
    t.setdaemon(true) #必须在t.start()之前设置
    t.start()

    print('主线程')
    print(t.is_alive())
    '''
    主线程
    true
    '''

守护线程实例2:

import time
def foo():
    print(123)
    time.sleep(1)
    print("end123")

def bar():
    print(456)
    time.sleep(3)
    print("end456")


t1=thread(target=foo)
t2=thread(target=bar)

t1.daemon=true
t1.start()
t2.start()
print("main-------")

2.2 锁

2.2.1 同步锁

多个线程抢占资源的情况:

from threading import thread
import os,time
def work():
    global n
    temp=n
    time.sleep(0.1)
    n=temp-1
if __name__ == '__main__':
    n=100
    l=[]
    for i in range(100):
        p=thread(target=work)
        l.append(p)
        p.start()
    for p in l:
        p.join()

    print(n) #结果可能为99

对公共数据的操作:

import threading
r=threading.lock()
r.acquire()
r.release()

同步锁的引用:

from threading import thread,lock
import os,time
def work():
    global n
    lock.acquire()
    temp=n
    time.sleep(0.1)
    n=temp-1
    lock.release()
if __name__ == '__main__':
    lock=lock()
    n=100
    l=[]
    for i in range(100):
        p=thread(target=work)
        l.append(p)
        p.start()
    for p in l:
        p.join()

    print(n) #结果肯定为0,由原来的并发执行变成串行,牺牲了执行效率保证了数据安全

互斥锁与join的区别:

#不加锁:并发执行,速度快,数据不安全
from threading import current_thread,thread,lock
import os,time
def task():
    global n
    print('%s is running' %current_thread().getname())
    temp=n
    time.sleep(0.5)
    n=temp-1

if __name__ == '__main__':
    n=100
    lock=lock()
    threads=[]
    start_time=time.time()
    for i in range(100):
        t=thread(target=task)
        threads.append(t)
        t.start()
    for t in threads:
        t.join()

    stop_time=time.time()
    print('主:%s n:%s' %(stop_time-start_time,n))

'''
thread-1 is running
thread-2 is running
......
thread-100 is running
主:0.5216062068939209 n:99
'''

#不加锁:未加锁部分并发执行,加锁部分串行执行,速度慢,数据安全
from threading import current_thread,thread,lock
import os,time
def task():
    #未加锁的代码并发运行
    time.sleep(3)
    print('%s start to run' %current_thread().getname())
    global n
    #加锁的代码串行运行
    lock.acquire()
    temp=n
    time.sleep(0.5)
    n=temp-1
    lock.release()

if __name__ == '__main__':
    n=100
    lock=lock()
    threads=[]
    start_time=time.time()
    for i in range(100):
        t=thread(target=task)
        threads.append(t)
        t.start()
    for t in threads:
        t.join()
    stop_time=time.time()
    print('主:%s n:%s' %(stop_time-start_time,n))

'''
thread-1 is running
thread-2 is running
......
thread-100 is running
主:53.294203758239746 n:0
'''

#有的同学可能有疑问:既然加锁会让运行变成串行,那么我在start之后立即使用join,就不用加锁了啊,也是串行的效果啊
#没错:在start之后立刻使用jion,肯定会将100个任务的执行变成串行,毫无疑问,最终n的结果也肯定是0,是安全的,但问题是
#start后立即join:任务内的所有代码都是串行执行的,而加锁,只是加锁的部分即修改共享数据的部分是串行的
#单从保证数据安全方面,二者都可以实现,但很明显是加锁的效率更高.
from threading import current_thread,thread,lock
import os,time
def task():
    time.sleep(3)
    print('%s start to run' %current_thread().getname())
    global n
    temp=n
    time.sleep(0.5)
    n=temp-1

if __name__ == '__main__':
    n=100
    lock=lock()
    start_time=time.time()
    for i in range(100):
        t=thread(target=task)
        t.start()
        t.join()
    stop_time=time.time()
    print('主:%s n:%s' %(stop_time-start_time,n))

'''
thread-1 start to run
thread-2 start to run
......
thread-100 start to run
主:350.6937336921692 n:0 #耗时是多么的恐怖
'''
)

2.2.2 死锁与递归锁

进程也有死锁与递归锁,在进程那里忘记说了,放到这里一切说了额。

所谓死锁: 是指两个或两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程,如下就是死锁。

from threading import lock as lock
import time
mutexa=lock()
mutexa.acquire()
mutexa.acquire()
print(123)
mutexa.release()
mutexa.release()

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

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

递归锁rlock:

from threading import rlock as lock
import time
mutexa=lock()
mutexa.acquire()
mutexa.acquire()
print(123)
mutexa.release()
mutexa.release()

典型问题:科学家吃面

死锁问题:

import time
from threading import thread,lock
noodle_lock = lock()
fork_lock = lock()
def eat1(name):
    noodle_lock.acquire()
    print('%s 抢到了面条'%name)
    fork_lock.acquire()
    print('%s 抢到了叉子'%name)
    print('%s 吃面'%name)
    fork_lock.release()
    noodle_lock.release()

def eat2(name):
    fork_lock.acquire()
    print('%s 抢到了叉子' % name)
    time.sleep(1)
    noodle_lock.acquire()
    print('%s 抢到了面条' % name)
    print('%s 吃面' % name)
    noodle_lock.release()
    fork_lock.release()

for name in ['哪吒','egon','yuan']:
    t1 = thread(target=eat1,args=(name,))
    t2 = thread(target=eat2,args=(name,))
    t1.start()
    t2.start()

递归锁解决死锁问题:

import time
from threading import thread,rlock
fork_lock = noodle_lock = rlock()
def eat1(name):
    noodle_lock.acquire()
    print('%s 抢到了面条'%name)
    fork_lock.acquire()
    print('%s 抢到了叉子'%name)
    print('%s 吃面'%name)
    fork_lock.release()
    noodle_lock.release()

def eat2(name):
    fork_lock.acquire()
    print('%s 抢到了叉子' % name)
    time.sleep(1)
    noodle_lock.acquire()
    print('%s 抢到了面条' % name)
    print('%s 吃面' % name)
    noodle_lock.release()
    fork_lock.release()

for name in ['哪吒','egon','yuan']:
    t1 = thread(target=eat1,args=(name,))
    t2 = thread(target=eat2,args=(name,))
    t1.start()
    t2.start()

2.3 线程队列

queue队列 :使用import queue,用法与进程queue一样

queue is especially useful in threaded programming when information must be exchanged safely between multiple threads.

class queue.queue(maxsize=0) #先进先出

先进先出:

import queue

q=queue.queue()
q.put('first')
q.put('second')
q.put('third')

print(q.get())
print(q.get())
print(q.get())
'''
结果(先进先出):
first
second
third
'''

class queue.lifoqueue(maxsize=0) #last in fisrt out

后进先出:

import queue

q=queue.lifoqueue()
q.put('first')
q.put('second')
q.put('third')

print(q.get())
print(q.get())
print(q.get())
'''
结果(后进先出):
third
second
first
'''

class queue.priorityqueue(maxsize=0) #存储数据时可设置优先级的队列

优先级队列:

import queue

q=queue.priorityqueue()
#put进入一个元组,元组的第一个元素是优先级(通常是数字,也可以是非数字之间的比较),数字越小优先级越高
q.put((20,'a'))
q.put((10,'b'))
q.put((30,'c'))

print(q.get())
print(q.get())
print(q.get())
'''
结果(数字越小优先级越高,优先级高的优先出队):
(10, 'b')
(20, 'a')
(30, 'c')
'''

2.4 python标准模块--concurrent.futures

#1 介绍
concurrent.futures模块提供了高度封装的异步调用接口
threadpoolexecutor:线程池,提供异步调用
processpoolexecutor: 进程池,提供异步调用
both implement the same interface, which is defined by the abstract executor class.

#2 基本方法
#submit(fn, *args, **kwargs)
异步提交任务

#map(func, *iterables, timeout=none, chunksize=1) 
取代for循环submit的操作

#shutdown(wait=true) 
相当于进程池的pool.close()+pool.join()操作
wait=true,等待池内所有任务执行完毕回收完资源后才继续
wait=false,立即返回,并不会等待池内的任务执行完毕
但不管wait参数为何值,整个程序都会等到所有任务执行完毕
submit和map必须在shutdown之前

#result(timeout=none)
取得结果

#add_done_callback(fn)
回调函数

# done()
判断某一个线程是否完成

# cancle()
取消某个任务

processpoolexecutor:

#介绍
the processpoolexecutor class is an executor subclass that uses a pool of processes to execute calls asynchronously. processpoolexecutor uses the multiprocessing module, which allows it to side-step the global interpreter lock but also means that only picklable objects can be executed and returned.

class concurrent.futures.processpoolexecutor(max_workers=none, mp_context=none)
an executor subclass that executes calls asynchronously using a pool of at most max_workers processes. if max_workers is none or not given, it will default to the number of processors on the machine. if max_workers is lower or equal to 0, then a valueerror will be raised.


#用法
from concurrent.futures import threadpoolexecutor,processpoolexecutor

import os,time,random
def task(n):
    print('%s is runing' %os.getpid())
    time.sleep(random.randint(1,3))
    return n**2

if __name__ == '__main__':

    executor=processpoolexecutor(max_workers=3)

    futures=[]
    for i in range(11):
        future=executor.submit(task,i)
        futures.append(future)
    executor.shutdown(true)
    print('+++>')
    for future in futures:
        print(future.result())

threadpoolexecutor:

#介绍
threadpoolexecutor is an executor subclass that uses a pool of threads to execute calls asynchronously.
class concurrent.futures.threadpoolexecutor(max_workers=none, thread_name_prefix='')
an executor subclass that uses a pool of at most max_workers threads to execute calls asynchronously.

changed in version 3.5: if max_workers is none or not given, it will default to the number of processors on the machine, multiplied by 5, assuming that threadpoolexecutor is often used to overlap i/o instead of cpu work and the number of workers should be higher than the number of workers for processpoolexecutor.

new in version 3.6: the thread_name_prefix argument was added to allow users to control the threading.thread names for worker threads created by the pool for easier debugging.

#用法
与processpoolexecutor相同

map的用法:

from concurrent.futures import threadpoolexecutor,processpoolexecutor

import os,time,random
def task(n):
    print('%s is runing' %os.getpid())
    time.sleep(random.randint(1,3))
    return n**2

if __name__ == '__main__':

    executor=threadpoolexecutor(max_workers=3)

    # for i in range(11):
    #     future=executor.submit(task,i)

    executor.map(task,range(1,12)) #map取代了for+submit

回调函数:

from concurrent.futures import threadpoolexecutor,processpoolexecutor
from multiprocessing import pool
import requests
import json
import os

def get_page(url):
    print('<进程%s> get %s' %(os.getpid(),url))
    respone=requests.get(url)
    if respone.status_code == 200:
        return {'url':url,'text':respone.text}

def parse_page(res):
    res=res.result()
    print('<进程%s> parse %s' %(os.getpid(),res['url']))
    parse_res='url:<%s> size:[%s]\n' %(res['url'],len(res['text']))
    with open('db.txt','a') as f:
        f.write(parse_res)


if __name__ == '__main__':
    urls=[
        'https://www.baidu.com',
        'https://www.python.org',
        'https://www.openstack.org',
        'https://help.github.com/',
        'http://www.sina.com.cn/'
    ]

    # p=pool(3)
    # for url in urls:
    #     p.apply_async(get_page,args=(url,),callback=pasrse_page)
    # p.close()
    # p.join()

    p=processpoolexecutor(3)
    for url in urls:
        p.submit(get_page,url).add_done_callback(parse_page) #parse_page拿到的是一个future对象obj,需要用obj.result()拿到结果