python进阶之 线程编程

1.进程回顾

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

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

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

3.线程的基本概念

线程是进程中的一个单位
进程:计算机中最小的资源分配单位
线程:计算机中被CPU调度的最小单位

4.为什么要使用线程?

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

 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中的线程模块

threading 和 multiprocessing在使用上基本相同
先有的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解释器不能使用多核

  线程的其他方法

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()) 

   通过dis模块查看机器码

from dis import dis
def func():
    a =[]
    a.append(1)
dis(func)

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

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两次,并且可以做到多个线程被锁的代码同时只有一个线程执行,中就是互斥锁,就是死锁
递归锁:在同一个线程中可以连续acquire多次,并且可以做到多个线程被锁的代码同时只有一个线程执行,但是大部分时候递归锁的情况都能使用互斥锁来解决(通常使用递归锁的代码都是有逻辑上问题的)  一般情况互斥锁的性能和时间复杂度要比递归所低,但是递归锁一把锁就可以打天下  递归锁最大的特点是:在同一个线程和进程不会出现死锁现象,并且能连续acquire两次,这是最大的特点  
死锁现象;创建了两把锁以上,并且交替使用就可能出现死锁  只要是1把锁,递归锁永远不会死锁  只要是2把锁以及以上,都有可能产生死锁现象 r1 = r2 = RLock() 这是同一把锁  只要是2把锁以上,交替使用,递归锁和互斥锁都会产生死锁

1.gil 保证线程同一时刻只能一个线程访问CPU,不可能有两个线程同时在CPU上执行指令2.lock 锁  保证某一段代码 在没有执行完毕之后,不可能有另一个线程也执
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()

死锁

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()

递归锁

 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({'})
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())

优先级队列

线程资料

系列回顾

上一篇:2)Javascript设计模式:Singleton模式


下一篇:Oracle使用备忘