线程

线程概念

线程是操作系统最小的调度单位,是一串指令的集合。一个标准的线程由线程ID、当前指令指针PC、寄存器和堆栈组成。线程包括操作系统内核调度的内核线程和用户自行调度的用户线程。

线程状态

线程状态包括新建、就绪、运行、阻塞、死亡五种状态。

  1. 新建状态 用new语句创建的线程处于新建状态,此时线程和普通对象一样,仅仅在堆区中被分配了内存;
  2. 就绪状态 当一个线程对象创建后,其他线程调用它的start()方法,该线程就进入就绪状态。此时线程处于可运行池中,等待获得CPU的使用权;
  3. 运行状态处于这个状态的线程占用CPU,执行程序代码。只有处于就绪状态的线程才有机会转到运行状态 ;
  4. 阻塞状态 阻塞状态是指线程因为某些原因放弃CPU,暂时停止运行。当线程处于阻塞状态时,不会给线程分配CPU。直到线程重新进入就绪状态,它才有机会转到运行状态;阻塞状态分以下三种情况:
    • 等待阻塞:执行的线程执行wait()方法,该线程放入等待池中。
    • 同步阻塞:执行的线程在获取对象的同步锁时,若该同步锁被别的线程占用。则该线程放入锁池中
    • 其他阻塞:执行的线程执行sleep()或join()方法,或者发出了I/O请求时,该线程置为堵塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完成时。线程又一次转入就绪状态。
  5. 死亡状态 当线程退出run()方法时,就进入死亡状态,该线程结束生命周期;

线程状态转换图
在这里插入图片描述

上下文切换

每个线程都有它自己的一组C P U寄存器,称为线程的上下文。该上下文反映了线程上次运行时该线程的CPU寄存器的状态。线程的这组C P U寄存器保存在一个CONTEXT结构。CONTEXT结构本身则包含在线程的内核对象中。指令指针和堆栈指针寄存器是线程上下文中两个最重要的寄存器。线程总是在进程的上下文中运行的。因此,这些地址都用于标识拥有线程的进程地址空间中的内存。当线程的内核对象被初始化时,CONTEXT结构的堆栈指针寄存器被设置为线程堆栈上用来放置pfnStartAddr的地址。当线程完全初始化后,系统就要查看CREATE_SUSPENDED标志是否已经传递给CreateThread。如果该标志没有传递,系统便将线程的暂停计数递减为0,该线程可以调度到一个进程中。然后系统用上次保存在线程上下文中的值加载到实际的C P U寄存器中。这时线程就可以执行代码,并对它的进程的地址空间中的数据进行操作。CONTEXT结构的作用就是用于记录线程当前执行的进度信息。CONTEXT结构是特定于CPU的唯一数据结构。那么CONTEXT结构中究竟存在哪些东西呢?它包含了主机C P U上的每个寄存器的数据结构

线程操作

本文采用python2.7进行编码

直接调用线程

# !/usr/bin/python
# _*_ coding:utf-8 _*_

import threading
import time

def run(i):	
	print('test', i)
	time.sleep(1)
	
# 创建线程,target为线程执行的函数,args为传入函数的参数
t1 = threading.Thread(target=run, args=('t1',))     
t2 = threading.Thread(target=run, args=('t2',))
t3 = threading.Thread(target=run, args=('t3',))
# 开启线程
t1.start()
t2.start()
t3.start()

返回结果

python create_thread.py
(‘test’, ‘t1’)
(‘test’, ‘t2’)
(‘test’, ‘t3’)

继承线程

# !/usr/bin/python
# _*_ coding:utf-8 _*_

import threading
import time

exitFlag = 0

class myThread(threading.Thread):	# 继承父类threading.Thread
	def __init__(self, threadID, name, counter):
		threading.Thread.__init__(self)
		self.threadID = threadID
		self.name = name
		self.counter = counter
	def run(self): 			#把要执行的代码写到run函数里面,线程在创建后会直接运行run函数
		print "Starting "+ self.name
		print_Ex(self.name, self.counter, 5)
		print "Exiting "+ self.name

def print_Ex(threadName, delay, counter):
	while counter:
		if exitFlag:
			(threading.Thread).exit()
		time.sleep(delay)
		print "counter: %d %s: %s" % (counter, threadName, time.ctime(time.time()))
		counter -= 1

# 创建新线程
thread1 = myThread(1, "Thread-x", 1)
thread2 = myThread(2, "Thread-y", 2)

