12线程

一、线程

  1. 线程和进程的区别

    • 进程
      • cpu(操作系统)分配资源最小单位
      • 开启和关闭进程非常消耗资源
      • 进程之间数据隔离
      • 开启的进程个数最大不超过cpu个数的2倍
    • 线程
      • 是进程最小单位,处理事务的操作者
      • 开启和关闭非常快
      • 线程之间数据不安全
      • 可以开启很多的线程,消耗小
  2. 在CPython中的多线程

    • gc 垃圾回收机制
    • 因为多个线程之间要共享数据,所以必须要有锁
    • 全局解释器锁 GIL
      • 同一个进程中的多个线程,只能有一个线程真正被cpu执行
      • 节约了IO操作时间,并不是节约了CPU计算时间
  3. 线程的用法和进程一样,只不过模块不一样

    from threading import Thread
    import time
    
    def func(i):
        print('{} is start'.format(i))
        time.sleep(1)
        print('{} is end'.format(i))
    
    if __name__ == '__main__':
        th_list=[]
        for i in range(10):
            th=Thread(target=func,args=(i,))
            th.start()
            th_list.append(th)
        for t in th_list:
            t.join()
        print('all end')
    
    from threading import Thread,enumerate,active_count,current_thread
    

    print(enumerate()):列表 打印当前所有活着的线程

    print(active_count()):数字 打印当前所有活着的线程个数

    print(current_thread().ident):对象 打印当前线程的线程ID

    线程.is_alive():检测线程是否还存活

    线程.setName():获取线程的名字

    线程.getName():设置线程的名字

  4. 使用面向对象创建线程

    from threading import Thread
    import time
    
    class ther(Thread):
        def __init__(self,id):
            super().__init__()
            self.id=id
        def run(self):
            print('{} is start'.format(self.id))
            time.sleep(1)
            print('{} is end'.format(self.id))
    
    if __name__ == '__main__':
        th_list=[]
        for i in range(10):
            th=ther(i)
            th.start()
            th_list.append(th)
        for t in th_list:t.join()
        print('all is end')
    
  5. 主线程和子线程

    • 主线程会等待子线程结束才会结束
  6. 守护线程与守护进程

    • 守护线程会随着主线程结束而结束,主线程结束后如果还有别的子线程,守护线程仍然会守护
      • 守护线程守护所有的线程包括子线程
    • 守护进程会随着主进程代码结束而结束
      • 守护进程只守护主线程
    def func1():
    	while True:
    		time.sleep(0.3)
    		print("这是func1,死循环")
    
    
    def func2():
    	print("这是func2开始")
    	time.sleep(1)
    	print("这是func2结束")
    
    
    if __name__ == '__main__':
    	th1 = Thread(target=func1)
    	th2 = Thread(target=func2)
    	th1.setDaemon(True)
    	th1.start()
    	th2.start()
    	print("主线程结束")
    #结果如下:
    这是func2开始
    主线程结束
    这是func1,死循环
    这是func1,死循环
    这是func1,死循环
    这是func2结束
    

二、线程锁,Lock,Samephore

  1. 案例:

    from threading import Thread,Lock
    import time
    
    n = 0
    
    
    def func1(lock):
    	global n
    	for i in range(1000000):
    		with lock:
    			n += 1
    
    
    def func2(lock):
    	global n
    	for i in range(1000000):
    		with lock:
    			n -= 1
    
    
    if __name__ == '__main__':
    	stime = time.time()
    	lock = Lock()
    	lst = []
    	for i in range(10):
    		t1 = Thread(target=func1, args=(lock,))
    		t2 = Thread(target=func2, args=(lock,))
    		t1.start()
    		t2.start()
    		lst.append(t1)
    		lst.append(t2)
    	for s in lst:
    		s.join()
    	etime = time.time()
    	print("最后的结果是:{}".format(n))
    	print("耗时:{}".format(etime-stime))
    #如果多个进程操作同一个数据,就需要加一个锁保证数据准确性。
    
  2. 数据不安全

    • 因为线程有GIL锁,所以在线程中也存在数据不安全的现象

      • 执行a+=1时,cpu至少做两件事:
        • 执行a+1
        • 把结果赋值给a
      • 但是如果赋值之前,GIL切换到了另一个进程,那么就导致a的数值发生了变化
    • 涉及到多个线程对同一个变量进行计算并赋值给自己的时候就会出现数据不安全

      • +=、-=、*=、/=,while、if
      • 列表,字典中的方法是数据安全的
  3. 可以用锁解决数据不安全

    • 加一个Lock
    • 虽然GIL锁仍然会切换,但是代码被锁住了(其他进程处于阻塞状态),所以最终还是自己执行,直到锁结束
    • 保证了数据安全,但是会影响执行效率
  4. 死锁

    • 死锁现象:
      • 多把锁并且在多个线程中交叉使用就会出现死锁现象
    • 如果是互斥锁,解决死锁现象最快方法就是把所有互斥锁都改成递归锁
    • 递归锁效率比互斥锁效率高
    • 尽量只使用一把互斥锁
    from threading import Thread, Lock
    import time
    
    
    def eat(lock, name):
    	with lock:
    		print("{name}拿到了筷子,{name}拿到了面条".format(name=name))
    		print("{}吃面条".format(name))
    		time.sleep(0.5)
    		print("{name}放下筷子,{name}放下面条".format(name=name))
    
    
    if __name__ == '__main__':
    	lock = Lock()
    	lst1 = ["test1", "tet2"]
    	lst2 = ["test3", "tet4"]
    	for name in lst1:
    		Thread(target=eat,args=(lock,name)).start()
    	for name in lst2:
    		Thread(target=eat,args=(lock,name)).start()
    

