python进阶之 线程编程

1.进程回顾

之前已经了解了操作系统中进程的概念,程序并不能单独运行,只有将程序装载到内存中,系统为它分配资源才能运行,而这种执行的程序就称之为进程
程序和进程的区别就在于:程序是指令的集合,它是进程运行的静态描述文本进程是程序的一次执行活动,属于动态概念
在多道编程中,我们允许多个程序同时加载到内存中,在操作系统的调度下,可以实现并发地执行。这是这样的设计,大大提高了CPU的利用率。
进程的出现让每个用户感觉到自己独享CPU,因此,进程就是为了在CPU上实现多道编程而提出的。

2.有了进程为什么还需要线程?

进程有很多优点,它提供了多道编程,让我们感觉我们每个人都拥有自己的CPU和其他资源,可以提高计算机的利用率。
很多人就不理解了,既然进程这么优秀,为什么还要线程呢?其实,仔细观察就会发现进程还是有很多缺陷的,主要体现在两点上:   1.进程只能在一个时间干一件事,如果想同时干两件事或多件事,进程就无能为力了。   2.进程在执行的过程中如果阻塞,例如等待输入,整个进程就会挂起,即使进程中有些工作不依赖于输入的数据,也将无法执行,浪费系统资源。
  3.进程的开启和销毁都会消耗较很多的资源

3.线程的基本概念

线程是进程中的执行单位,是操作系统分配cpu时间的最小单位,进程中有很多线程,在进程中的第一个线程称为主线程。
进程:计算机中最小的资源分配单位
线程:计算机中被CPU调度的最小单位