# 开启线程
thread1.start()
thread2.start()

print "Exiting Main Thread"

返回结果

python extends_thread.py
Starting Thread-x
Starting Thread-y
Exiting Main Thread
counter: 5 Thread-x: Tue Nov 24 17:29:28 2020
counter: 4 Thread-x: Tue Nov 24 17:29:29 2020
counter: 5 Thread-y: Tue Nov 24 17:29:29 2020
counter: 3 Thread-x: Tue Nov 24 17:29:30 2020
counter: 4 Thread-y: Tue Nov 24 17:29:31 2020
counter: 2 Thread-x: Tue Nov 24 17:29:31 2020
counter: 1 Thread-x: Tue Nov 24 17:29:32 2020
Exiting Thread-x
counter: 3 Thread-y: Tue Nov 24 17:29:33 2020
counter: 2 Thread-y: Tue Nov 24 17:29:35 2020
counter: 1 Thread-y: Tue Nov 24 17:29:37 2020
Exiting Thread-y

可以看到开启两个线程Thread-x和Thread-y,两个线程的执行的无序的,交错执行

单线程&多线程

# !/usr/bin/python
# _*_ coding:utf-8 _*_

import threading
import time

def run(i):	
	print('test', i)
	time.sleep(1)

ticks_start = time.time()
thread_arr = []
for i in range(1,1000):
	s = "thread"+ str(i)
	s = threading.Thread(target=run, args=(s,))
	thread_arr.append(s)
for j in thread_arr:
	j.start()	# 开启线程
ticks_end = time.time()

single_start = time.time()
for i in range(1,1000):
	s = "thread"+ str(i)
	run(s)
single_end = time.time()

print "多线程执行一千次总耗时:%s 秒" % str(ticks_end-ticks_start)
print "单线程执行一千次总耗时:%s 秒" % str(single_end-single_start)

单线程可以理解为在一个线程中串行执行程序代码,多线程是多个线程并发运行,单线程可以保证程序执行顺序,而多线程模式下主线程创建子线程之后,主线程就与子线程相互独立,不管子线程是否执行完成,主线程都会继续执行下去,所以各个子线程创建是有先后顺序的但执行完毕时的时间顺序会错乱,上述代码执行结果如下:


(‘test’, ‘thread984’)
(‘test’, ‘thread985’)
(‘test’, ‘thread986’)
(‘test’, ‘thread987’)
(‘test’, ‘thread988’)
(‘test’, ‘thread989’)
(‘test’, ‘thread990’)
(‘test’, ‘thread991’)
(‘test’, ‘thread992’)
(‘test’, ‘thread993’)
(‘test’, ‘thread994’)
(‘test’, ‘thread995’)
(‘test’, ‘thread996’)
(‘test’, ‘thread997’)
(‘test’, ‘thread998’)
(‘test’, ‘thread999’)
多线程执行一千次总耗时:0 秒
单线程执行一千次总耗时:999 秒

线程等待

ticks_start = time.time()
thread_arr = []
for i in range(1,1000):
	s = "thread"+ str(i)
	s = threading.Thread(target=run, args=(s,))
	thread_arr.append(s)
for j in thread_arr:
	j.start()	# 开启线程
	j.join()	# 开启线程等待
print "线程等待执行一千次总耗时:%s 秒" % str(time.time()-ticks_start)

(‘test’, ‘thread1’)
(‘test’, ‘thread2’)
(‘test’, ‘thread3’)
(‘test’, ‘thread4’)
(‘test’, ‘thread5’)
(‘test’, ‘thread6’)
(‘test’, ‘thread7’)
(‘test’, ‘thread8’)
(‘test’, ‘thread9’)

主线程中创建的子线程使用join()时进行线程等待,只有当前子线程程序执行完成后才执行下一步操作。join()操作使得线程间按顺序执行,类似于串行运行。

守护线程

# !/usr/bin/python
# _*_ coding:utf-8 _*_

import threading
import time

def run(i):
	print('test', i)
	time.sleep(1)

# 多线程
tick_start1 = time.time()
for i in range(10000):
	s = threading.Thread(target=run, args=('thread-%s' % i,))
	s.start()	# 开启线程
tick_end = time.time()

tick_start = time.time()
for i in range(10000):
	t = threading.Thread(target=run, args=('thread-%s' % i,))
	t.setDaemon(True)	# 将t线程设置为守护线程
	t.start()	# 开启守护线程