三、事件Event

1、案例

# ###模拟连接远程数据库
from threading import Thread, Event
import random, time


# 模拟网络延迟
def check(e):
	time.sleep(random.randrange(1, 6))
	e.set()


# 连接数据库
def connect(e):
	# 连接四次
	for i in range(1, 4):
		# 等待1s
		e.wait(1)
		if e.is_set():
			print("正在校验用户合法性")
			print("连接成功")
			break
		else:
			print("第{}次连接失败,正在重试".format(i))
	else:
		# 如果三次没有连接成功,就报出连接超时异常
		raise TimeoutError


if __name__ == '__main__':
	e = Event()
	t1 = Thread(target=check, args=(e,))
	t2 = Thread(target=connect, args=(e,))
	t1.start()
	t2.start()
	t2.join()

四、队列、栈

1、队列,专门处理进程之间的数据

import queue
q=queue.Queue(4)
q.put(1)
q.put(2)
q.put(3)
q.put(4)
q.put(5)
print('第五个数据')
  • q=queue.Queue(4):4表示该队列中最多保留4个数据
  • "第五个数据"这句话不会执行,因为最多放四个,执行到q.put(5)就阻塞了,等待队列中有空位置。
import queue
q=queue.Queue(4)
q.put(4)
print(q.get_nowait())
try:
    q.get_nowait()
except queue.Empty:
    print('队列中没有数据了')
    
#结果如下:
4
队列中没有数据了
  • 使用q.get_nowait()方法,如果队列中有数据就取,如果没有数据就报一个queue.Empty的异常
  1. ,先进后出,后进先出

    from queue import LifoQueue
    lq=LifoQueue()
    lq.put(1)
    lq.put(2)
    print(lq.get())
    print(lq.get())
    #打印结果是:
    2
    1
    
  2. 优先级队列

    from queue import PriorityQueue
    pq=PriorityQueue()
    pq.put((1,"1"))
    pq.put((2,"2"))
    pq.put((0,"0"))
    print(pq.get())
    print(pq.get())
    print(pq.get())
    #结果为:
    (0,"0")
    (1,"1")
    (2,"2")
    

    按照ASCII码大小来决定队列的顺序,如果放的是一个元组,就用元组第一个元素进行排序

