Worker那些事儿

Worker那些事儿

Worker之前世今生

不知道是谁发明了worker这个单词,百度解释如下:劳动者; 工人,员工; [虫] 工蜂,工蚁…,看名字都是一些受苦的角色。

也不知道是谁把worker这个词引入了软件行业,几乎任何语言中都有worker线程的存在。有的语言中只是概念性的存在,有的语言则干脆定义的worker的API。如:Java GUI库Swing中的SwingWorker,C#中的BackgroundWorker,Android中的AsyncTask,HTML5中的Web Worker,…。

上文中提到了“worker线程”。是的,有了多线程,才有了worker这个词。编程语言的世界如此,其他行业亦如此。

众所周知,为了提高计算机资源的利用率,操作系统引入了进程/线程的概念。线程也被称作“轻量级进进程”,在大多数现代操作系统中,都是以线程为最基本的调度单位。通常来说,使用线程有如下的优势:

  1. 发挥多处理器的强大威力
  2. 简化问题建模方式
  3. 简化异步事件的处理方式
  4. 响应更灵敏的用户界面

看起来很美好,其实操作系统引入多线程是一把双刃剑。自从多线程出现后,就和另外一个词形影不离,那就是“线程安全”。看下最经典的非线程安全例子:

非线程安全的序列号生成器

public class UnSafeSessionGenerator {
    private int sessionID;

    public int getNextSessionID() {
        return sessionID++;
    }
}

如果执行时机不对,那么两个线程在调用getNextSessionID方法时会得到相同的值(100)。虽然自增运算sessionID++看上去是单个操作,但事实上包含三个独立的操作:读取sessionID,将sessionID加1,并将运算结果写入sessionID

错误执行结果示意图

unsafedemo

线程封闭技术

如何保证程序的线程安全性,不是本文讨论的重点。但是,从上面的例子中可以得出一个很朴素的结论:如果仅在单线程内部访问数据则可以保证线程的安全性。这种朴素的技术被称为“线程封闭”,它是实现线程安全性的最简单方式之一。在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.sengMessagerunOnUiThread而不使用AsyncTask
- js的武器库就更强大了,各种开源库,AjaxWeb Worker好像更没有存在的必要。

除了网络上的各种资料对上述几种Worker好处的介绍,笔者认为Worker存在以及使用Worker的主要好处在于:
- 减少重复。Worker对后台进程和UI进程交互行为进行了建模、封装,避免语言的使用者到处实现;
- 减少犯错。毕竟这个行业里有很多蹩脚的程序员,能让程序员想的、做的少一点,就尽量少一点吧;
- 职责分离。使用Worker,将后台处理和UI处理分离,实现了SOLID中最重要的单一职责原则。

总结
  • 不同的技术,解决相似问题的思路总是相似的;
  • 学习不同技术时,要善于比较,思考各种技术的异同
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值