print('multi thread takes: ', tick_end - tick_start1)
print('deamon thread takes: ', time.time() - tick_start)

运行结果:

(‘test’, ‘thread-9997’)
(‘test’, ‘thread-9998’)
(‘test’, ‘thread-9999’('multi thread takes: ', )6.8978190422058105)
('deamon thread takes: ', 6.895900011062622)

多线程执行时间比守护线程执行时间要长一些,守护线程是为主线程服务的,只要非守护线程执行完成程序就会直接结束。当一个子线程被设置为守护线程,程序就不会再等待他执行完成再结束。
注意:setDaemon(true) 需要在 start 之前设置

线程锁

# !/usr/bin/python
# _*_ coding:utf-8 _*_

import threading
import time

def run(i):
	lock.acquire()	# 获取锁
	global num
	time.sleep(0.01)
	num += 1
	print(i)
	lock.release()	# 释放锁

lock = threading.Lock()		# 创建线程锁
start_time = time.time()
threading_list = []
num = 0
for i in range(50):
	t = threading.Thread(target=run, args=('thread-%s' % i,))
	t.start() 		# 开启线程
	threading_list.append(t)
for item in threading_list:
	item.join()		# 开启线程等待
print(num)

一个进程可以有多个线程,同一个进程内的线程共享父进程的内存空间,也就是说每个线程可以访问同一份数据。为避免共享数据为误读、脏读,需要使用互斥锁来控制。各个线程对同一个共享数据的操作只有在获得锁之后才可以操作数据,数据操作完成后需立即释放锁以释放数据资源供其他线程使用。这里的线程锁与线程串行执行不同,串行执行是对整个应用程序依照严格的先后顺序执行,互斥锁是针对某部分数据处理逻辑加锁,并不针对所有程序执行过程的时间进行控制。

死锁

# !/usr/bin/python
# _*_ coding:utf-8 _*_

import threading

def run1():
	lock.acquire()
	global num1
	num1 += 1
	lock.release()
	return num1

def run2():
	lock.acquire()
	global num2
	num2 += 1
	lock.release()
	return num2

def run3():
	lock.acquire()
	res1 = run1() 	# 此时run3()获得锁但是还未释放,run1()无法获得锁,这种情况就是死锁
	res2 = run2()
	lock.release()
	print(res1, res2)

num1, num2 = 0, 0
lock = threading.Lock()
for i in range(10):
	t = threading.Thread(target=run3)
	t.start()

while threading.active_count() != 1:
	print(threading.active_count()) 	# 打印当前存活的线程
else:
	print('-----------finished-----------')
	print(num1, num2)

当有多层互斥锁同时存在时,外层获得锁后没有释放而内层再次获得锁操作将会失败,导致内层逻辑无法执行出现死锁,程序进入死循环。为了避免产生死锁,使用递归锁来处理。

递归锁

# !/usr/bin/python
# _*_ coding:utf-8 _*_

import threading

def run1():
	lock.acquire() 	# 第一把锁没释放的情况下接着上第二把锁
	global num1
	num1 += 1
	lock.release()	# 释放第二把锁
	return num1

def run2():
	lock.acquire()	# 第一把锁没释放的情况下接着上第三把锁
	global num2
	num2 += 1
	lock.release()	# 释放第三把锁
	return num2

def run3():
	lock.acquire()	# 第一把锁
	res1 = run1()
	res2 = run2()
	lock.release()	# 释放第一把锁
	print(res1, res2)

num1, num2 = 0, 0
lock = threading.RLock()	# 创建递归锁
for i in range(10):
	t = threading.Thread(target=run3)
	t.start()

while threading.active_count() != 1:
	print(threading.active_count())
else:
	print('---------finish-----------')
	print(num1, num2)

递归锁允许在同一个线程中多次获得锁,但是对锁的开启和释放顺序有要求,必须顺序执行,如开锁r1->r2->r3,则释放必须按顺序r3->r2->r1,否则会出现线程内的部分操作一直阻塞占用。递归锁必须由获取它的线程释放。一旦线程获得了递归锁,同一个线程再次获取它将不阻塞。

互斥锁与递归锁区别

互斥锁acquire()获得锁后将阻塞直至程序release()释放锁才释放资源,如程序执行中有执行acquire()操作,但是还未执行release()操作,此时程序再次执行acquire()对数据进行操作,系统将报错,必须由前一逻辑release()后才能获得锁。acquire()和release()可以在不同的线程中操作,如下:

