天天看点

Python 多任务 - 线程的理解

什么是多任务?

简单地说,就是操作系统可以同时运行多个任务。实现多任务有多种方式,线程、进程、协程。

并行和并发的区别?

并发:指的是任务数多余cpu核数,通过操作系统的各种任务调度算法,实现用多个任务“一起”执行(实际上总有一些任务不在执行,因为切换任务的速度相当快,看上去一起执行而已)

并行:指的是任务数小于等于cpu核数,即任务真的是一起执行的

真的多任务叫并行,假的多任务叫并发。

什么是线程?

可以简单理解为同一进程中有多个计数器,每个线程的执行时间不确定,而每个进程的时间片相等,线程是操作系统调度执行的最小单位。

python的thread模块是比较底层的模块,python的threading模块是对thread做了一些包装的,可以更加方便的被使用。

demo

import time
import threading


def sing():
    """唱歌 5秒钟"""
    for i in range(5):
        print("----正在唱:菊花茶----")
        time.sleep(1)


def dance():
    """跳舞 5秒钟"""
    for i in range(5):
        print("----正在跳舞----")
        time.sleep(1)


def main():
    t1 = threading.Thread(target=sing)
    t2 = threading.Thread(target=dance)
    t1.start()
    t2.start()


if __name__ == "__main__":
    main()

           
Import threading

# 创建一个线程对象

t1 = threading.Thread(target=func_name, args=(num,), name=”子线程名字”)

# 创建一个线程并启动

t1.start()

# 等待子线程执行完毕之后再继续向下执行主线程

t1.join()

"""
备注:主线程会等待子线程结束之后才会结束,主线程一死,子线程也会死。线程的调度是随机的,并没有先后顺序。
"""

           

枚举函数enumerate的用法

enumerate()是python的内置函数,在字典上是枚举、列举的意思。用于将一个可遍历的数据对象(如列表、元组或字符串)组合为一个索引序列,同时列出数据和数据下标,一般用在 for 循环当中。通过threading.enumerate()就可以获取线程列表。

多线程注意事项

  • 多线程程序的执行顺序是不确定的。
  • 线程的启动顺序、run函数中每次循环的执行顺序都不能确定。
  • 每个线程默认都有一个名字。
  • 当线程的run()方法结束时该线程完成。
  • 无法控制线程调度程序,但可以通过别的方式来影响线程的调度方式。

多线程共享全局变量

import threading
import time

# 定义一个全局变量
num = 100

def test1():
    global num
    num += 1
    print("-----test1 num = %d" % num)


def test2():
    print("-----test2 num = %d" % unm)


def main():
    t1 = threading.Thread(target=test1)
    t2 = threading.Thread(target=test1)

    t1.start()
    time.sleep(1)

    t2.start()
    time.sleep(1)

    print("------main num = %d ----" % num)


if __name__ == "__main__":
    main()

           

多线程资源竞争

import threading
import time

# 定义一个全局变量
g_num = 0


def test1(num):
    global g_num
    for i in range(num):
        g_num += 1
    print("-----in test1 g_num=%d----" % g_num)


def test2(num):
    global g_num
    for i in range(num):
        g_num += 1
    print("-----in test2 g_num=%d=----" % g_num)


def main():
    t1 = threading.Thread(target=test1, args=(1000000,))
    t2 = threading.Thread(target=test2, args=(1000000,))

    t1.start()
    t2.start()

    # 等待上面的2个线程执行完毕....
    time.sleep(5)

    print("-----in main Thread g_num = %d---" % g_num)

if __name__ == "__main__":
    main()

           

互斥锁

  • 当多个线程几乎同时修改某一个共享数据的时候,需要进行同步控制。
  • 线程同步能够保证多个线程安全的访问竞争资源,最简单的同步机制是引入互斥锁。
  • 互斥锁为资源引入一个状态:锁定/非锁定
  • 当某个线程要更改共享数据时,先将其锁定,此时资源的状态为“锁定”,其他线程不能更改;直到改线程释放资源,将资源的状态变成“非锁定”,其他的线程才能再次锁定该资源。互斥锁保证了每次只有一个线程进行写入操作,从而保证了多线程情况下数据的正确性。

互斥锁的运用

# 由于多线程之间共享全局变量就会导致出现资源竞争的问题,为了避免这种竞争出现,利用互斥锁可以实现线程同步。

# 创建锁

Mutex = threading.Lock()

# 加锁

Mutex.acquire()

# 释放锁

Mutex.release()

           

demo

使用互斥锁解决资源竞争

import threading
import time

# 定义一个全局变量
g_num = 0


def test1(num):
    global g_num
    # 上锁,如果之前没有被上锁,那么此时 上锁成功
    # 如果上锁之前 已经被上锁了,那么此时会堵塞在这里,直到 这个锁被解开位置
    mutex.acquire()
    for i in range(num):
        g_num += 1
    # 解锁
    mutex.release()
    print("-----in test1 g_num=%d----" % g_num)


def test2(num):
    global g_num
    mutex.acquire()
    for i in range(num):
        g_num += 1
    mutex.release()
    print("-----in test2 g_num=%d=----" % g_num)


# 创建一个互斥锁,默认是没有上锁的
mutex = threading.Lock()


def main():
    t1 = threading.Thread(target=test1, args=(1000000,))
    t2 = threading.Thread(target=test2, args=(1000000,))

    t1.start()
    t2.start()

    # 等待上面的2个线程执行完毕....
    time.sleep(2)

    print("-----in main Thread g_num = %d---" % g_num)

if __name__ == "__main__":
    main()


           

注意

  • 如果这个锁之前是没有上锁的,那么acquire不会阻塞
  • 如果在调用acquire对这个锁上锁之前,它已经被其他线程上了锁,那么此时acquire会堵塞,直到这个锁被解锁为止。

上锁过程

当一个线程调用锁的acquire()方法获得锁时,锁就进入“locked”状态。

每次只有一个线程可以获得锁。如果此时另一个线程试图获得这个锁,该线程就会变为“blocked”状态,称为"阻塞",直到拥有锁的线程调用锁的release()方法释放锁之后,锁进入"unlocked"状态。

线程调度程序从处于同步阻塞状态的线程中选择一个来获得锁,并使得该线程进入运行(running)状态。

互斥锁的运用

# 由于多线程之间共享全局变量就会导致出现资源竞争的问题,为了避免这种竞争出现,利用互斥锁可以实现线程同步。

# 创建锁

Mutex = threading.Lock()

# 加锁

Mutex.acquire()

# 释放锁

Mutex.release()

           

?

锁的好处:

确保了某段关键代码只能由一个线程从头到尾完整的执行。

锁的坏处:

阻止了多线程并发执行,不同的线程持有不同的锁,并试图获取对方持有的锁时,可能会造成死锁。

由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对方持有的锁时,可能会造成死锁

什么是死锁?

  • 死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。
  • 在线程间共享多个资源的时候,如果两个线程分别占有一部分资源并且同时等待对方的资源,就会造成死锁,尽管死锁很少发生,但一旦发生就会造成应用的停止响应。

如何解决死锁?

程序设计时要尽量避免

添加超时时间等

银行家算法

银行家算法(Banker’s Algorithm)是一个避免死锁(Deadlock)的著名算法,是由艾兹格·迪杰斯特拉在1965年为T.H.E系统设计的一种避免死锁产生的算法。

exit(?)