并发编程 之 线程

线程

​ 进程:进程就是一个资源单位,它是用来申请内存空间的。
​ 线程:线程才是真正的执行单位,每一个进程中都会自带一个线程。

有了进程为什么要有线程

进程有很多优点,它提供了多道编程,让我们感觉我们每个人都拥有自己的CPU和其他资源,可以提高计算机的利用率。很多人就不理解了,既然进程这么优秀,为什么还要线程呢?其实,仔细观察就会发现进程还是有很多缺陷的,主要体现在两点上:

  • 进程只能在一个时间干一件事,如果想同时干两件事或多件事,进程就无能为力了。
  • 进程在执行的过程中如果阻塞,例如等待输入,整个进程就会挂起,即使进程中有些工作不依赖于输入的数据,也将无法执行。

如果开一个进程,需要申请内存空间,然后将代码拷贝到申请的内存空间中。这两个过程都是比较耗时。但是开线程是不需要申请内存空间,所以开线程的开销远远小于开进程的开销!!!

进程和线程的关系

线程与进程的区别可以归纳为以下4点:

1)地址空间和其它资源(如打开文件):进程间相互独立,同一进程的各线程间共享。某进程内的线程在其它进程不可见。

2)通信:进程间通信IPC,线程间可以直接读写进程数据段(如全局变量)来进行通信——需要进程同步和互斥手段的辅助,以保证数据的一致性。

3)调度和切换:线程上下文切换比进程上下文切换要快得多。

4)在多线程操作系统中,进程不是一个可执行的实体。

Python代码的执行由Python虚拟机(也叫解释器主循环)来控制。Python在设计之初就考虑到要在主循环中,同时只有一个线程在执行。虽然 Python 解释器中可以“运行”多个线程,但在任意时刻只有一个线程在解释器中运行。
  对Python虚拟机的访问由全局解释器锁(GIL)来控制,正是这个锁能保证同一时刻只有一个线程在运行。
在多线程环境中,Python 虚拟机按以下方式执行:
a、设置 GIL;
b、切换到一个线程去运行;
c、运行指定数量的字节码指令或者线程主动让出控制(可以调用 time.sleep(0));
d、把线程设置为睡眠状态;
e、解锁 GIL;
d、再次重复以上所有步骤。

threading模块

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

开启线程的两种方式

from threading import Thread
import time

def task(name):
    print('%s is running'%name)
    time.sleep(1)
    print('%s is over'%name)

if __name__ == '__main__':
    t = Thread(target=task,args=('dog',))
    t.start()  # 开启线程的速度非常快,几乎代码执行完线程就已经开启
    print('主')
from threading import Thread
import time

class MyThread(Thread):
    def __init__(self,name):
        super().__init__()
        self.name = name
    def run(self):
        print('%s is running' % self.name)
        time.sleep(1)
        print('%s is over'%self.name)

if __name__ == '__main__':
    t = MyThread('cat')
    t.start()
    print('主')

线程之间数据的共享

线程之间的数据是可以进行共享的。

from threading import Thread

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

t = Thread(target=task)
t.start()
t.join()
print(x)
'''
输出:666
'''

线程对象的其他属性和方法

sfrom threading import Thread,active_count,current_thread
import os
import time


def task(name):
    # print('%s is running'%name,os.getpid())
    print('%s is running'%name,current_thread().name,current_thread().getName())
    time.sleep(1)
    print('%s is over'%name)

def info(name):
    print('%s is running' % name, current_thread().name, current_thread().getName())
    time.sleep(1)
    print('%s is over' % name)

t = Thread(target=task,args=('线程1',))
t1 = Thread(target=info,args=('线程2',))
t.start()
t1.start()
t.join()
print(active_count())  # 当前存活的线程数
print(os.getpid())
print(current_thread().name)
print(current_thread().getName())

线程互锁

from threading import Thread,Lock
import time
import random

