python并发编程之多线程

目录

一、开启线程的两种方式

练习:

二、线程之间数据共享

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

四、守护线程

五、线程互斥锁

六、GIL全局解释器锁

七、死锁与递归锁

八、信号量

九、event事件

十、定时器

十一、线程queue


进程只是用来把资源集中到一起(进程只是一个资源单位,或者说资源集合),而线程才是cpu上的执行单位。

线程是操作系统能进行运算调度的最小单位,是一串指令的集合。它包含在进程当中。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。

多线程(即多个控制线程)的概念是,在一个进程中存在多个控制线程,多个控制线程共享该进程的地址空间,相当于一个车间内有多条流水线,都共用一个车间的资源。

每个进程都自带一个线程。

开启一个线程的开销要远远小于开进程

开进程:
    申请内存空间,耗时
    将代码拷贝到申请的内存空间中,耗时
开线程:
    不需要申请内存空间,直接开启

进程与线程的区别
1、线程共享内存空间;进程的内存是独立的
2、线程共享进程中的数据;进程独立
3、同一个进程的线程之间可以直接交流(数据共享,传递);两个进程想通信,必须
通过一个中间代理来实现
4、创建新线程很简单,创建新进程需要对其父进程进行一次克隆
5、一个线程可以控制和操作同一进程里的其他线程;但是进程只能操作子进程
6、对一个线程的修改可能会影响其他线程;但对进程的修改不会影响子进程
为什么用多线程呢?

      1. 多线程共享一个进程的地址空间

      2. 线程比进程更轻量级,线程比进程更容易创建可撤销,在许多操作系统中,创建一个线程比创建一个进程要快10-100倍,在有大量线程需要动态和快速修改时,这一特性很有用

      3. 若多个线程都是cpu密集型的,那么并不能获得性能上的增强,但是如果存在大量的计算和大量的I/O处理,拥有多个线程允许这些活动彼此重叠运行,从而会加快程序执行的速度。

      4. 在多cpu系统中,为了最大限度的利用多核,可以开启多个线程,比开进程开销要小的多。(这一条并不适用于python)

案例

开启一个文字处理软件进程,该进程肯定需要办不止一件事情,比如监听键盘输入,处理文字,定时自动将文字保存到硬盘,这三个任务操作的都是同一块数据,因而不能用多进程。只能在一个进程里并发地开启三个线程,如果是单线程,那就只能是,键盘输入时,不能处理文字和自动保存,自动保存时又不能输入和处理文字。

一、开启线程的两种方式

线程开启时,并不需要导入文件,因此可以不写在if __name__ == '__main__'里面。

1、直接开

from threading import Thread
import time

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

t = Thread(target=task, args=('lxx',))
t.start()
print('主')

2、继承类重写run方法

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('lsb')
    t.start()
    print('主')

练习:

1、开启三个线程,一个接收用户输入,一个将用户输入的内容格式化成大写,一个将格式化后的结果存入文件

from threading import Thread
# 一个接收用户输入,一个将用户输入的内容格式化成大写,一个将格式化后的结果存入文件
msg_l = []
format_l = []

def get_input():
    while True:
        msg = input(">>:").strip()
        if len(msg) == 0:
            continue
        if msg == 'q':
            break
        msg_l.append(msg)


def exchange():
    while True:
        if msg_l:
            res = msg_l.pop()
            format_l.append(res.upper())


def save():
    while True:
        if format_l:
            with open('db.txt', 'a', encoding='utf-8') as f:
                res = format_l.pop()
                f.write(res + '\n')


if __name__ == '__main__':
    t1 = Thread(target=get_input)
    t2 = Thread(target=exchange)
    t3 = Thread(target=save)
    t1.start()
    t2.start()
    t3.start()

2、多线程实现并发通信

服务端

from threading import Thread
import socket

server = socket.socket()
server.bind(('127.0.0.1', 8080))
server.listen(5)


def task(conn):
    while True:
        try:
            msg = conn.recv(1024)
            if len(msg)==0:
                break
            print(msg.decode('utf-8'))
            conn.send(msg.upper())
        except ConnectionResetError:
            break


if __name__ == '__main__':
    while True:
        conn,addr = server.accept()
        t = Thread(target=task, args=(conn,))
        t.start()

客户端

import socket

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

while True:
    cmd = input(">>:").encode("utf-8")
    if len(cmd) == 0:
        continue
    client.send(cmd)
    msg = client.recv(1024)
    print(msg.decode("utf-8"))

 

二、线程之间数据共享

all the threads in a process have the same view of the memory.
所有在同一个进程里的线程是共享同一块内存空间的。

from threading import Thread

x = 100

def task():
    global x
    x = 666


t = Thread(target=task)
t.start()
t.join()
print(x)  # 666

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

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


