python 并发编程之多线程

一、线程理论

1.什么是线程

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

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

2.进程与线程的区别

  • 同一进程内的多个线程共享该进程内的地址资源

  • 创建线程的开销要远小于创建进程的开销(创建一个进程,就是创建一个车间,涉及到申请空间,而且在该空间内建至少一条流水线,但创建线程,就只是在一个车间内造一条流水线,无需申请空间,所以创建开销小)

3.多线程应用举例

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

二、开启线程的两种方式

方式一:

import time, random
from threading import Thread

def task(name):
    print('%s is running' %name)
    time.sleep(random.randrange(1, 3))
    print('%s running end' %name)


if __name__ == '__main__':
    t1 = Thread(target=task, args=('gudon', ))
    t1.start()
    print('主线程。。。')
    
    
---------------------------打印结果-----------------------------
gudon is running
主线程。。。
gudon running end

方式二:

import time, random
from threading import Thread

class MyThread(Thread):
    def __init__(self, name):
        super().__init__()
        self.name = name

    def run(self):
        print('%s is running' %self.name)
        time.sleep(random.randrange(1,3))
        print('%s running end' %self.name)


if __name__ == "__main__":
    t1 = MyThread('Astro')
    t1.start()
    print('主线程......')
    
---------------------------打印结果-----------------------------
Astro is running
主线程......
Astro running end

multprocess 、threading 两个模块在使用方式上相似性很大

二、进程与线程的区别

1.开线程的开销远小于开进程的开销

进程:

import time, random
from threading import Thread
from multiprocessing import Process


def task(name):
    print('%s is running' %name)
    time.sleep(random.randrange(1, 3))
    print('%s running end' %name)


if __name__ == '__main__':
    p = Process(target=task, args=('Astro', ))
    p.start() # p.start ()将开启进程的信号发给操作系统后,操作系统要申请内存空间,让好拷贝父进程地址空间到子进程,开销远大于线程
    print('主...........')
    

---------------------------【进程】打印结果-----------------------------
主...........
Astro is running
Astro running end

线程:

import time, random
from threading import Thread
from multiprocessing import Process

def task(name):
    print('%s is running' %name)
    time.sleep(random.randrange(1, 3))
    print('%s running end' %name)


if __name__ == '__main__':
    t1 = Thread(target=task, args=('Astro', ))
    t1.start() #几乎是t.start ()的同时就将线程开启了,线程的创建开销要小鱼进程创建的开销
    print('主...........')
    
---------------------------【线程】打印结果-----------------------------

Astro is running
主...........
Astro running end

2.同一进程内的多个线程共享该进程的地址空间

from threading import Thread
from multiprocessing import Process

n = 100
def task():
    global n
    n = 0

if __name__ == "__main__":
    p1 = Process(target=task)
    p1.start()
    p1.join()

    print('主 %s' %n)  
    
---------------------------打印结果-----------------------------
主 100

子进程 p1 创建的时候,会把主进程中的数据复制一份,进程之间数据不会互相影响,所以子进程p1 中 n=0 后,只是子进程中的 n 改了,主进程的 n 不会受影响 ,即 进程之间地址空间是隔离的

from threading import Thread
from multiprocessing import Process

n = 100
def task():
    global n
    n = 0

if __name__ == "__main__":
    t1 = Thread(target=task)
    t1.start()
    t1.join()

    print('主 %s' %n)

---------------------------打印结果-----------------------------
主 0

同一进程内的线程之间共享进程内的数据,所以为 0

3. pid

pid 就是 process id ,进程的id号。

开多个进程,每个进程都有不同的pid

from multiprocessing import Process, current_process
from threading import Thread
import os

def task():
   # print('子进程...', current_process().pid)  # 也可以使用 os.getpid()或 current_process().pid 来查看当前进程的pid,os.getppid() 可以查看当前进程的父进程的pid
    print('子进程PID:%s  父进程的PID:%s' % (os.getpid(), os.getppid()))

if __name__ == '__main__':
    p1 = Process(target=task)
    p1.start()

    print('主线程', current_process().pid)
    
    
---------------------------打印结果-----------------------------
主线程 808
子进程PID:7668  父进程的PID:808

在主进程下开启多个线程,每个线程都跟主进程的pid一样

from threading import Thread
import os
def task():
    print('子线程pid:',os.getpid())

if __name__ == '__main__':
    t1 = Thread(target=task, )
    t1.start()

    print('主线程pid:', os.getpid())

---------------------------打印结果-----------------------------
子线程pid: 9084
主线程pid: 9084