import threading
import time

lock = threading.Lock()  # 主线程上锁
lock.acquire()

def func():
    lock.release()		# 子线程解锁
    print("lock is released")

t = threading.Thread(target=func)
t.start()

递归锁必须由获取它的线程释放,上述操作如果使用RLock将报错,递归锁获得锁后,同一线程内再次获取将不阻塞。

进程信号量

# !/usr/bin/python
# _*_ coding:utf-8 _*_

import threading
import time
import sys

def run(i):
	semaphore.acquire()
	s = 'threading:'+ str(i)+ '\n'
	sys.stdout.write(s)
	time.sleep(1)
	semaphore.release()
	
# 线程信号量同时允许一定数量的线程更改数据
semaphore = threading.BoundedSemaphore(5)		# 最多允许5个线程同时运行
for i in range(20):
	t = threading.Thread(target=run, args=(i,))
	t.start()

while threading.active_count() != 1:
	pass
else:
	print('Done')

执行结果:

threading:0
threading:1
threading:2
threading:3
threading:4
threading:5
threading:6
threading:7
threading:8
threading:9
threading:11
threading:10
threading:12
threading:13
threading:14
threading:16
threading:15
threading:17
threading:18
threading:19
Done

互斥锁同时只允许一个线程更改数据,而Semaphore是同时允许一定数量的线程更改数据。从程序的运行过程可以看出:开始有5个线程在运行,这5个线程结束之后又有5个线程启动

事件

Event 是一种非常简单的线程通信机制,一个线程发出一个 Event,另一个线程可通过该 Event 被触发。

Event 本身管理一个内部旗标,程序可以通过 Event 的 set() 方法将该旗标设置为 True,也可以调用 clear() 方法将该旗标设置为 False。程序可以调用 wait() 方法来阻塞当前线程,直到 Event 的内部旗标被设置为 True。

Event 提供了如下方法:

  • is_set():该方法返回 Event 的内部旗标是否为True。
  • set():该方法将会把 Event 的内部旗标设置为 True,并唤醒所有处于等待状态的线程。
  • clear():该方法将 Event 的内部旗标设置为 False,通常接下来会调用 wait() 方法来阻塞当前线程。
  • wait(timeout=None):该方法会阻塞当前线程。

下面是一个检索即时消息服务器中未读消息的示例:

import zmq, time, sys, thread, json, os
import threading

context = zmq.Context()
socket = context.socket(zmq.REQ)
socket.connect("tcp://127.0.0.1:5555")
event = threading.Event() 		# 创建事件
event.set()		# 将Event内部旗标设置为true

count = 0 
def heartbeat():
	global token
	global count

	while True:
		if event.is_set():
			data = "1"+ json.dumps({"token": token, "type": "query"})
			socket.send(data)
			message = socket.recv()
	
			if message == '0':
				event.clear()	# 无消息时事件等待
			else:
				print time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())+ "接收到消息: "+ message
		elif count > 18:
			print time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())+ "事件唤醒,继续执行。。。"
			event.set()
			count = 0
		else:
			time.sleep(5)	# 每隔5分钟继续检索新消息
			count += 1

# 发送消息
t1 = threading.Thread(target=heartbeat, args=())		# 创建线程
t1.start()		# 开启线程

实战演练

  • 用多线程执行递归程序:
for i in range(30):
	for j in range(30):
		for k in range(30):
			print(i+1, j+1, k+1)
import threading, time

def run1(i):
	lock.acquire() 	# 第一把锁没释放的情况下接着上第二把锁
	global count
	for j in range(count):
		run2(i, j)
	lock.release()	# 释放第二把锁

def run2(i, j):
	lock.acquire()	# 第一把锁没释放的情况下接着上第三把锁
	global count
	for k in range(count):
		print(i+1, j+1, k+1)
	lock.release()	# 释放第三把锁
	cost = time.time() - tick_start
	print "====================================================="
	print "耗时:%s 秒" % str(cost)
	print "====================================================="


def run3(i):
	lock.acquire()	# 第一把锁
	run1(i)
	lock.release()	# 释放第一把锁

tick_start = time.time()
count = 30
lock = threading.RLock()	# 创建递归锁

for i in range(count*count*count):
	t = threading.Thread(target=run3, args= (i,))
	t.start()

单进程执行27000次需要27000s左右,使用多线程执行后一次开启30个线程,执行时间为0.08秒左右

  • 二次优化,假设count总数为303030,怎么办?
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值