多线程编程

文章目录

  • 一、进程与线程区别
  • 二、进程
    • 2.1 进程
    • 2.2 进程执行带参数的任务
  • 三、线程
    • 3.1 线程
    • 3.2 线程执行带参数的任务
    • 3.3 子消主消
    • 3.4 主消子毁
    • 3.5 共享全局变量引发问题(互斥,死锁)
    • 3.6 死锁

一、进程与线程区别

多进程开发比单进程多线程开发稳定性要强。多进程比多线程消耗的资源多,但比多线程稳定,某些进程挂了不会影响其他进程
进程

操作系统进行资源分配的基本单位
每个进程至少都有一个线程,可以有多个线程
可以使用多核,资源开销大
进程之间不共享全局变量 (线程会共享)
主进程会等待所有的子进程执行结束再结束

线程

cpu调度的基本单位
线程之间执行是交替无序的
不能使用多核,资源开销小
主线程会等待所有的子线程执行结束再结束(主线程执行完了但没结束,在等待子线程结束后再结束)
线程之间共享全局变量 (进程不共享)(注意资源禁止问题,解决方法:互斥锁或者线程同步)
线程之间共享全局变量数据出现错误问题

二、进程

2.1 进程

并发:多个任务交替进程
并行:多个任务同时进行

进程是操作系统进行资源分配的基本单位,是Python程序中实现多任务的一种方式

multiprocessing.Process([group[, target[, name[, args[, kwargs]]]]])(中括号为可选参数):

group:指定进程组,目前只能使用None
target:执行的目标任务名(函数名)
name:进程名字
args:以元组方式给执行任务传参
kwargs:以字典方式给执行任务传参

Process创建的实例对象的常用方法

start():启动子进程实例(创建子进程)
join():等待子进程执行结束

举例:两个函数交替并行进行

# 首先导入进程包
import multiprocessing
import time

# 指定任务函数
def task1():
    for i in range(5):
        print('A --', i+1)
        time.sleep(1)

def task2():
    for i in range(5):
        print('B --', i+1)
        time.sleep(1)


if __name__ == '__main__':
    # 创建子进程对象,  target指定执行的任务为task1,task1后面的函数不能加括号
    p1 = multiprocessing.Process(target=task1)
    p2 = multiprocessing.Process(target=task2)
    # 两个交替进行
    # 启动子进程
    p1.start()
    p2.start()

'''
A -- 1
B -- 1
A -- 2
B -- 2
A -- 3
B -- 3
A -- 4
B -- 4
A -- 5
B -- 5
'''

获取进程编号的目的是验证主进程和子进程的关系,可以得知子进程由那个主进程创建出来的。获取进程编号有两种操作:获取当前进程标号,获取当前父进程编号

获取当前进程编号:os.getpid()
获取当前父进程编号:os.getppid()

举例:获取进程ID,子进程的父进程的ID和父进程ID是一样的

import multiprocessing
import time
import os

def task1():
    print(f"任务1的PID:{os.getpid()} 父进程的PID是{os.getppid()}")
    time.sleep(1)

def task2():
    print(f"任务2的PID:{os.getpid()} 父进程的PID是{os.getppid()}")
    time.sleep(1)


if __name__ == '__main__':
    # 主进程PID
    print(f"主进程的PID:{os.getpid()} 父进程的PID是{os.getppid()}")

    # 创建子进程对象
    p1 = multiprocessing.Process(target=task1)
    p2 = multiprocessing.Process(target=task2)

    # 启动子进程
    p1.start()
    p2.start()

'''
主进程的PID:8268 父进程的PID是7564
任务1的PID:4452 父进程的PID是8268
任务2的PID:13444 父进程的PID是8268
'''

举例:设置进程名

if __name__ == '__main__':

    # 创建子进程对象
    p1 = multiprocessing.Process(target=task1, name='P1')  # 进程名为P1

    print(p1)
    # 启动子进程
    p1.start()
    print(p1)
'''  两个输出结果中前者initial没开始,后者started开始执行


'''

举例:获取当前进程对象

import multiprocessing
import time

def task1():
    # 获取当前进程对象
    mp = multiprocessing.current_process()
    print('task1: ', mp)    # task1:  
    time.sleep(1)


if __name__ == '__main__':
    # 获取当前进程对象
    # 在那个进程里面就是获取那个进程对象
    mp = multiprocessing.current_process()
    print(mp)        # <_MainProcess name='MainProcess' parent=None started>
    print(mp.name)   # MainProcess

    # 创建子进程对象
    p1 = multiprocessing.Process(target=task1)

    # 启动子进程
    p1.start()