三、线程池、进程池

  1. 池的概念

    • 要在程序开始的时候,还没开始提交任务先创建几个线程或进程,把这些线程或进程放在一个池子里
  2. 为什么要用池

    • 节约时间:先开进程或线程,有任务来的时候就可以直接调用池中的数据
    • 开好的线程或者进程会一直存在池中,可以被多个任务反复利用,这样可以极大的减少了开启、关闭、调度进程\线程的时间
    • 池中的线程\进程的个数,控制了操作系统需要调度的任务的个数,控制池中的单位,有利于提高系统的效率,减少操作系统的负担。
  3. 进程池特点

    • 可以控制池的数量
      • 比如一个池中最多可以起4个进程\线程,调用的时候,同时执行的进程\线程最多有4个,结束一个新增一个。
    • 并不是分批执行,结束一个就可以进来一个
  4. 默认进程池数量:os.cpu_count(),有几个cpu,进程池中最大有几个进程

  5. 默认线程池数量:os.cpu_count()*5,cpu个数乘5

  6. 例子

    from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor
    from threading import current_thread
    import time
    import random
    
    
    def func():
        print(current_thread().ident,'start')
        time.sleep(random.randint(1,4))
        print(current_thread().ident,'end')
    
    #池中有三个线程
    tp=ThreadPoolExecutor(3)
    for _ in range(5):
        tp.submit(func)
    #结果如下:
    2732 start
    7688 start
    4396 start
    2732 end
    2732 start
    2732 end
    2732 start
    4396 end
    7688 end
    2732 end
    
    • from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor:加载模块,线程池进程池
    • from threading import current_thread:查看线程详细情况
    • tp=ThreadPoolExecutor(3):开启三个线程
    • tp.submit(func):把需要执行的内容放入池中执行,向池中传递任务
  7. 可以传递参数

    from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor
    from threading import current_thread
    import time
    import random
    
    
    def func(a,b):
        print(current_thread().ident,'start',a,b)
        time.sleep(random.randint(1,4))
        print(current_thread().ident,'end')
    
    tp=ThreadPoolExecutor(3)
    for i in range(5):
        tp.submit(func,i,b=i+1)
    
    • 可以在tp对象中直接传递参数
  8. 获取返回值

    from concurrent.futures import ThreadPoolExecutor
    
    def func(a,b):
        return a+b
    
    tp=ThreadPoolExecutor(4)
    ret_l=[]
    for i in range(5):
        ret=tp.submit(func,i,i+1)
        ret_l.append(ret)
    
    for obj in ret_l:
        print(obj.result())
    
    • 进程或线程中可以返回一个结果
    • 这个结果是一个Future对象,未来对象,通过result方法获取返回值
    • 获得的结果可能并不是现在用的,留作以后用
  9. map方法

    • 使用map可以使代码简洁,但是不方便传递参数
    • map(进程名,可迭代对象),返回一个可迭代对象,可以通过循环返回执行的结果。
    from concurrent.futures import ThreadPoolExecutor
    
    def func(a):
        b=a+1
        return a+b
    
    tp=ThreadPoolExecutor(4)
    ret=tp.map(func,range(5))
    for obj in ret:
        print(obj)
    
  10. 等待所有进程\线程执行结束再执行下面代码

  • shutdown(),类似于join
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor
import os, time


def task(i):
	print("这是第{}个任务".format(i))
	time.sleep(0.5)


if __name__ == '__main__':
	# 进程池中最大有5个进程
	p = ProcessPoolExecutor(5)
	for i in range(10):
		# 开始执行进程,第一个参数是任务函数名,后面是参数
		p.submit(task, i)
	# 等待所有进程执行结束,类似于join
	p.shutdown()
	print("所有任务执行结束")

四、回调函数,效率最高

  • 把函数当成一个参数传递给另一个函数,在当前函数执行完毕之后,最后调用一个改参数(函数),这个函数就是回调函数。

  • add_done_callback(函数名)

  • 一般用于执行任务之后把结果给另一个程序再次处理,可以使用回调函数。

  • 进程池的回调函数由主进程执行,线程池的回调函数由子线程执行。

  • 获取进程运行结果,立即执行获取结果后的代码。但是不能自动识别是哪一个任务的结果

    from concurrent.futures import ThreadPoolExecutor
    import time
    import random
    
    def func(a):
        b=a+1
        time.sleep(random.random())
        return a*b
    
    def print_ret(ret):
        print(ret.result())
    tp=ThreadPoolExecutor(4)
    for i in range(10):
        ret=tp.submit(func,i)
        ret.add_done_callback(print_ret)
    #结果如下:
    6
    0
    12
    2
    20
    72
    56
    30
    90
    42
    
   
   + 如果没有使用回调函数,只能按照顺序去处理,异步非阻塞
   + 加入回调函数,哪个结果先回来就先处理哪个,异步阻塞
   
9. 例子

   + 使用回调函数处理结果

     ```python
     from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor
     import requests
     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/'
         ]
         tp=ThreadPoolExecutor(4)
         for url in urls:
             ret=tp.submit(get_page,url)
             ret.add_done_callback(parse_page)
  1. 使用场景
    • 进程池
      • 适用于高计算,没有IO操作
      • 没有网络操作,没有数据库操作,没有文件操作,没有input操作
      • 如果使用进程池,池中进程的数量,一般是大于cpu个数,小于cpu个数2倍
    • 线程池
      • 启用cpu个数*5
    • 假如有一个4c的计算机
      • 可以启用的并发为:5个进程*20个线程=100个并发
posted @ 2023-07-12 09:52  majic_jie  阅读(49)  评论(0)    收藏  举报