mutex = Lock()
n = 100

def task():
    global n
    mutex.acquire()
    tmp = n
    time.sleep(0.1)
    n = 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(n)

守护线程

from threading import Thread
from multiprocessing import Process
import time
def foo():
    print(123)
    time.sleep(1)
    print("end123")

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

if __name__ == '__main__':
    # t1=Thread(target=foo)
    # t2=Thread(target=bar)

    t1=Process(target=foo)
    t2=Process(target=bar)
    t1.daemon=True
    t1.start()
    t2.start()
    print("main-------")

    '''
    123
    main-------
    456
    end456
    '''

    '''
    main-------
    123
    456
    end456
    '''

    '''
    main-------
    456
    end456
    '''

全局解释器锁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.)

结论:在Cpython解释器中,同一个进程下开启的多线程,同一时刻只能有一个线程执行,无法利用多核优势

  • GIL其实就是一把互斥锁(牺牲了效率但是保证了数据的安全)。
  • 线程是执行单位,但是不能直接运行,需要先拿到python解释器解释之后才能被cpu执行
  • 同一时刻同一个进程内多个线程无法实现并行,但是可以实现并发
  • 为什么要有GIL是因为它内部的垃圾回收机制不是线程安全的
  • 垃圾回收机制也是一个任务,跟你的代码不是串行运行,如果是串行会明显有卡顿这个垃圾回收到底是开进程还是开线程?肯定是线程,线程肯定也是一段代码,所以想运行也必须要拿到python解释器
  • 假设能够并行,会出现什么情况?一个线程刚好要造一个a=1的绑定关系之前,这个垃圾线程来扫描,矛盾点就来了,谁成功都不对!
  • 也就意味着在Cpython解释器上有一把GIL全局解释器锁
  • 同一个进程下的多个线程不能实现并行但是能够实现并发,多个进程下的线程能够实现并行

Python中的多线程到底有没有用?

Python中的多线程到底有没有用,要看解决的是什么问题了。现在分别有两种任务:

  • 单核情况下:四个任务

  • 多核情况下:四个任务

计算密集型:一个任务要计算十秒,四个进程和四个线程,肯定是进程快

IO密集型:任务都是纯io情况下,线程开销比进程小,肯定是线程好

计算密集型案例代码:

from multiprocessing import Process
from threading import Thread
import os,time
def work():
    res=0
    for i in range(100000000):
        res*=i

if __name__ == '__main__':
    l=[]
    print(os.cpu_count())  # 本机为12核
    start=time.time()
    for i in range(12):
        # p=Process(target=work) #耗时8s多
        p=Thread(target=work) #耗时44s多
        l.append(p)
        p.start()
    for p in l:
        p.join()
    stop=time.time()
    print('run time is %s' %(stop-start))

IO密集型案例代码

from multiprocessing import Process
from threading import Thread
import threading
import os,time
def work():
    time.sleep(2)

if __name__ == '__main__':
    l=[]
    print(os.cpu_count()) #本机为12核
    start=time.time()
    for i in range(400):
        p=Process(target=work) #耗时12s多,大部分时间耗费在创建进程上
        # p=Thread(target=work) #耗时2s多
        l.append(p)
        p.start()
    for p in l:
        p.join()
    stop=time.time()
    print('run time is %s' %(stop-start))

GIL与自定义互斥锁

不同的数据需要加不同的锁才能保证数据的安全,GIL锁只是对线程加锁,对数据并没有加锁的效果

from threading import Thread,Lock
import time

mutex=Lock()
n=100
def task():
    global n
    with mutex:
        temp=n
        time.sleep(0.1)
        n=temp-1

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

    for t in l:
        t.join()
    print(n)
# 对于修改不同的数据,需要加不同的锁进行处理

对于修改不同的数据,需要加不同的锁进行处理

死锁与递归锁

自定义锁一次acquire必须对应一次release,不能连续acquire