'''
<_MainProcess name='MainProcess' parent=None started>
MainProcess
task1:  
'''

2.2 进程执行带参数的任务

给参方式:
args:以元组方式给执行任务传参
kwargs:以字典方式给执行任务传参

import multiprocessing
import time


def task1(count):   # 如果多个参数则(count, content), 下面传入(3,‘hello’)
    for i in range(count):
        print('P1 working ... ')
        time.sleep(0.5)
    else:
        print("P1 end !!!")

def task2(count, content):
    for i in range(count):
        print('P2 working ... ',content)
        time.sleep(0.5)
    else:
        print("P2 end !!!")


if __name__ == '__main__':
    # 创建子进程对象
    # args传参  
    p1 = multiprocessing.Process(target=task1, args=(3,))  # 一个参数时逗号里面的逗号不能省
    # kwargs传参
    p2 = multiprocessing.Process(target=task2, kwargs={"count": 3, "content": 'hello'})

    # 启动子进程
    p1.start()
    p2.start()
'''
P1 working ... 
P2 working ...  hello
P1 working ... 
P2 working ...  hello
P1 working ... 
P2 working ...  hello
P1 end !!!
P2 end !!!
'''

join可以控制进程树顺序,原本P1、P2是交替进行,使用join可以让P2在P1结束之后在进行

import multiprocessing
import time
import os



def task1():
    for i in range(3):
        print("P1 进程")
        time.sleep(0.5)
    else:
        print("P1 进程结束")

def task2():
    for i in range(3):
        print("P2 进程")
        time.sleep(0.5)
    else:
        print("P2 进程结束")

if __name__ == '__main__':
    # 创建子进程对象
    p1 = multiprocessing.Process(target=task1)
    p2 = multiprocessing.Process(target=task2)

    # 启动子进程
    p1.start()

    # 将子进程加入到当前进程之前
    # 当前进程会等待p1进程执行完成后,再继承向后执行
    p1.join()
    p2.start()
'''
P1 进程
P1 进程
P1 进程
P1 进程结束
P2 进程
P2 进程
P2 进程
P2 进程结束
'''

一般来说,主进程会等待所有的子进程执行结束再结束,比如下面这个例子,主进程只需要1秒就可以执行完,子进程需要2.5秒,主进程运行完成后子进程还在进行,主进程也就还没结束(主进程最后一句有输出不代表结束),主进程在等子进程结束之后在结束,代码案例:

import multiprocessing
import time

def task1():
    for i in range(5):
        print("子进程")
        time.sleep(0.5)
    else:
        print("子进程结束")


if __name__ == '__main__':
    # 创建子进程对象
    p1 = multiprocessing.Process(target=task1)

    # 启动子进程
    p1.start()

    time.sleep(1)
    print("主进程结束")  # 虽然这一句不是最后输出,但输出之后主进程还没结束,在等待子进程结束而结束
'''
子进程
子进程
主进程结束
子进程
子进程
子进程
子进程结束
'''

如果想要主进程结束之后销毁子进程(父进程结束子进程跟着结束),下面两种方法设置(原本子进程要输出5次,却只输出了两次),代码如下:

import multiprocessing
import time


def task1():
    for i in range(5):
        print("P1 进程")
        time.sleep(0.5)
    else:
        print("P1 进程结束")

if __name__ == '__main__':
    # 创建子进程对象
    p1 = multiprocessing.Process(target=task1)
    
    # 方式二
    # 设置守护进程注意: 设置进程时,需在开始进程之前设置
    # p1.daemon = True

    # 启动子进程
    p1.start()

    time.sleep(1)
    # 方式一
    # 在主进程结束之前,手动调用方法结束子进程
    p1.terminate()


    print("主进程结束") 

'''
P1 进程
P1 进程
主进程结束
'''

三、线程

全局解释器锁(GIL):用来保证同时只能有一个线程运行,执行线程前设置GIL,执行结束解锁GIL
推荐使用threading模块,不建议使用thread模块的原因之一是在主线程退出后,所有的其他线程在没有清理的情况下直接退出,threading会确保在重要的子进程在进程结束之后在结束主线程,保持整个进程的存活