线程(thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
线程是独立调度和分派的基本单位。
同一进程中的多条线程将共享该进程中的全部系统资源,如虚拟地址空间,文件描述符和信号处理等等。但同一进程中的多个线程有各自的调用栈(call stack),自己的寄存器环境(register context),自己的线程本地存储(thread-local storage)。一个进程可以有很多线程,每条线程并行执行不同的任务。

4.为什么要使用线程?

复制代码
1.线程也叫轻量级的进程,最大的特点是同一个进程中的多个子线程是共享一部分数据(一个进程的多个子进程和父进程相互隔离,没有关系)
2.线程的创建\销毁\上线文切换要比进程高效

什么是上下文切换?
  
指的是cpu从一个进程/线程切换到另一个进程/线程的过程
  在操作系统中,CPU切换到另一个进程需要保存当前进程的状态并恢复另一个进程的状态:当前运行任务转为就绪(或者挂起、删除)状态,另一个被选定的就绪任务成为当前任务。上下文切换包括保存当前任务的运行环境,恢复将要运行任务的运行环境

线程的特点:
  1.
轻型实体
    线程中的实体基本上不拥有系统资源,只是有一点必不可少的、能保证独立运行的资源。
    线程的实体包括程序、数据和TCB。线程是动态概念,它的动态特性由线程控制块TCB(Thread Control Block)描述。
    TCB包括以下信息:
      (1)线程状态。
      (2)当线程不运行时,被保存的现场资源。
      (3)一组执行堆栈。
      (4)存放每个线程的局部变量主存区。
      (5)访问同一个进程中的主存和其它资源。用于指示被执行指令序列的程序计数器、保留局部变量、少数状态参数和返回地址等的一组寄存器和堆栈。
  
2.独立调度和分派的基本单位。
    在多线程OS中,线程是能独立运行的基本单位,因而也是独立调度和分派的基本单位。由于线程很“轻”,故线程的切换非常迅速且开销小(在同一进程中的)。
    3.共享进程资源。
      线程在同一进程中的各个线程,都可以共享该进程所拥有的资源,这首先表现在:所有线程都具有相同的进程id,这意味着,线程可以访问该进程的每一个内存资源;此外,还可以访问进程所拥有的已打开文件、定时器、信号量机构等。由于同一个进程内的线程共享内存和文件,所以线程之间互相通信不必调用内核。
   4.可并发执行。
      在一个进程中的多个线程之间,可以并发执行,甚至允许在一个进程中所有线程都能并发执行;同样,不同进程中的线程也能并发执行,充分利用和发挥了处理机与外围设备并行工作的能力。 
复制代码

 5.进程和线程的区别?

复制代码
进程:数据隔离(但也可以使用Manager模块实现数据共享),使用开销大
线程:数据共享(全局变量),使用开销小

通过小的细节来体现进程和线程的区别:
  1.os.getpid()
    进程:父进程和子进程的pid都不同,所以都各自有自己的内存空间
  2.if __name_- == '__main__'
    
进程和线程的创建原理不同,所以不需要if __name__ == '__main__'
    因为新的线程是在主线程的内存中,所以新的线程和主线程共享同一段代码,不需要import导入,也就不存在子线程中又重复一次创建线程的过程

在cpython中多进程可以使用多核,但是多线程不能使用多核
是cpython解释器的原因造成的,因为cpython存在GIL全局解释器锁,导致同一时间内只能有一个线程访问cpu,保证数据安全,但是jpython和pypy就能访问多核的cpu 正常的多个线程和多个进程可不可以利用多核(多个cpu )? 可以,因为cpu执行的是进程中的线程

解释性语言不能多个线程同时访问多个cpu,这样做能保证数据不出错
复制代码

 6.Python中的线程模块

复制代码

Python提供了几个用于多线程编程的模块,包括thread、threading和Queue等。thread和threading模块允许程序员创建和管理线程。thread模块提供了基本的线程和锁的支持,threading提供了更高级别、功能更强的线程管理的功能。Queue模块允许用户创建一个可以用于多个线程之间共享数据的队列数据结构。
  避免使用thread模块,因为更高级别的threading模块更为先进,对线程的支持更为完善,而且使用thread模块里的属性有可能会与threading出现冲突;其次低级别的thread模块的同步原语很少(实际上只有一个),而threading模块则有很多;再者,thread模块中当主线程结束时,所有的线程都会被强制结束掉,没有警告也不会有正常的清除工作,至少threading模块能确保重要的子线程退出后进程才退出。 

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


multiprocess模块的完全模仿了threading模块的接口,二者在使用层面,有很大的相似性 multiprocess 先有的threading模块,没有池的功能 multiprocessing完全模仿threading模块完成的,实现了池的功能 concurrent.futures,实现了线程池\进程池
复制代码

  如何开启一个线程

复制代码
from threading import Thread
import os
def func():
    print('in fucn ',os.getpid())
print('in main ',os.getpid())
Thread(target=func).start()
#两个打印pid是一样的,说明线程是由进程产生的
方法1
复制代码
复制代码
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('kobe')
    t.start()
    print('主线程')
方法2
复制代码

  开启一个多线程

复制代码
import time
from threading import Thread
import os
def func(i):
    time.sleep(1)
    print('in fucn',i,os.getpid())
print('in main',os.getpid())
for i in range(20):
#Thread(target=func,args=(i,)).start()  #问题:多个线程在同一个进程中处理数据的时候,数据需不需要锁?
当然需要,这就是GIL锁存在的意义:同一进程内的同一时刻只能有一个线程访问cpu来修改同一个数据,导致Python中的多线程是‘伪多线程’,所以cpython解释器不能使用多核
View Code
复制代码

  线程的其他方法

复制代码
Thread实例对象的方法
    isAlive(): 返回线程是否活动的。
    getName(): 返回线程名。
    setName(): 设置线程名。

threading模块提供的一些方法:
    threading.currentThread(): 返回当前的线程变量。
    threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行子线程启动后、结束前,不包括启动前和终止后的线程。
    threading.active_count(): 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果
        主线程也是一个线程,每个线程没有被start()的时候们不会被执行
        判断主线程是否结束了    

线程有terminate么?
    没有terminate 不能强制结束
    所有的子线程都会在执行完所有的任务之后自动结束

# 返回一个存储着所有线程对象的列表
from threading import enumerate,Thread
def func():
    print('in son thread')
Thread(target=func).start()
print(enumerate()) 
View Code
复制代码

   通过dis模块查看机器码

复制代码
from dis import dis
def func():
    a =[]
    a.append(1)
dis(func)

注意:cpu在0.9ms内可以处理450w机器码
python解释器 --> 字节码 --> 机器码 
复制代码

  使用线程获取网页源码

复制代码
# -*- coding: utf-8 -*- 
# @Time    : 2019/5/15 17:01 
# @Author  : p0st
# @Site    :  
# @File    : 获取网页源代码.py
# @Software: PyCharm
url_dic = {
    '协程':'http://www.cnblogs.com/Eva-J/articles/8324673.html',
    '线程':'http://www.cnblogs.com/Eva-J/articles/8306047.html',
    '目录':'https://www.cnblogs.com/Eva-J/p/7277026.html',
    '百度':'http://www.baidu.com',
    'sogou':'http://www.sogou.com',
    '4399':'http://www.4399.com',
    '豆瓣':'http://www.douban.com',
    'sina':'http://www.sina.com.cn',
    '淘宝':'http://www.taobao.com',
    'JD':'http://www.JD.com'
}

from threading import Thread
from queue import Queue
import requests
import time
def func(q):
    while 1:
        data = q.get()
        if data==None:
            q.put(None)
            break
        else:
            domain,url = data.split('*')
            code = requests.get(url).content
            with open(domain+".html",'wb') as f:
                f.write(code)

if __name__ == '__main__':
    start = time.time()
    q = Queue()
    # print(len(url_dic))
    for item in url_dic:
        str = item+'*'+url_dic[item]
        q.put(str)
    p1=Thread(target=func,args=(q,))
    p2=Thread(target=func,args=(q,))
    p1.start()
    p2.start()
    q.put(None)
    print(time.time()-start)
View Code
复制代码
总结:
  当时写这段代码的时候没什么问题,后来再看的时候出现了问题。
  问题是:当我在进程中创建两个线程p1,p2之后,主线程还执行不执行任务那??当p1将第一个网页的源码获取之后是获取另外一个网页的源码还是直接死掉?
  1.主线程不执行任务
    可以在获取源码的网页里面,获取下当前线程,threading.current_thread()。可以发现结果并没有主线程。
  2.p1不会死掉,p1还是会重新执行另一个新的任务
    因为线程挂掉的原因是主线程所在的进程挂掉了,才会被回收资源,主线程不会回收子线程的资源,因为线程资源也属于当前进程的资源
    所以,在当前进程还在存活的情况下,p1还是会去执行其他新的任务。

7.线程中守护线程

复制代码
import time
from threading import Thread,activeCount

#守护线程守护的
def daemon_func():
    while True:
        time.sleep(0.5)
        print('守护线程')
def son_func():
    print('start son')
    time.sleep(10)
    print('end son')
    print('子线程:',time.time()-start)

start= time.time()
#子线程
t = Thread(target=daemon_func)
t.daemon = True
t.start()
#子线程
Thread(target=son_func).start()
#主线程
print(activeCount())
time.sleep(3)
print('主线程结束')
print('主线程:',time.time()-start)
线程中的守护线程
复制代码

 无论是进程还是线程,都遵循:守护xx会等待主xx运行完毕后被销毁。需要强调的是:运行完毕并非终止运行
  1.对主进程来说,运行完毕指的是主进程代码运行完毕
  2.对主线程来说,运行完毕指的是主线程所在的进程内所有非守护线程统统运行完毕,主线程才算运行完毕

复制代码
如果你设置一个线程为守护线程,就表示你在说这个线程是不重要的,在进程退出的时候,不用等待这个线程退出。
如果你的主线程在退出的时候,不用等待那些子线程完成,那就设置这些线程的daemon属性。

1.主线程会等待子线程的结束而结束,主线程不会管守护线程的线程是否执行完毕
2.守护线程会随着主线程的结束而结束,注意是运行完毕,并非终止运行,守护线程会守护主线程和所有的子线程
3.整个Python会在所有的非守护线程退出后才会结束,即进程中没有非守护线程存在的时候才结束。(主进程会随着主线程的结束而结束)
 
问题
1.主线程需不需要回收子线程的资源
# 不需要,线程资源属于进程,所以进程结束了,线程的资源自然就被回收了
2.主线程为什么要等待子线程结束之后才结束
# 主线程结束意味着进程进程,进程结束,所有的子线程都会结束,要想让子线程能够顺利执行完,主线程只能等
3.守护线程到底是怎么结束的
# 主线程结束了,主进程也结束,守护线程被主进程的结束(运行完毕)给杀死了 
复制代码

 8.思考:有了GIL锁,线程中还需要锁么?

复制代码
有必要
理论上有了GIL锁会保证同一时间内只有一个线程执行cpu指令,但是运行多线程不加锁的话,会产生数据不安全的情况
有下列代码:
  当tt里面的第一个线程在执行的时候,假如说执行到count =count+1
  但是还没来得及把count放回去的时候,cpu就切换到下一个线程执行,此时的count并没有
  修改,还是0 ,于是就将coun加1,count=1,在切换到上一个线程继续执行,cpu寄存器里面
  存放的上一个线程的状态是要将count+1的结果存回给count,所以在两个线程运行的时候只加了一次
  造成count值不对,也就造成了数据不安全的问题。所以要加上锁
  这种情况主要是由于是cpu切换线程造成的
'''
coun = 0
def func():
count +=1
for i in range(2):
tt = Thread(target=func)
tt.start()
'''
线程中不安全现象:
  1.对全局变量进行修改
  2.对某个值进行+=,-=,*=,/+,%=等等操作
  产生的原因是cpu切换线程造成的

为什么某个值进行+=,-=,*=,/+,%=在cpu切换的时候造成数据不安全的现象?
  因为在count +=1等操作,在cpu的机器码中执行是可再分的,分成两步执行,count +1,count=count+1
  所以在 cpu切换线程的时候,有可能出现两个线程分别对一个数据进行修改,而返回的时候只是返回一个
  但是Python中常用的数据类型,list dict tuple set等修改或增加的方法都具有原子性(不是增加就是修改,就算cpu切换也无所谓)
  
具有原子性也是在cpu底层机器码中体现出来的

注意:
  队列(queue)的原子性比以上的数据类型更强,因为队列的queue.get()获取不到数据的时候回阻塞住,而列表的list.pop()在没有数据时会报错
复制代码

9.锁

复制代码
互斥锁:在同一个线程中连续acquire两次,并且可以做到多个线程被锁的代码同时只有一个线程执行,中就是互斥锁,就是死锁
  
互斥锁与join的区别是:
  互斥锁是随机抢锁的,join是完全串行的。但是二者都是串行的
  mutex是semaphore的一种特殊情况(n=1时)。也就是说,完全可以用后者替代前者。但是,因为mutex较为简单,且效率高,所以在必须保证资源独占的情况下,还是采用这种设计。
  semaphore信号量:用来保证多个线程不会冲突
递归锁:在同一个线程中可以连续acquire多次,并且可以做到多个线程被锁的代码同时只有一个线程执行,但是大部分时候递归锁的情况都能使用互斥锁来解决(通常使用递归锁的代码都是有逻辑上问题的)
  一般情况互斥锁的性能和时间复杂度要比递归所低,但是递归锁一把锁就可以打天下
  递归锁最大的特点是:在同一个线程和进程不会出现死锁现象,并且能连续acquire两次,这是最大的特点

  递归锁就是一把锁,每应用一次计数器就加1,当不为0的时候,其他的线程就可以抢占这个锁了,递归锁是解决死锁的最好办法
死锁现象;创建了两把锁以上,并且交替使用就可能出现死锁
  只要是1把锁,递归锁永远不会死锁
  只要是2把锁以及以上,都有可能产生死锁现象 r1 = r2 = RLock() 这是同一把锁
  只要是2把锁以上,交替使用,递归锁和互斥锁都会产生死锁


1.gil 保证线程同一时刻只能一个线程在解释器中执行,不可能有两个线程同时在解释器上运行
2.lock 锁 保证某一段代码 在没有执行完毕之后,不可能有另一个线程也执


复制代码
复制代码
from threading import RLock,Thread
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()
    print('%s放下叉子了' % name)
    noodle_lock.release()
    print('%s放下面了' % name)

def eat2(name):
    fork_lock.acquire()    # 阻塞 wusir等叉子
    print('%s拿到叉子了' % name)
    noodle_lock.acquire()
    print('%s拿到面了'%name)
    print('%s吃面'%name)
    noodle_lock.release()
    print('%s放下面了' % name)
    fork_lock.release()
    print('%s放下叉子了' % name)

Thread(target=eat1,args = ('alex',)).start()
Thread(target=eat2,args = ('wusir',)).start()
Thread(target=eat1,args = ('baoyuan',)).start()
递归锁
复制代码
复制代码
import os
from threading import Thread,Lock
import time
x = 100
def task(lock):
    with lock:
        global  x
        temp =x
        temp -=1
        x =temp
if __name__ == '__main__':
    li = []
    lock = Lock()
    for item in range(100):
        t = Thread(target=task,args=(lock,))
        li.append(t)
        t.start()
    for ii in li:
        ii.join()
    print(x)
互斥锁 
复制代码
复制代码
from threading import Lock,Thread
import time

noodle_lock = Lock()
fork_lock = Lock()

def eat1(name):
    noodle_lock.acquire()   # 阻塞等面
    time.sleep(0.5)
    print('%s拿到面了'%name)
    fork_lock.acquire()
    print('%s拿到叉子了' % name)
    print('%s吃面'%name)
    fork_lock.release()
    print('%s放下叉子了' % name)
    noodle_lock.release()
    print('%s放下面了' % name)

def eat2(name):
    fork_lock.acquire()    # 阻塞等叉子
    print('%s拿到叉子了' % name)
    noodle_lock.acquire()
    print('%s拿到面了'%name)
    print('%s吃面'%name)
    noodle_lock.release()
    print('%s放下面了' % name)
    fork_lock.release()
    print('%s放下叉子了' % name)

Thread(target=eat1,args = ('admin',)).start()
Thread(target=eat2,args = ('kobe',)).start()
Thread(target=eat1,args = ('jordan',)).start()
死锁现象:
    1.在使用锁之后,可能会出现,两个或多个线程共同的抢占资源,造成的相互等待的现象
    2.连续两次的acquire就会可能发生死锁现象
死锁
复制代码

 

10.队列

Queue就是一个线程队列的类,自带lock锁,实现了线程安全的数据类型
队列是一个线程安全的数据类型,队列在多线程中占有重要的安全位置
from queue import Queue:是线程安全的,和进程无关,进程通信是ipc,线程是共享内存的,二者通信方式不同
复制代码
from queue import Queue
#Queue就是一个线程队列的类,自带lock锁,实现了线程安全的数据类型
#队列是一个线程安全的数据类型,只有几个特点的属性不是安全的

q = Queue()   # 先进先出队列
# 在多线程下都不准
# q.empty() 判断是否为空
# q.full()  判断是否为满
# q.qsize() 队列的大小
#在多进程下都是数据安全的
q.put({1,2,3})
q.put_nowait('abc')
print(q.get_nowait())
print(q.get())
先进先出队列
复制代码
复制代码
# 先进后出的队列 last in first out
from queue import LifoQueue   #线程安全的队列  栈和后进先出的场景都可以用
lfq = LifoQueue()
lfq.put(1)
lfq.put('abc')
lfq.put({'1','2'})
print(lfq.get())
print(lfq.get())
print(lfq.get())
后进先出队列
复制代码
复制代码
from queue import PriorityQueue  # 优先级队列
pq = PriorityQueue()
pq.put((10,'askdhiu'))  #前面的数字就是优先级,越小越优先
pq.put((2,'asljlg'))
pq.put((20,'asljlg'))
print(pq.get())
print(pq.get())
print(pq.get())
优先级队列
复制代码
复制代码
Python的deque模块,它是collections库的一部分。deque实现了双端队列,意味着你可以从队列的两端加入和删除元素。

from collections import deque
# 实例化一个deque对象
d = deque()
# 和list的操作有些类似
d.append('a')
d.append('b')
d.append('c')
print(len(d))
print(d[0])
print(d[-1])
print(d)

从队列两端pop数据
from collections import deque
# 从队列的两端pop数据
d1 = deque('abcde')
print(d1)
d1.popleft()
print(d1)
d1.pop()
print(d1)

我们也可以限制deque中元素的个数,当deque的元素数超过能存放的元素数,它会从相对一端pop元素(默认从左边删除元素)。例如:
from collections import deque
d = deque(maxlen=5)  # 限制元素为5
d.append(1)
d.append(2)
d.append(3)
d.append(4)
d.append(5)
print(d)
d.append(6)
d.append(7)
print(d)
d.append(8)
d.append(9)
d.append(10)
print(d)

我们也可以扩展deque中的元素:
from collections import deque
d = deque([1, 2, 3, 4, 5])
print(d)
d.extendleft([0])
d.extend([6, 7, 8])
print(d)

deque中的方法有:
append(x):把元素x添加到队列的右端
appendleft(x):把元素x添加到队列的左端
clear():清空队列中所有元素
copy():创建队列的浅拷贝
count(x):计算队列中等于x元素的个数
extend(iterable):在队列右端通过添加元素扩展
extendleft(iterable):在队列左端通过添加元素扩展
index(x[, start[, stop]]):返回x元素在队列中的索引,放回第一个匹配,如果没有找到抛ValueError
insert(i, x):在队列的i索引处,插入x元素
pop():移除并返回deque右端的元素,如果没有元素抛IndexError
popleft():移除并返回deque左端的元素,如果没有元素抛IndexError
remove(value):删除第一个匹配value的元素,如果没有找到抛ValueError
reverse():在原地反转队列中的元素
rotate(n):把队列左端n个元素放到右端,如果为负值,右端到左端。如果n为1,等同d.appendleft(d.pop())
maxlen:只读属性,队列中的最大元素数
双端队列
复制代码

 

线程资料

 

 

 

 

 

系列回顾

posted @   thep0st  阅读(98)  评论(0编辑  收藏  举报
(评论功能已被禁用)
编辑推荐:
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
· AI与.NET技术实操系列(六):基于图像分类模型对图像进行分类
点击右上角即可分享
微信分享提示