递归锁可以连续的acquire,每acquire一次计数加一

from threading import Thread,Lock,RLock
import time

# mutexA=Lock()
# mutexB=Lock()
mutexB=mutexA=RLock()


class Mythead(Thread):
    def run(self):
        self.f1()
        self.f2()

    def f1(self):
        mutexA.acquire()
        print('%s 抢到A锁' %self.name)
        mutexB.acquire()
        print('%s 抢到B锁' %self.name)
        mutexB.release()
        mutexA.release()

    def f2(self):
        mutexB.acquire()
        print('%s 抢到了B锁' %self.name)
        time.sleep(2)
        mutexA.acquire()
        print('%s 抢到了A锁' %self.name)
        mutexA.release()
        mutexB.release()

if __name__ == '__main__':
    for i in range(100):
        t=Mythead()
        t.start()

信号量

自定义的互斥锁如果是一个厕所,那么信号量就相当于公共厕所,门口挂着多个厕所的钥匙。抢和释放跟互斥锁一致

from threading import Thread,Semaphore
import time
import random
sm = Semaphore(5)  # 公共厕所里面有五个坑位,在厕所外面放了五把钥匙

def task(name):
    sm.acquire()
    print('%s正在蹲坑'%name)
    # 模拟蹲坑耗时
    time.sleep(random.randint(1,5))
    sm.release()

if __name__ == '__main__':
    for i in range(20):
        t = Thread(target=task,args=('伞兵%s号'%i,))
        t.start()

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)


if __name__ == '__main__':
    t = Thread(target=light)
    t.start()

    for i in range(10):
        t = Thread(target=car,args=('%s'%i,))
        t.start()

线程queue

同一个进程下的线程数据都是共享的为什么还要用queue?queue本身自带锁的功能,能够保证数据的安全

# 我们现在的q只能在本地使用,后面我们会学基于网络的q 
import queue
先进先出
queue.Queue() #先进先出
q=queue.Queue(3)
q.put(1)
q.put(2)
q.put(3)
print(q.get())
print(q.get())
print(q.get())
后进先出->堆栈
queue.LifoQueue() #后进先出->堆栈
q=queue.LifoQueue(3)
q.put(1)
q.put(2)
q.put(3)
print(q.get())
print(q.get())
print(q.get())
优先级用数字表示,数字越小优先级越高
queue.PriorityQueue() #优先级
q=queue.PriorityQueue(3) #优先级,优先级用数字表示,数字越小优先级越高
q.put((10,'a'))
q.put((-1,'b'))
q.put((100,'c'))
print(q.get())
print(q.get())
print(q.get())

进程池与线程池

实现并发的手段有两种,多线程和多进程。注:并发是指多个任务看起来是同时运行的。主要是切换+保存状态。

进程池内部维护一个进程序列,当使用时,则去进程池中获取一个进程,如果进程池序列中没有可供使用的进进程,那么程序就会等待,直到进程池中有可用进程为止。就是固定有几个进程可以使用。

线程池主要用于:
1)需要大量的线程来完成任务,且完成任务的时间比较短。 比如WEB服务器完成网页请求这样的任务,使用线程池技术是非常合适的。因为单个任务小,而任务数量巨大。但对于长时间的任务,比如一个Telnet连接请求,线程池的优点就不明显了。因为Telnet会话时间比线程的创建时间大多了。
2)对性能要求苛刻的应用,比如要求服务器迅速响应客户请求。
3)接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。

进程池/线程池的使用:

我们需要导入concurrent.futures模块下的ThreadPoolExecutor和ProcessPoolExecutor

from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor
import time
import os
# 实例化池对象
# 不指定参数的情况,默认是当前计算机cpu个数乘以5,也可以指定线程个数
pool = ThreadPoolExecutor(5)
# pool = ProcessPoolExecutor(5)  # 创建进程池,默认是当前计算机的cpu个数,其他与线程池代码一样

