Python多线程
线程(thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
进程是资源分配的最小单位,线程是CPU调度的最小单位,每一个进程中至少有一个线程。
线程的特点
在多线程OS中,通常是在一个进程中包括多个线程,每个线程都是作为利用CPU的基本单位,是花费最小开销的实体。
- 轻型实体;
线程中的实体基本上不拥有系统资源,只是有一点必不可少的、能保证独立运行的资源。
线程的实体包括程序、数据和TCB。线程是动态概念,它的动态特性由线程控制块TCB(Thread Control Block)描述。 - 独立调度和分派的基本单位;
在多线程OS中,线程是能独立运行的基本单位,因而也是独立调度和分派的基本单位。由于线程很“轻”,故线程的切换非常迅速且开销小(在同一进程中的)。 - 可并发执行;
在一个进程中的多个线程之间,可以并发执行,甚至允许在一个进程中所有线程都能并发执行;同样,不同进程中的线程也能并发执行,充分利用和发挥了处理机与外围设备并行工作的能力。 - 共享进程资源;
在同一进程中的各个线程,都可以共享该进程所拥有的资源,这首先表现在:所有线程都具有相同的地址空间(进程的地址空间),这意味着,线程可以访问该地址空间的每一个虚地址;此外,还可以访问进程所拥有的已打开文件、定时器、信号量机构等。由于同一个进程内的线程共享内存和文件,所以线程之间互相通信不必调用内核。
创建线程
python的thread模块是比较底层的模块,python的threading模块是对thread做了一些包装的,可以更加方便的被使用
1 | class threading.Thread(group=None, target=None, name=None, args=(), kwargs={}, *, daemon=None) |
参数:
- group:目前此参数为None,在实现ThreadGroup类时为将来的扩展保留。
- target:target接收的是一个函数的地址,由run()方法调用执行函数中的内容。默认为无,表示未调用任何内容。
- name :线程名,可自行定义。
- args:target接收的是函数名,此函数的位置参数以元组的形式存放在args中,用于执行函数时调用。
- kwargs :target接收的是函数名,此函数的关键字参数以字典的形式存放在kwargs中,用于执行函数时调用。
- daemon:如果为True表示该线程为守护线程。
方法:
- threading模块的方法:
- threading.currentThread(): 返回当前的线程变量。
- threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。
- threading.activeCount(): 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果。
- Thread类方法:
- start():开启线程,一个Thread对象只能调用一次start()方法,如果在同一线程对象上多次调用此方法,则会引发RuntimeError。
- run():执行start()方法会调用run(),该方将创建Thread对象时传递给target的函数名,和传递给args、kwargs的参数组合成一个完整的函数,并执行该函数。run()方法一般在自定义Thead类时会用到。
- join(timeout=None):join会阻塞、等待线程,timeout单位为秒,因为join()总是返回none,所以在设置timeout调用join(timeout)之后,需要使用isalive()判断线程是否执行完成,如果isalive为True表示线程在规定时间内没有执行完,线程超时。如果join(timeout=None)则会等待线程执行完毕后才会执行join()后面的代码,一般用于等待线程结束。
- name:获取线程名。
- getName():获取线程名。
- setName(name):设置线程名。
- ident:“线程标识符”,如果线程尚未启动,则为None。如果线程启动是一个非零整数。
- is_alive():判断线程的存活状态,在run()方法开始之前,直到run()方法终止之后。如果线程存活返回True,否则返回False。
- daemon:如果thread.daemon=True表示该线程为守护线程,必须在调用Start()之前设置此项,否则将引发RuntimeError。默认为False
- isDaemon():判断一个线程是否是守护线程。
- setDaemon(daemonic):设置线程为守护线程。
thread创建线程
1 | import threading |
执行结果:
1 | 线程--- 0 |
thread子类创建线程
1 | import threading |
执行结果:
1 | Thread-1 0 |
共享全局变量
利用线程对变量num分别加1000000次:
1 | from threading import Thread |
执行结果:
1 | 当前值--num:1196015 |
python之间线程是可以共享同一进程中的所有资源,包括变量。但为什么上述代码执行结果不是2000000,这和操作系统的cpu处理速度和调度算法有关,“ num += 1 ” 可以看成 “ num = num + 1 ”,分为两部,先加法,再赋值;例如当num=1000,线程1的num加法执行完,要进行赋值“num = 1001”时,cpu切换了线程,执行线程2,执行了一个时间片后,再回到线程1,尽管这时num的值叠加了好几轮,执行了“num = 1001”,num的值就又回到原点了;同时执行两个线程没有达到预期效果。
总结:
- 在一个进程内的所有线程共享全局变量,能够在不适用其他方式的前提下完成多线程之间的数据共享
- 缺点就是,线程是对全局变量随意遂改可能造成多线程之间对全局变量的混乱(即线程非安全)
互斥锁
当多个线程几乎同时修改某一个共享数据的时候,需要进行同步控制
线程同步能够保证多个线程安全访问竞争资源,最简单的同步机制是引入互斥锁。
- 线程同步:即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作, 其他线程才能对该内存地址进行操作,而其他线程又处于等待状态,实现线程同步的方法有很多,临界区对象就是其中一种。
互斥锁为资源引入一个状态:锁定/非锁定。
某个线程要更改共享数据时,先将其锁定,此时资源的状态为“锁定”,其他线程不能更改;直到该线程释放资源,将资源的状态变成“非锁定”,其他的线程才能再次锁定该资源。互斥锁保证了每次只有一个线程进行写入操作,从而保证了多线程情况下数据的正确性。
threading模块中定义了Lock类,可以方便的处理锁定:
1 | #创建锁 |
其中,锁定方法acquire可以有一个blocking参数。
- 如果设定blocking为True,则当前线程会堵塞,直到获取到这个锁为止(如果没有指定,那么默认为True)
- 如果设定blocking为False,则当前线程不会堵塞
使用互斥锁实现上面的例子的代码如下:
1 | from threading import Thread, Lock |
执行结果:
1 | 当前值--num:1000000 |
死锁
在线程间共享多个资源的时候,如果两个线程分别占有一部分资源并且同时等待对方的资源,就会造成死锁。
尽管死锁很少发生,但一旦发生就会造成应用的停止响应。下面看一个死锁的例子
1 | import threading |
注:time.sleep(1)的作用是保证两个线程都抢占到资源;由于代码比较少,cpu运行速度快,延时的作用防止线程1直接执行完,而看不到结果
线程1和线程2各自等待对方资源释放,这样就陷入死锁了。
执行结果:
1 | Thread-1-------acquire mutexA |
避免死锁的方法
- 增加超时时间
- 银行家算法