守护线程:进程结束时不需要等待这个线程结束,可以设置为守护线程标记。如果某线程设置为守护线程,表示该线程不重要,进程退出时不需要等待该线程。即,如果主线程准备退出,不需要等待某子线程结束,就可以为这些子线程设置为守护进程标记,标记值为真时表示不重要,或者说该线程只是用来等待客户端请求而不做任何其他事情。要设置守护线程需要在启动线程之前设置如下语句:thread.deamon = True。要查看线程守护状态也是检查该值,一个新的子线程会继承父线程的守护标记。主线程会在所有非守护线程结束之后才会退出

3.1 线程

cpu调度的基本单位
线程之间执行是交替无序的
不能使用多核,资源开销小
主线程会等待所有的子线程执行结束再结束(主线程执行完了但没结束,在等待子线程结束后再结束)
线程之间共享全局变量 (进程不共享)(注意资源禁止问题,解决方法:互斥锁或者线程同步)
线程之间共享全局变量数据出现错误问题

线程是实现多任务的另外一种方式。线程是cpu调度的基本单位,每个进程至少都有一个线程

threading.Thread([group[, target[, name[, args[, kwargs]]]]])(中括号为可选参数):

group:指定线程组,目前只能使用None
target:执行的目标任务名
name:进程名字
args:以元组方式给执行任务传参
kwargs:以字典方式给执行任务传参

Process创建的实例对象的常用方法

start():启动子线程实例(创建子进程)
join():等待子线程执行结束
import threading
import time

# 指定任务函数
def task1():
    for i in range(5):
        print('A --', i+1)
        time.sleep(1)

def task2():
    for i in range(5):
        print('B --', i+1)
        time.sleep(1)


if __name__ == '__main__':
    # 创建线程
    p1 = threading.Thread(target=task1)
    p2 = threading.Thread(target=task2)

    # 启动线程   两个交替随机进行,P1后面不一定是P2执行
    p1.start()
    p2.start()
'''
A -- 1
B -- 1
A -- 2
B -- 2
A -- 3
B -- 3
A -- 4
B -- 4
A -- 5
B -- 5
'''

举例:设置线程名

if __name__ == '__main__':
        # 创建线程对象
        p1 = threading.Thread(target=task1, name='P1')  # 线程名为P1

        print(p1)
        # 启动线程
        p1.start()
        print(p1)
'''


'''

举例:获取当前进程对象

import threading
import time

def task1():
    # 获取当前线程对象
    mp = threading.current_thread()
    print('task1: ', mp)    # task1:  
    time.sleep(1)


if __name__ == '__main__':
    # 获取当前线程对象
    # 在哪个线程里面就是获取那个线程对象
    mp = threading.current_thread()
    print(mp)        # <_MainThread(MainThread, started 15336)>
    print(mp.name)   # MainThread

    # 创建线程对象
    p1 = threading.Thread(target=task1)

    # 启动线程
    p1.start()
'''
<_MainThread(MainThread, started 15336)>
MainThread
task1:  
'''

3.2 线程执行带参数的任务

给参方式:
args:以元组方式给执行任务传参
kwargs:以字典方式给执行任务传参

import threading
import time


def task1(count):   # 如果多个参数则(count, content), 下面传入(3,‘hello’)
    for i in range(count):
        print('P1 working ... ')
        time.sleep(0.5)
    else:
        print("P1 end !!!")

def task2(count, content):
    for i in range(count):
        print('P2 working ... ', content)
        time.sleep(0.5)
    else:
        print("P2 end !!!")


if __name__ == '__main__':
    # 创建线程对象
    # args传参
    p1 = threading.Thread(target=task1, args=(3,))  # 一个参数时逗号里面的逗号不能省
    # kwargs传参
    p2 = threading.Thread(target=task2, kwargs={"count": 3, "content": 'hello'})

    # 启动线程
    p1.start()
    p2.start()
'''
P1 working ... 
P2 working ...  hello
P1 working ... 
P2 working ...  hello
P1 working ... 
P2 working ...  hello
P1 end !!!
P2 end !!!
'''

3.3 子消主消

一般来说,主线程会等待所有的子线程执行结束再结束,比如下面这个例子,主线程只需要1秒就可以执行完,子线程需要2.5秒,主线程运行完成后子线程还在进行,主线程也就还没结束(主线程最后一句有输出不代表结束),主线程在等子线程结束之后在结束,代码案例:

import threading
import time


def task1():
    for i in range(5):
        print("子线程")
        time.sleep(0.5)
    else:
        print("子线程结束")


if __name__ == '__main__':
    # 创建子线程对象
    p1 = threading.Thread(target=task1)

    # 启动子线程
    p1.start()

    time.sleep(1)
    print("主线程结束")  # 虽然这一句不是最后输出,但输出之后主线程还没结束,在等待子线程结束而结束
