在改变某个DOM元素的高度时,先把这块代码的执行给锁住了,只有执行完了才能释放这把锁,其它线程运行到这里的时候也要去申请那把锁,但是由于这把锁没有被释放,所以它就堵塞在那里了,只有等到锁被释放了,它才能拿到这把锁再继续加锁。
互斥锁使用太多会导致性能下降,因为线程堵塞在哪里它要不断地查那个锁能不能用了,所以要占用CPU。
第二种锁叫读写锁。
(2)读写锁
如下,假设读写锁用ReadWriteRock表示:
function changeHeight(height){
lock.lockForWrite();
$("#my-div")[0].style.height = height + "px";
lock.unlock();
function getHeight(){
//允许多个线程同时读,但是不允许有一个线程进行写
lock.lockForRead();
var height = $("#my-div")[0].style.height;
lock.unlock();
return height;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
var
lock
=
new
ReadWriteLock
(
)
;
function
changeHeight
(
height
)
{
lock
.
lockForWrite
(
)
;
$
(
"#my-div"
)
[
0
]
.
style
.
height
=
height
+
"px"
;
lock
.
unlock
(
)
;
}
function
getHeight
(
)
{
//允许多个线程同时读,但是不允许有一个线程进行写
lock
.
lockForRead
(
)
;
var
height
=
$
(
"#my-div"
)
[
0
]
.
style
.
height
;
lock
.
unlock
(
)
;
return
height
;
}
在第二个函数getHeight获取高度的时候可以给它加一个读锁,这样其它线程如果想读是可以同时读的,但是不允许有一个线程进行写入,如调第一个函数的线程将会堵塞。同理只要有一个线程在写,另外的线程就不能读。
第三种锁叫条件变量。
(3)条件变量
条件变量是为了解决生产者和消费者的问题,由用互斥锁和读写锁会导致线程一直堵塞在那里占用CPU,而使用信号通知的方式可以先让堵塞的线程进入睡眠方式,等生产者生产出东西后通知消费者,唤醒它进行消费。
不同编程语言锁的实现不一样,但是总体上可以分为上面那三种。
7. 多线程操作DOM的问题
上面只是做到了不允许多个线程同时执行:
$(“#my-div”)[0].style.height = height + “px”;
但是另外的函数也可以用选择器去获取那个DOM结点然后去设置它的高度,所以无法避免多线程同时写的问题。
如果真的发生了同时写的情况,那么不仅仅是页面崩了,而是整个浏览器都崩了。所以这个就比较危险了,假设那边我有一个页面打了1万个字还没保存,但是因为不小了打开了你的页面,导致整个浏览器挂了还没保存就有点悲摧了。这里有个问题,为什么浏览器会挂呢?因为操作系统检测到异常,它要把你杀了,如果不把你杀了,它自己就要脆了,它一脆你也脆了。为什么以前的windows系统会蓝屏,因为它没有检测到应用程序的异常,任由应用程序胡作非为,结果为了保护硬件它必须得挂,它一挂应用程序也得跟着挂。
所以JS不给程序员犯错的机会。
8. JS没有线程同步的概念
JS的多线程无法操作DOM,没有window对象,每个线程的数据都是独立的。主线程传给子线程的数据是通过拷贝复制,同样地子线程给主线程的数据也是通过拷贝复制,而不是共享共一块内存区域。
所以会web worker基本上出不了什么错。
然后我们再来看一下JS的单线程模型。
9. JS的单线程模型
如下图所示,应该可以很清楚地表示:
在主逻辑里面fun1和fun2的调用是连在一起的,它是一个执行单元,要么还没执行,要么得一口气执行完。执行完之后,再执行setTimout append到后面的。然后由于已经超过了setInterval定的20ms,所以又马上执行setInterval的函数。这里可以看出setTimeout的计时是从逻辑单元执行完了才开始计时,而setInterval是执行到这一行的时候就开时计时了。
单线程里面有个特例:异步回调,异步回调是Chrome自己的IO线程处理的,每发一个请求必须要有一个线程跟着,Chrome限制了同一个域最多只能发6个请求。
再来看下Chrome的多线程模型。
10. Chrome的多线程模型
每开一个tab,Chrome就会创建一个进程,进程是线程的容器,如下Chrome的任务管理器:
我们从click事件来看一下Chrome的线程模式是怎么样的,如下图表所示:
首先用户点击了鼠标,浏览器的UI线程收到之后,这个消息数据封装成一个鼠标事件发送给IO线程,IO线程再分配给具体页面的渲染线程。其中IO线程和UI线程是浏览器的线程,而渲染线程是每个页面自己的线程。
如果在执行一段很耗时的JS代码,渲染线程里的render线程将会被堵塞,而main线程继续接收IO线程发过来的消息并排队,等待render线程处理。也就是说当页面卡住的时候,不断地点击鼠标,等页面空闲了,点击的事件会再继续触发。
这个是从浏览器线程到页面线程的过程,反过来从页面线程到浏览器线程的例子如下图表所示(在代码里面改变光标形状):
看完Chrome的,再来看Node.js的线程模型。
11. Node.js的单线程模型
我们知道Node.js也是单线程的,但是单线程如何处理高并发呢?传统的web服务是多线程的,它们通常是先初始化一个线程池,来一个连接就从线程池里取出一个空闲的线程处理,而用Node.js如果有一个连接处理时间过长,那么其它请求将会被堵塞。
但是由于数据库连接本来是就是多线程,调用操作系统的IO文件读取也是多线程,所以Node.js的异步是借助于数据库和IO多线程。
这样的好处是不需要启动新的线程,不需要开辟新线程的空间,不需要进行线程的上下文切换,所以当服务应用不是计算类型的,使用Node.js可能反而会更快,同时由于是单线程的所以写代码更容易。缺点是不能够提供很耗CPU的服务,如图形渲染,复杂的算法计算等。
可以在同一台多核的服务器上开多几个Node服务弥补单线程的缺陷,然后用nginx均匀地分发请求。
我们出来转了一圈之后,再回到webworker
12. 内联webworker
当new很多个worker.js的时候,浏览器会从缓存里面取worker.js:
有时候并不想多管理一个JS文件,想要把它搞内联的。这个时候可以用HTML5的新的数据类型Blob,如下代码所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var
blobURL
=
URL
.
createObjectURL
(
new
Blob
(
[
'('
,
function
(
)
{
function
fibonacci
(
)
{
}
onmessage
=
function
(
event
)
{
var
num
=
event
.
data
;
var
result
=
fibonacci
(
num
)
;
postMessage
(
result
)
;
}
}
.
toString
(
)
,
')()'
]
,
{
type
:
'application/javascript'
}
)
)
;
var
worker
=
new
Worker
(
blobURL
)
;
worker
.
onmessage
=
function
(
event
)
{
console
.
log
(
`
recieve
result
:
$
{
event
.
data
}
`
)
}
;
Blob还经常被用于分割大文件。
最后,JS的设计是单线程,后来HTML5又引入webworker,它只能用于计算,因为它不能改DOM,无法造成视觉上的效果。然后它不能共享内存,没有线程同步的概念,所以可以认为JS还是单线程,可以把webworker当成另外的一种回调机制。
本文并不是要介绍webworker怎么用,重点还是介绍一下多线程的一些概念,例如什么叫多线程,它和CPU又有什么关系,什么叫线程同步,为什么要进行线程同步,还讨论了JS/Chrome/Node的线程模型,相信看了本文对多线程应该会有更好的理解。
15,737
WebSocket与TCP/IP
WebAssembly与程序编译
目录:
基础技术
感谢分享!已推荐到《开发者头条》:https://toutiao.io/posts/uytvvb 欢迎点赞支持!
欢迎订阅《前端疯魔院》https://toutiao.io/subject/104480