Worker那些事儿
Worker之前世今生
不知道是谁发明了worker这个单词,百度解释如下:劳动者; 工人,员工; [虫] 工蜂,工蚁…,看名字都是一些受苦的角色。
也不知道是谁把worker这个词引入了软件行业,几乎任何语言中都有worker线程的存在。有的语言中只是概念性的存在,有的语言则干脆定义的worker的API。如:Java GUI库Swing中的SwingWorker,C#中的BackgroundWorker,Android中的AsyncTask,HTML5中的Web Worker,…。
上文中提到了“worker线程”。是的,有了多线程,才有了worker这个词。编程语言的世界如此,其他行业亦如此。
众所周知,为了提高计算机资源的利用率,操作系统引入了进程/线程的概念。线程也被称作“轻量级进进程”,在大多数现代操作系统中,都是以线程为最基本的调度单位。通常来说,使用线程有如下的优势:
- 发挥多处理器的强大威力
- 简化问题建模方式
- 简化异步事件的处理方式
- 响应更灵敏的用户界面
看起来很美好,其实操作系统引入多线程是一把双刃剑。自从多线程出现后,就和另外一个词形影不离,那就是“线程安全”。看下最经典的非线程安全例子:
public class UnSafeSessionGenerator {
private int sessionID;
public int getNextSessionID() {
return sessionID++;
}
}
如果执行时机不对,那么两个线程在调用getNextSessionID
方法时会得到相同的值(100)。虽然自增运算sessionID++
看上去是单个操作,但事实上包含三个独立的操作:读取sessionID
,将sessionID
加1,并将运算结果写入sessionID
。
线程封闭技术
如何保证程序的线程安全性,不是本文讨论的重点。但是,从上面的例子中可以得出一个很朴素的结论:如果仅在单线程内部访问数据则可以保证线程的安全性。这种朴素的技术被称为“线程封闭”,它是实现线程安全性的最简单方式之一。在UI相关的程序设计中大量使用了线程封闭技术,UI使用一个事件分发线程对界面操作进行封闭,实现界面安全访问的同时减少用户界面的阻塞时间。用现在一句流行的话说就是“UI线程负责貌美如花,worker(s)负责赚钱养家”。下面,分别看下Swing、Android、Html5中Worker是怎么工作的。
Swingworker
有如下需求:执行某耗时操作,每隔5秒更新一次界面上进度条通知客户。
不使用SwingWorker的代码:
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
public class UpdateProgressBarInEDT extends JDialog
implements ActionListener {
private final JProgressBar progressBar;
public UpdateProgressBarInEDT(){
setTitle("Unuse SwingWorker");
setLayout(
new BoxLayout(getContentPane(),BoxLayout.Y_AXIS));
progressBar = new JProgressBar(1, 10);
progressBar.setPreferredSize(new Dimension(200, 60));
add(progressBar);
JPanel footerPanel =
new JPanel(new FlowLayout(FlowLayout.RIGHT));
JButton okBtn = new JButton("OK");
footerPanel.add(okBtn);
okBtn.addActionListener(this);
okBtn.setActionCommand("OK");
JButton cancelBtn = new JButton("Cancel");
footerPanel.add(cancelBtn);
add(footerPanel);
cancelBtn.addActionListener(this);
cancelBtn.setActionCommand("Cancel");
setSize(200, 100);
setDefaultCloseOperation(DISPOSE_ON_CLOSE);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
UpdateProgressBarInEDT progressBarInEDTDemo
= new UpdateProgressBarInEDT();
progressBarInEDTDemo.setVisible(true);
progressBarInEDTDemo.setLocationRelativeTo(null);
}
});
}
@Override
public void actionPerformed(ActionEvent e) {
if(e.getActionCommand().equals("OK")) {
for (int i = 0; i < 10; i++) {
proceed(i);
snap(5000);
}
}
if(e.getActionCommand().equals("Cancel")) {
initProgress();
}
}
private void initProgress() {
progressBar.setValue(0);
}
private void proceed(int process) {
progressBar.setValue(progressBar.getValue() + process);
}
private void snap(int millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
尝试运行下这段代码会发现,点击【OK】按钮后,界面即失去响应,进度条没有按照我们的预期每隔5秒前进10%。并且【Cancel】按钮也无法响应。
使用SwingWorker的代码:
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.List;
public class UpdateProgressBarInEDT extends JDialog
implements ActionListener {
private final JProgressBar progressBar;
private SwingWorker<Void, Integer> swingWorker;
public UpdateProgressBarInEDT() {
setTitle("Unuse SwingWorker");
setLayout(new BoxLayout(getContentPane(), BoxLayout.Y_AXIS));
progressBar = new JProgressBar(0, 10);
progressBar.setPreferredSize(new Dimension(200, 60));
add(progressBar);
JPanel footerPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
JButton okBtn = new JButton("OK");
footerPanel.add(okBtn);
okBtn.addActionListener(this);
okBtn.setActionCommand("OK");
JButton cancelBtn = new JButton("Cancel");
footerPanel.add(cancelBtn);
add(footerPanel);
cancelBtn.addActionListener(this);
cancelBtn.setActionCommand("Cancel");
swingWorker = buildSwingWorker();
setSize(200, 100);
setDefaultCloseOperation(DISPOSE_ON_CLOSE);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
UpdateProgressBarInEDT progressBarInEDTDemo
= new UpdateProgressBarInEDT();
progressBarInEDTDemo.setVisible(true);
progressBarInEDTDemo.setLocationRelativeTo(null);
}
});
}
@Override
public void actionPerformed(ActionEvent e) {
if (e.getActionCommand().equals("OK")) {
if(swingWorker.isCancelled() || swingWorker.isDone())
{
swingWorker = buildSwingWorker();
}
swingWorker.execute();
}
if (e.getActionCommand().equals("Cancel")) {
swingWorker.cancel(true);
initProgress();
}
}
private SwingWorker<Void, Integer> buildSwingWorker() {
return new SwingWorker<Void, Integer>() {
@Override
protected Void doInBackground() throws Exception {
for (int i = 1; i <= 10; i++) {
publish(i);
snap(5000);
}
return null;
}
@Override
protected void process(List<Integer> chunks) {
for (Integer chunk : chunks) {
proceed(chunk);
}
}
};
}
private void initProgress() {
progressBar.setValue(0);
}
private void proceed(int process) {
progressBar.setValue(progressBar.getValue() + process);
}
private void snap(int millis) throws InterruptedException {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
throw e;
}
}
}
运行上面的代码,点击【OK】按钮后,进度条按照我们的预期每隔5秒前进10%。点击【Cancel】按钮后进度条归零。再次点击【OK】按钮后,进度条重新开始前进。
AsyncTask
上述的需求,我们在Andrioid里实现一次。
不使用AsyncTask,在Actiity里直接控制进度条。
布局代码:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ProgressBar android:id="@+id/progress"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="30dp"
android:max="10"
android:progress="0" />
<LinearLayout
android:orientation="horizontal"
android:gravity="end"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<Button android:id="@+id/ok"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/ok_text"
android:onClick="handleOK"/>
<Button android:id="@+id/cancel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/cancel_text"
android:onClick="handleCancel"/>
</LinearLayout>
</LinearLayout>
MainActivity:
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.ProgressBar;
public class MainActivity extends Activity {
private ProgressBar progressBar;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
progressBar = (ProgressBar) findViewById(R.id.progress);
}
public void handleOK(View view) {
for (int i = 0; i < 10; i++) {
progressBar.incrementProgressBy(1);
snap(5000);
}
}
public void handleCancel(View view) {
progressBar.setProgress(0);
}
private void snap(int millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
与Swing的第一个例子表现一致,点击【OK】按钮后App界面直接无响应了。
使用AsyncTask改写:
import android.app.Activity;
import android.os.AsyncTask;
import android.os.Bundle;
import android.view.View;
import android.widget.ProgressBar;
public class MainActivity extends Activity {
private ProgressBar progressBar;
private AsyncTask<Void,Integer,String> asyncTask;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
progressBar = (ProgressBar) findViewById(R.id.progress);
asyncTask = buildAsyncTask();
}
private AsyncTask<Void,Integer,String> buildAsyncTask() {
return new AsyncTask<Void,Integer,String>() {
@Override
protected String doInBackground(Void... params) {
for (int i = 0; i < 10; i++) {
publishProgress(i);
snap(5000);
}
return null;
}
@Override
protected void onProgressUpdate(Integer... values) {
for (Integer progress : values) {
progressBar.setProgress(progress);
}
}
};
}
public void handleOK(View view) {
if(asyncTask.isCancelled()) {
asyncTask = buildAsyncTask();
}
asyncTask.execute();
}
public void handleCancel(View view) {
asyncTask.cancel(true);
progressBar.setProgress(0);
}
private void snap(int millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
这次,进度条可以正常前进和取消了。
Web Worker
当在HTML页面中执行脚本时,页面的状态是不可响应的,直到脚本已完成。
Web worker是运行在后台的JavaScript,独立于其他脚本,不会影响页面的性能。用户可以继续做任何愿意做的事情:点击、选取内容等等,而此时 web worker 在后台运行。
在html/js中,我们再次实现进度条需求。
不使用Webworker:
<!DOCTYPE HTML>
<html>
<head>
<style type="text/css">
#center {
margin: 50px auto;
width: 400px;
}
#loading {
width: 397px;
height: 49px;
background: url(back.png) no-repeat;
}
#loading div {
width: 0px;
height: 48px;
background: url(proceed.png) no-repeat;
color: #fff;
text-align: center;
font-family: Tahoma;
font-size: 18px;
line-height: 48px;
}
</style>
<script type="text/javascript" src="jquery.js">
</script>
<script type="text/javascript">
function setProgress(progress){
if (progress >= 0) {
$("#loading > div").css("width", progress + "%");
$("#loading > div").html(progress + "%");
}
}
var i = 0;
function doProgress(){
while (i < 10) {
sleep(5000);
setProgress((i+1)*10);
i++;
}
}
function sleep(numberMillis){
var now = new Date();
var exitTime = now.getTime() + numberMillis;
while (true) {
now = new Date();
if (now.getTime() > exitTime)
return;
}
}
function cancelProgress(){
setProgress(0);
}
</script>
</head>
<body>
<div id="center">
<div id="loading">
<div>
</div>
<input type = "button" value="OK" style="width:100px" onclick="doProgress()"/>
<input type = "button" value="Cancel" style="width:100px" onclick="cancelProgress()"/>
</div>
</div>
</body>
</html>
在浏览器中打开此页面,点击【OK】按钮,进度条没有按照我们的预期每隔5秒前进10%,而是50秒后一次冲到了100%。
使用Webworker改写:
worker.js
var i = 0;
function doProgress(){
while (i < 10) {
sleep(5000);
postMessage((i + 1) * 10);
i++;
}
}
function sleep(numberMillis){
var now = new Date();
var exitTime = now.getTime() + numberMillis;
while (true) {
now = new Date();
if (now.getTime() > exitTime)
return;
}
}
doProgress();
页面代码:
<!DOCTYPE HTML>
<html>
<head>
<style type="text/css">
#center {
margin: 50px auto;
width: 400px;
}
#loading {
width: 397px;
height: 49px;
background: url(back.png) no-repeat;
}
#loading div {
width: 0px;
height: 48px;
background: url(proceed.png) no-repeat;
color: #fff;
text-align: center;
font-family: Tahoma;
font-size: 18px;
line-height: 48px;
}
</style>
<script type="text/javascript" src="jquery.js">
</script>
<script type="text/javascript">
function setProgress(progress){
if (progress >= 0) {
$("#loading > div").css("width", progress + "%");
$("#loading > div").html(progress + "%");
}
}
function doProgress(){
var w = new Worker("worker.js");
w.onmessage = function(event){
setProgress(event.data)
};
}
function cancelProgress(){
setProgress(0);
}
</script>
</head>
<body>
<div id="center">
<div id="loading">
<div>
</div>
<input type = "button" value="OK" style="width:100px" onclick="doProgress()"/>
<input type = "button" value="Cancel" style="width:100px" onclick="cancelProgress()"/>
</div>
</div>
</body>
</html>
在浏览器中再次打开此页面,点击【OK】按钮,进度条按照我们的预期前进了。
也许你会说,实现Worker进程和UI进程交互,其实不需要Worker:
- Swing里,可以用自定义线程+invokeLater(new Runnable(){...})
方式而不使用SwingWorker;
- 在Andriod里,可以使用自定义线程+handler.sengMessage
或runOnUiThread
而不使用AsyncTask
- js的武器库就更强大了,各种开源库,Ajax
,Web Worker
好像更没有存在的必要。
除了网络上的各种资料对上述几种Worker好处的介绍,笔者认为Worker
存在以及使用Worker
的主要好处在于:
- 减少重复。Worker对后台进程和UI进程交互行为进行了建模、封装,避免语言的使用者到处实现;
- 减少犯错。毕竟这个行业里有很多蹩脚的程序员,能让程序员想的、做的少一点,就尽量少一点吧;
- 职责分离。使用Worker,将后台处理和UI处理分离,实现了SOLID中最重要的单一职责原则。
总结
- 不同的技术,解决相似问题的思路总是相似的;
- 学习不同技术时,要善于比较,思考各种技术的异同