def task(name):
    print("%s is running" % name, os.getpid())  # 获取进程号,2660
    print("%s is running" % name, current_thread().name)  # Thread-1
    time.sleep(1)
    print("%s is over" % name)


def info(name):
    print("%s is running" % name, os.getpid())  # 2660,可以发现他们同属一个进程
    print("%s is running" % name, current_thread().name)  # Thread-2
    time.sleep(1)
    print("%s is over" % name)


t = Thread(target=task, args=('lxx',))
t1 = Thread(target=info, args=('lsb',))
t.start()
t1.start()
t.join()
print(active_count())  # 当前存活的线程数:2(t结束了)
print(os.getpid())  # 2660
print(current_thread().name)  # MainThread
print(current_thread().getName())  # MainThread

四、守护线程

守护线程会等待主线程运行完毕后被销毁

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

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=('lsb',))
    t.daemon = True
    t.start()
    print('主')

# 输出:
# lsb is running
# 主

五、线程互斥锁

涉及到数据安全的问题,线程也需要加锁,保证数据的安全性。加锁会牺牲程序运行的效率。

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)

用多线程互斥锁来模拟抢票:

info.txt文件内容:  {"ticket": 5}

from threading import Thread,Lock
import time
import json
import random
lock = Lock()


def search(i):
    with open('info.txt', 'r', encoding='utf-8') as f:
        data = json.load(f)
    print("用户[%s]查询余票数量为:%s" % (i, data.get('ticket')))
    return data


def buy(i):
    time.sleep(random.randint(1, 3))
    lock.acquire()
    data = search(i)
    # time.sleep(random.randint(1, 3))
    if data.get('ticket') > 0:
        with open('info.txt', 'w', encoding='utf-8') as f:
            data['ticket'] -= 1
            json.dump(data, f)
        print('用户[%s]抢票成功' % i)
    else:
        print('用户[%s]抢票失败' % i)
    lock.release()


if __name__ == '__main__':
    t_ls = []
    for i in range(10):
        t = Thread(target=buy,args=(i,))
        t.start()
        t_ls.append(t)

    for t in t_ls:
        t.join()

六、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存在的原因是Cpython解释器的内存管理不是线程安全的。

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

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

同一个进程下的多个线程不能实现并行但是可以并发,多个进程下的线程能够实现并行。

那么python解释器的多线程是不是就没有用?
四个任务:计算密集型的任务   每个任务耗时10s
单核情况下:
    多线程好一点,消耗的资源少
多核情况下:
    执行四个进程,10s多一点
    执行四个线程,40s多
四个任务:IO密集型的任务   每个任务IO耗时10s
单核情况下:
    多线程好一点
多核情况下:
    多线程好一点
多线程与多进程都有自己的优点,要根据项目需求合理选择

案例:

计算密集型:

def work():
    res = 0
    for i in range(100000000):
        res *= i

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

IO密集型:

def work():
    time.sleep(2)

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

GIL全局解释器锁与普通锁的对比:

对于不同的数据,要想保住安全,需要加不同的锁处理

GIL 与Lock是两把锁,保护的数据不一样,前者是解释器级别的(当然保护的就是解释器级别的数据,比如垃圾回收的数据),后者是保护用户自己开发的应用程序的数据,很明显GIL不负责这件事,只能用户自定义加锁处理,即Lock

from threading import Thread, Lock
import time

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)  # 0

以上代码想得到预期的结果就需要自己加锁,解释器级别的锁并不能保证我们自己的数据的安全性。

七、死锁与递归锁

自定义锁一次acquire必须对应一次release,不能连续acquire
递归锁可以连续的acquire,每acquire一次计数加一:针对的是第一个抢到锁的人

from threading import Thread,Lock,RLock
import time

mutexA = Lock()
mutexB = Lock()

class MyThread(Thread):
    def run(self):
        self.fun1()
        self.fun2()

    def fun1(self):
        mutexA.acquire()
        print("%s 抢到A锁了" % self.name)
        mutexB.acquire()
        print("%s 抢到B锁了" % self.name)
        mutexB.release()
        print("%s 释放B锁了" % self.name)
        mutexA.release()
        print("%s 释放A锁了" % self.name)

    def fun2(self):
        mutexB.acquire()
        print("%s 抢到B锁了" % self.name)
        time.sleep(1)
        mutexA.acquire()
        print("%s 抢到A锁了" % self.name)
        mutexA.release()
        print("%s 释放A锁了" % self.name)
        mutexB.release()
        print("%s 释放B锁了" % self.name)


for i in range(100):
    t = MyThread()
    t.start()

'''
输出:
Thread-1 抢到A锁了
Thread-1 抢到B锁了
Thread-1 释放B锁了
Thread-1 释放A锁了
Thread-1 抢到B锁了
Thread-2 抢到A锁了
原因:
Thread-1线程先抢到了A锁,其他线程就阻塞在A锁处,等待A锁的释放。
Thread-1线程执行完后续操作后释放了A锁,这时其他现场又抢到了A锁,
这时Thread-1线程执行fun2时,抢到B锁后需要抢A锁,而Thread-2线程
抢到了A锁需要抢B锁,谁也不会主动释放,这就造成了死锁的问题。

通过RLock可以解决死锁。
'''

