并发编程 之 线程

线程

​ 进程:资源单位
​ 线程:执行单位
​ 注意:每一个进程中都会自带一个线程

有了进程为什么要有线程

进程有很多优点,它提供了多道编程,让我们感觉我们每个人都拥有自己的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=('egon',))
    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('jason')
    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)接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。

socket服务端实现并发

import socket
from threading import Thread

server = socket.socket()
server.bind(('127.0.0.1',8080))
server.listen(5)  # 半连接池

def communicate(conn):
    while True:
        try:
            data = conn.recv(1024)  # 阻塞
            if len(data) == 0:break
            print(data)
            conn.send(data.upper())
        except ConnectionResetError:
            break
    conn.close()

while True:
    conn,addr = server.accept()  # 阻塞
    print(addr)
    t = Thread(target=communicate,args=(conn,))
    t.start()

客户端

import socket

client = socket.socket()
client.connect(('127.0.0.1',8080))

while True:
    info = input('>>>:').encode('utf-8')
    if len(info) == 0:continue
    client.send(info)
    data = client.recv(1024)
    print(data)

猜你喜欢

转载自blog.csdn.net/linwow/article/details/89928551