三、Thread对象的其他属性或方法

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

threading模块提供的一些方法:
  # threading.currentThread(): 返回当前的线程变量。
  # threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。
  # threading.activeCount(): 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果。
from threading import Thread, currentThread,active_count,enumerate
import time
def task():
    print('%s is running '%currentThread().getName())
    time.sleep(2)
    print('%s is done' %currentThread().getName())


if __name__ == "__main__":
    t = Thread(target=task, )
    # t = Thread(target=task, name='子线程001')  # 也可以在这里改子线程名字
    t.start()
    # t.setName('子线程001')
    t.join()
    currentThread().setName('主线程')
    print(active_count())  # 返回正在运行的线程数量
    print(enumerate())  # [<_MainThread(主线程, started 6904)>]     默认为:[<_MainThread(MainThread, started 6904)>]
    
    
---------------------------打印结果-----------------------------
子线程001 is running 
子线程001 is done
1
[<_MainThread(主线程, started 6432)>]

四、守护线程与互斥锁

1.守护线程

无论是进程还是线程,都遵循:守护xxx会等待主xxx运行完毕后被销毁

1、对主进程来说,运行完毕指的是主进程代码运行完毕

主进程在其代码结束后就已经算运行完毕了(守护进程在此时就被回收),然后主进程会一直等非守护的子进程都运行完毕后回收子进程的资源(否则会产生僵尸进程),才会结束,

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

主线程在其他非守护线程运行完毕后才算运行完毕(守护线程在此时就被回收)。因为主线程的结束意味着进程的结束,进程整体的资源都将被回收,而进程必须保证非守护线程都运行完毕后才能结束。
from threading import Thread
import time

def sayHi(name):
    time.sleep(2)
    print('%s say Hello')

if __name__ == '__main__':
    t = Thread(target=sayHi, args=('Astro', ))
    t.setDaemon(True)  # 设置为守护线程, t.daemon = True 也可以
    t.start()

    print('主线程')
    print(t.is_alive())
    
----------------------执行结果---------------------------
主线程
True

t.start() 后,系统会马上创建出一条子线程,但是由于子线程中 time.sleep(2) ,2秒的时间对计算机来说已经很长了,所以在

print('主线程')
print(t.is_alive())

执行后,主线程结束,子线程跟着就结束了,因为我们设置了该子线程为守护线程

在看一个例子:

def foo():
    print('foo runing..')
    time.sleep(1)
    print('foo end')

def bar():
    print('bar running..')
    time.sleep(3)
    print('bar end')


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

    t1.setDaemon(True)
    t1.start()
    t2.start()

    print('main.......')
    
----------------------执行结果---------------------------   
foo runing..
bar running..
main.......
foo end
bar end

t1 为守护进程,t1.start() 后,马上执行 foo函数,然后 time.sleep(1)

此时 t2.start() 开启了线程,执行 bar函数后 time.sleep(3)

3秒的时间内,执行了主线程的 print('main......') , 然后主线程任务已经完成,但是由于 子线程t2 还未执行完毕,t2 非守护线程,主线程还需要等待 非守护线程t2运行完毕后才能结束,所以在等待 t2结束的时间里,t1 线程执行完毕,t1只sleep 1秒,时间上足够t1 先执行了

然后t2 三秒后执行自己剩下的部分

2.互斥锁

from threading import Thread, Lock
import time

n = 100
def task(mutex):
    global n
    mutex.acquire()  # 加互斥锁
    temp = n
    time.sleep(0.1)
    n = temp - 1
    mutex.release()

if __name__ == '__main__':
    mutex = Lock()
    t_list = []
    for i in range(100):
        t = Thread(target=task, args=(mutex, ))
        t_list.append(t)
        t.start()

    for t in t_list:
        t.join()

    print('main....',n)
 
----------------------执行结果---------------------------
main.... 0

如果不加互斥锁的情况下,得到的结果是 main.... 99

因为,在循环中 t.start() 的时候,100个线程会立马创建出来,然后在函数中,100个线程的 temp都被赋值为了 100,所以 n = temp - 1 只是循环的被赋值为 99 而已

另外,互斥锁只保证里局部的串行,与join 不一样

五、GIL 全局解释器锁

运行这段代码

import os, time
print(os.getpid())  # 5132
time.sleep(1000)

然后cmd 查看一下 tasklist |findstr python

结果:

python.exe                    5132 Console                    1      9,568 K

猜你喜欢

转载自www.cnblogs.com/friday69/p/9615654.html