def task(n):
    print(n, os.getpid())
    time.sleep(2)
    return n ** 2

if __name__ == '__main__':
    t_ls = []
    for i in range(20):
        future = pool.submit(task, i)
        # print(future)  # 如果在此处直接打印结果,会使得每次执行都要等待结果,变成同步提交
        t_ls.append(future)
    # 关闭池子并且等待池子中所有的任务运行完毕,再去拿列表中的结果
    pool.shutdown()
    for p in t_ls:
        print('>>>:',p.result())
    print('主')

# 输出:
'''
0 6168
1 6168
2 6168
3 6168
4 6168
5 61686 6168
78 6168
9 6168
 6168
>>>: 0
>>>: 1
>>>: 4
>>>: 9
>>>: 16
>>>: 25
>>>: 36
>>>: 49
>>>: 64
>>>: 81
>>>主
>>>'''

以上代码可以实现在子线程执行完后,再统一去拿执行的结果,结果是有序的;但是如果我们想一边执行一边获取结果呢?这时候回调函数就派上了用场。

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

# 实例化池对象
# 不指定参数的情况,默认是当前计算机cpu个数乘以5,也可以指定线程个数
pool = ThreadPoolExecutor(5)  # 创建了一个池子,池子里面有5个线程,进程池和线程池的唯一区别就是这里
# 不指定参数的情况,进程池默认是当前计算机cpu个数
# pool = ProcessPoolExecutor(5)  # 创建了一个池子,池子里面有5个进程

def task(n):
    print(n, os.getpid())
    time.sleep(2)
    return n ** 2
"""
提交任务的方式
    同步:提交任务之后,原地等待任务的返回结果,再继续执行下一步代码
    异步:提交任务之后,不等待任务的返回结果(通过回调函数拿到返回结果并处理),直接执行下一步操作
"""
# 回调函数:异步提交之后一旦任务有返回结果,自动交给另外一个去执行
def call_back(n):
    print("拿到了结果:%s" % n.result())


if __name__ == '__main__':
    t_list = []
    for i in range(20):
        future = pool.submit(task, i).add_done_callback(call_back)  # 异步提交任务,通过回掉函数拿到执行结果
        t_list.append(future)  # 如果在此处直接打印结果print(future.result()),会使得每次执行等待结果,变成同步提交
    # print(t_list)  # 前面回调函数拿走结果之后,列表里面全剩了None
    print('主')

'''
执行结果:
0 8516
1 8516
2 8516
3 8516
4主
 8516
拿到了结果:0
5 8516
拿到了结果:1
6 8516
拿到了结果:4
拿到了结果:9
7 8516
拿到了结果:16
8 8516
9 8516
拿到了结果:25
拿到了结果:36
拿到了结果:49
拿到了结果:81
拿到了结果:64
'''

模拟爬虫:

from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor
from threading import current_thread
import time,random,os
import requests


def get(url):
    print('%s GET %s' %(current_thread().name,url))
    time.sleep(3)
    response=requests.get(url)
    if response.status_code == 200:
        res=response.text
    else:
        res='下载失败'
    return res

def parse(future):
    time.sleep(1)
    res=future.result()
    print('%s 解析结果为%s' %(current_thread().name,len(res)))

if __name__ == '__main__':
    urls=[
        'https://www.baidu.com',
        'https://www.sina.com.cn',
        'https://www.tmall.com',
    ]
     
    p=ThreadPoolExecutor(4)
    
    for url in urls:
        future=p.submit(get,url)
    	# 异步调用:提交完一个任务之后,不在原地等待,而是直接执行下一行代码,会导致任务是并发执行的,,结果futrue对象会在任务运行完毕后自动传给回调函数
        future.add_done_callback(parse)  # parse会在任务运行完毕后自动触发,然后接收一个参数future对象
     
    p.shutdown(wait=True)
     
    print('主',current_thread().name)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值