'''
子线程
子线程
子线程
主线程结束
子线程
子线程
子线程结束
'''

3.4 主消子毁

如果想要主线程结束之后销毁子线程(父线程结束子线程跟着结束),可以通过参数daemon=True方法设置(原本子线程要输出5次,却只输出了两次),代码如下:

import threading
import time


def task1():
    for i in range(5):
        print("P1 线程")
        time.sleep(0.5)
    else:
        print("P1 线程结束")


if __name__ == '__main__':
    # 创建子线程对象
    # 方式一
    p1 = threading.Thread(target=task1, daemon=True)

    # 启动子线程
    p1.start()
    time.sleep(1)
    print("主线程结束") 
'''
P1 线程
P1 线程
主线程结束
'''

3.5 共享全局变量引发问题(互斥,死锁)

比如定义一个变量,当多个线程对同一个变量同时进行修改的时候,可能会引发错误,举个例子,定义一个全局变量,两个线程分别对该变量进行+1,执行一百万次,正常来说得到的结果是两百万,结果小于两百万(使用全局变量需要使用global声明

import threading
import time


sum = 0
def task1():
    # 全局变量需要声明
    global sum
    for i in range(1000000):
        sum += 1
    t = threading.current_thread()
    print(f'{t.name} : {sum}')


if __name__ == '__main__':
    # 创建子线程对象
    p1 = threading.Thread(target=task1)
    p2 = threading.Thread(target=task1)

    # 启动子线程
    p1.start()
    p2.start()

    time.sleep(3)
    print("sum: ", sum)

解决该问题可以通过添加一个变量用来控制同一时刻只能有一个线程可以操作全局变量,不能多个同时进行(互斥锁:简单理解就是多个线程一起抢,抢到的先执行,没抢到的则等待使用者释放)

互斥锁的作用就是保证同一时刻只能有一个线程去操作共享数据,保证共享数据不会出现错误问题
使用互斥锁的好处确保某段关键代码只能由一个线程从头到尾完整地去执行
使用互斥锁会影响代码的执行效率,多任务改成了单任务执行
互斥锁如果没有使用好容易出现死锁的情况
import threading
import time


sum = 0
# 创建互斥锁
mutex = threading.Lock()

def task1():
    # 全局变量需要声明
    global sum

    for i in range(1000000):
        # 上锁
        mutex.acquire()
        sum += 1
        # 释放锁
        mutex.release()
        

if __name__ == '__main__':
    # 创建子线程对象
    p1 = threading.Thread(target=task1)
    p2 = threading.Thread(target=task1)

    # 启动子线程
    p1.start()
    p2.start()

    time.sleep(3)
    print("sum: ", sum)

3.6 死锁

死锁:一直等待对方释放锁的情景就是死锁,比如有两个人,两根筷子,一个人一次只能拿一根筷子,要拿到两根筷子之后才会放下,不然一直拿着,现在两个人都只拿到了一根筷子,在等另一个人放下,都没释放都在等待造成死锁,代码如下(运行一直不会结束)(哲学家死锁):

import threading
import time

sum = 0
# 创建互斥锁
A = threading.Lock()
B = threading.Lock()


def task1():
    A.acquire()
    print("甲获得第一根筷子,等待第二根")
    time.sleep(1)
    B.acquire()
    print("甲获得第二根筷子,吃一口饭,准备释放")
    time.sleep(0.5)
    B.release()
    A.release()

def task2():
    B.acquire()
    print("乙获得第一根筷子,等待第二根")
    time.sleep(1)
    A.acquire()
    print("乙获得第二根筷子,吃一口饭,准备释放")
    time.sleep(0.5)
    A.release()
    B.release()

if __name__ == '__main__':
    # 创建子线程对象
    p1 = threading.Thread(target=task1)
    p2 = threading.Thread(target=task2)

    # 启动子线程
    p1.start()
    p2.start()

有一种情况使用了acquire()但没使用release()也会造成死锁(比如某判断不符合退出却没有释放),代码如下:

import threading
import time

# 创建互斥锁
A = threading.Lock()

def task1(a):
    A.acquire()
    if a > 2:
        # 如果不释放会造成死锁
        A.release()
        return
    A.release()

if __name__ == '__main__':
    # 创建子线程对象
    for i in range(5):
        p1 = threading.Thread(target=task1,args=(i,))
        p1.start()

你可能感兴趣的:(Python,python)