通过递归锁RLock解决死锁

from threading import Thread,Lock,RLock
import time

mutexA = mutexB = RLock()
# 抢锁之后会有一个计数 抢一次计数加一 针对的是第一个抢到锁的人

class MyThread(Thread):
    def run(self):
        self.fun1()
        self.fun2()

    def fun1(self):
        mutexA.acquire()
        print("%s 抢到A锁了" % self.name)
        mutexB.acquire()
        print("%s 抢到B锁了" % self.name)
        mutexB.release()
        print("%s 释放B锁了" % self.name)
        mutexA.release()
        print("%s 释放A锁了" % self.name)

    def fun2(self):
        mutexB.acquire()
        print("%s 抢到B锁了" % self.name)
        time.sleep(1)
        mutexA.acquire()
        print("%s 抢到A锁了" % self.name)
        mutexA.release()
        print("%s 释放A锁了" % self.name)
        mutexB.release()
        print("%s 释放B锁了" % self.name)


for i in range(100):
    t = MyThread()
    t.start()

八、信号量

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

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

普通的互斥锁相当于独立卫生间,一群人抢一个厕所,即一堆线程抢一把锁。

而信号量相当于公共厕所,多个线程抢多把锁。

from threading import Thread, Semaphore
import time
import random

sm = Semaphore(5)  # 5把锁

def task(name):
    sm.acquire()
    print("%s 正在蹲坑" % name)
    # 模拟蹲坑耗时
    time.sleep(random.randint(0, 5))  # 0代表刘志鹏沙雕脱了裤子刚蹲下去放了个屁就站起来了
    sm.release()

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

九、event事件

python线程的事件用于主线程控制其他线程的执行,事件主要提供了三个方法wait、clear、set。

事件处理的机制:全局定义了一个“Flag”,如果“Flag”值为 False,那么当程序执行 event.wait 方法时
就会阻塞,如果“Flag”值为True,那么event.wait 方法时便不再阻塞。

clear:将“Flag”设置为False
set:将“Flag”设置为True

模拟等红绿灯1

from threading import Thread,Event
import time


event = Event()

def light():
    print("红灯亮着")
    time.sleep(3)
    event.set()  # 解除阻塞,给event发信号
    print("绿灯亮了")


def car(i):
    print("%s 正在等红灯" % i)
    event.wait()  # 阻塞
    print("%s 走你" % i)

t1 = Thread(target=light, )
t1.start()

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


'''
红灯亮着
0 正在等红灯
1 正在等红灯
2 正在等红灯
3 正在等红灯
4 正在等红灯
5 正在等红灯
6 正在等红灯
7 正在等红灯
8 正在等红灯
9 正在等红灯
绿灯亮了
4 走你5 走你0 走你2 走你
9 走你8 走你


6 走你
3 走你1 走你
7 走你
'''

模拟等红绿灯2

import time
import threading

event = threading.Event()
def light():
    count = 0
    event.set()  # 设置标志位,相当于先设置绿灯
    while True:
        if count > 5 and count < 10:  # 改红灯
            event.clear()  # 清空标志位
            print("\033[41m 红灯亮了\033[0m")
        elif count > 10:
            event.set()
            count = 0
        else:
            print("\033[42m 绿灯亮了\033[0m")
        time.sleep(1)
        count += 1

def car(name):
    while True:
        if event.is_set():
            print("%s 走你" % name)
            time.sleep(1)
        else:
            print("%s 正在等绿灯" % name)
            event.wait()
            print("%s 绿灯亮了,走你" % name)
l = threading.Thread(target=light)
l.start()
c1 = threading.Thread(target=car, args=("BWM", ))
c2 = threading.Thread(target=car, args=("AODI", ))
c1.start()
c2.start()

十、定时器

定时器,指定n秒后执行某操作

from threading import Timer
 
 
def hello():
    print("hello, world")
 
t = Timer(1, hello)
t.start()  # after 1 seconds, "hello, world" will be printed

十一、线程queue

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

import queue

# 先进先出
q = queue.Queue(3)
q.put(1)
q.put(2)
q.put(3)
print(q.get())
print(q.get())
print(q.get())

# 后进先出
q = queue.LifoQueue()
q.put(1)
q.put(2)
q.put(3)
print(q.get())
print(q.get())
print(q.get())

# 可设置优先级
q = queue.PriorityQueue()
q.put((10,'a'))
q.put((-1,'b'))
q.put((100,'c'))
print(q.get())
print(q.get())
print(q.get())

 

你可能感兴趣的:(python基础)