songofhawk

songofhawk 查看完整档案

北京编辑  |  填写毕业院校天助定  |  技术总监 编辑 www.tzding.com 编辑
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 该用户太懒什么也没留下

个人动态

songofhawk 发布了文章 · 11月4日

解决Kivy页面输入框无法显示Windows输入法的问题

Kivy是Python环境下"著名"的图形界面环境,好吧,实在不怎么著名,但Python下的UI框架都太弱了,相对来说,Kivy已经是最好的——起码有自己的页面布局语法,不至于每个控件都得自己用代码new出来,也可以做出比较有现代感的页面样式。

当然,作为一款不那么活跃,还野心勃勃的开源产品,Kivy的漏洞也是多多,不说别的,光中文支持就弱爆了。要想在界面上显示中文,需要做下面这些事儿:

image.png

  • 在kv文件中引用这个字体(相对路径),而且每个控件都要引用——当然,可以用自定义控件减少引用次数。例如:
<LabelTd@Label>:
    font_name:'DroidSansFallback'
    color: 0.18,0.18,0.18,1
    text_size: self.width, self.height

但这只解决了显示问题,还有输入问题,TextInput控件确实可以在输入时显示中文,但却隐藏了输入法的选词框。正常情况下,如果激活了输入法,应该是这样的:
image.png

然而Kivy的Input却是这样的:
image.png

输入完成后,字也可以显示出来,可盲选谁能做到啊?!!

为此我翻烂了google,包括中英文搜索,也只发现寥寥几句“这个问题是SDL造成的,需要修改源码,重新编译sdl2.dll”。What?这也太敷衍了吧!

于是又google+尝试了很久,终于在win7+VS2012环境下成功编译了SDL源码(详见另一篇“从源码编译 sdl2.dll”)。但是改源码的地方,跟网上说的也不同:

打开项目中“SDL_windowskeyboard.c”这个文件, 找到开头"#ifndef SDL_DISABLE_WINDOWS_IME" 这句, 在它的前面加上"#define SDL_DISABLE_WINDOWS_IME",强行定义这个宏,再编译就没有问题了。(源码里面多处引用这个宏定义,做编译分支选择)

#include "../../SDL_internal.h"

#if SDL_VIDEO_DRIVER_WINDOWS

#include "SDL_windowsvideo.h"

#include "../../events/SDL_keyboard_c.h"
#include "../../events/scancodes_windows.h"

#include <imm.h>
#include <oleauto.h>

/*这里定义一个宏,强行显示输入法选择框*/
#define SDL_DISABLE_WINDOWS_IME
#ifndef SDL_DISABLE_WINDOWS_IME
static void IME_Init(SDL_VideoData *videodata, HWND hwnd);
static void IME_Enable(SDL_VideoData *videodata, HWND hwnd);
static void IME_Disable(SDL_VideoData *videodata, HWND hwnd);
static void IME_Quit(SDL_VideoData *videodata);
#endif /* !SDL_DISABLE_WINDOWS_IME */

#ifndef MAPVK_VK_TO_VSC
#define MAPVK_VK_TO_VSC     0
#endif
#ifndef MAPVK_VSC_TO_VK
#define MAPVK_VSC_TO_VK     1
#endif
#ifndef MAPVK_VK_TO_CHAR
#define MAPVK_VK_TO_CHAR    2
#endif

/* Alphabetic scancodes for PC keyboards */

说实话这个宏的名字挺迷惑的,明明叫"disable_windos_ime",不就是“禁用windows输入法”的意思么,怎么定义以后反而可以显示了呢?

最终的结果不是很完美——输入法选择框虽然有了,但不能自动定位到TextInput控件下面,不过起码是可用了。

查看原文

赞 0 收藏 0 评论 0

songofhawk 发布了文章 · 11月4日

从源码编译 sdl2.dll

本人对C++开发完全不熟悉,但一个python项目需要的缘故,要修改SDL2的源码,网上搜资料,发现关于SDL源码编译的内容少得可怜,而且对C++的门外汉太不友好了,导致走了很多弯路。

尝试了各种环境:

- 本机 Win10+VS_2019: 生成成功,不能用(提示不是有效的win32程序)
- 本机 Win10+VS_2015: 生成失败,找不到windows sdk
- 本机 Win10+MinGW: make失败, 无效的提示符
- 虚拟机 WinXP+MinGW: 安装失败, 无法下载需要的库文件
- 虚拟机 Win7+VS_2013:生成失败, 找不到windows sdk
- 虚拟机 Win7+MinGW: 安装失败, 无法下载需要的库文件(后来从本机copy后安装成功, 但make又失败了)
- **虚拟机 Win7+VS_2012**:生成成功,修改平台为x64可用

最后在Win7+vs_2012环境成功了,却发现仅仅是平台选项的问题,返回去在“本机 Win10+VS_2019”环境下修改配置, 也成功了。不过应该还是2012环境下生成的兼容性更好一些,2019的这个版本太新了。

下面详细记述过程

  1. 安装visual studio 2012 express (免费版,之后的版本都叫community了),注意一定要下载DVD版的,exe版非常小,要在安装的时候下载需要的包,经常下载不下来。VS2012官网下载 / VS2012百度网盘下载-提取码:q138)
  2. 下载SDL2源码。SDL2 官网下载 / SDL2 百度网盘下载-提取码:8n7z下载SDL2
  3. 用VS2012打开项目,由于SDL源码中已经做好了vs的solution,直接找到解决方案文件打开就好了,位置在[SDL ROOT]/VisualC/SDL.sln。如下图所示:VS 2012 解决方案
  4. 右击SDL2这个项目直接“生成”就可以了,生成结果会输入在console中。生成结果

要注意的是:根据运行环境是32位和64位的,需要调整平台(platform)选项。

  1. 右击项目,选择属性:image.png
  2. 点击配置管理器image.png
  3. 分别选择release和debug版本对应的平台,32位选win32,64位选x64
  4. 生成的时候,debug版带调试信息,但文件大,性能较差,release版更适合生产运行时使用。

===

查看原文

赞 0 收藏 0 评论 0

songofhawk 发布了文章 · 3月28日

解决网页上元素旋转以后,边角超出父节点范围的问题

问题

在HTML中,利用css来做一个元素的旋转是很方便的。比如下面这样一张图片(边框是父节点的):
image.png

只要做transform:rotate(45deg)的设置就可以旋转45度了,如果想指定旋转中心点,那么就再设置一下transform-origin:50% 50%。旋转以后的效果如下:

image.png

但有时候,我们并不希望它超出父节点边框,同时还能基本保持在左上角,那么应该如何调整元素的位置呢?

分析

看起来好像挺简单,假设旋转中心点是(0,0)点,它的左上角坐标(x1,y1) ,宽为w1,长为h1:

image.png

顺时针旋转c度之后,左上角坐标的位置可以通过三角函数计算得到:
image.png

x2 = x1*cos(c) - y1*sin(c)
y2 = x1*sin(c) + y1*cos(c)

同理,可以推导中左下角的新坐标,然后给矩形一个偏移量,把两个角重新移回来即可。

但实际上有点麻烦,因为一旦旋转超出了90度,就不一定是哪个角超过边框了,简单的处理方式,就是无论如何,把4个角都算一遍,取最小的x和最小y,来决定偏移量。但是这样性能显然不太好。有没有一次性计算的方法呢?

还是有的——那就是不计算独立点的坐标,而是用三角函数直接算该矩形旋转以后的外接矩形长和宽:
image.png

w2 = sin(a) * h1 + cos(a) * w1;
h2 = sin(a) * w2 + cos(a) * h1;

那么用新的长宽与原来长宽做比较,就可以得出应该偏移的量了:

x偏移 = (w2 - w1)/2
y偏移 = (h2 - h1)/2
查看原文

赞 0 收藏 0 评论 0

songofhawk 关注了用户 · 3月24日

阿山 @a_shan

一只英语专业的程序猿

微信公众号:GitWeb

微信交流群:公众号内加好友(备注思否),拉你进群

关注 2889

songofhawk 发布了文章 · 3月14日

用js在页面中创建svg标签不显示的问题

一段内嵌的svg标签,大概这个样子:

<div style="width:600px;height:600px">
    <svg width="100%" height="100%">
        <defs>
            <pattern id="polka-dots" x="0" y="0" width="100" height="100" patternUnits="userSpaceOnUse">
                <image xlink:href="./image/face.png" x="0" y="0" width="50" height="50"></image>
            </pattern>
        </defs>
        <rect x="0" y="0" width="100%" height="100%" fill="url(#polka-dots)"></rect>
    </svg>
</div>

在矩形框里平铺了部分图片,效果如下:
image.png

直接写在html里没问题,但通过js动态创建节点,就怎么也不显示,但是矩形框区域还在。

网上搜一下,大概都是说创建节点的时候,需要指定Namespace,也就是用createElementNS来代替createElement创建节点,Stack Overflow上还有人提供了一段封装好的函数:

function makeSVG(tag, attrs) {
    let el= document.createElementNS('http://www.w3.org/2000/svg', tag);
    for (let k in attrs) {
            el.setAttribute(k, attrs[k]);
    }
    return el;
}

但使用这个函数创建以后, 还是无法显示,一开始怀疑是pattern的问题——也许不能动态创建pattern?经过仔细验证,最终确认原来是image标签中的xlink:href属性,竟然需要一个跟svg节点不同的Namespace:'http://www.w3.org/1999/xlink'。。。

于是修改创建节点的函数:


function makeSVG(tag, attrs) {
    const ns = 'http://www.w3.org/2000/svg';
    const xlinkns = 'http://www.w3.org/1999/xlink';

    let el= document.createElementNS(ns, tag);
    if (tag==='svg'){
        el.setAttribute('xmlns:xlink', xlinkns);
    }
    for (let k in attrs) {
        if (k === 'xlink:href') {
            el.setAttributeNS(xlinkns, k, attrs[k]);
        } else {
            el.setAttribute(k, attrs[k]);
        }
    }
    return el;
}

这下可以正常使用了,调用示例如下:

window.addEventListener('load', function(){
    const svgtest = document.getElementById('svg-test');
    let svg = makeSVG('svg');
    svg.style.width='100%';
    svg.style.height='100%';
    let defs = makeSVG('defs');
    let pattern = makeSVG('pattern', {id:'polka-dots',x:'0',y:'0',width:'100',height:'100',patternUnits:'userSpaceOnUse'});
    let image = makeSVG('image',{'xlink:href':'./image/face.png', width:'50', height: '50', x:'0', y:'0'});
    let rect = makeSVG('rect',{x:'0',y:'0',width:'100%',height:'100%',fill:'url(#polka-dots)'});
    // image.onload = function(){
    // };
    defs.appendChild(pattern);
    pattern.appendChild(image);
    svg.appendChild(defs);
    svg.appendChild(rect);
    svgtest.appendChild(svg);
});
查看原文

赞 1 收藏 1 评论 0

songofhawk 发布了文章 · 2019-01-04

Java 并发方案全面学习总结

并发与并行的概念

  • 并发(Concurrency): 问题域中的概念—— 程序需要被设计成能够处理多个同时(或者几乎同时)发生的事件
  • 并行(Parallelism): 方法域中的概念——通过将问题中的多个部分 并行执行,来加速解决问题。

进程、线程与协程

进程与线程

它们都是并行机制的解决方案。

  • 进程: 进程是什么呢?直白地讲,进程就是应用程序的启动实例。比如我们运行一个游戏,打开一个软件,就是开启了一个进程。进程拥有代码和打开的文件资源、数据资源、独立的内存空间。启动一个进程非常消耗资源,一般一台机器最多启动数百个进程。
  • 线程: 线程从属于进程,是程序的实际执行者。一个进程至少包含一个主线程,也可以有更多的子线程。线程拥有自己的栈空间。在进程内启动线程也要消耗一定的资源,一般一个进程最多启动数千个线程。操作系统能够调度的最小单位就是线程了。
  • 协程: 协程又从属于线程,它不属于操作系统管辖,完全由程序控制,一个线程内可以启动数万甚至数百万协程。但也正是因为它由程序控制,它对编写代码的风格改变也最多。

Java的并行执行实现

JVM中的线程

  • 主线程: 独立生命周期的线程
  • 守护线程: 被主线程创建,随着创建线程结束而结束

线程状态

Java中的线程状态

要注意的是,线程不是调用start之后马上进入运行中的状态,而是在"可运行"状态,由操作系统来决定调度哪个线程来运行。

Jetty中的线程

Web服务器都有自己管理的线程池, 比如轻量级的Jetty, 就有以下三种类型的线程:

  • Acceptor
  • Selector
  • Worker

Jetty线程模型

最原始的多线程——Thread类

继承类 vs 实现接口

  • 继承Thread类
  • 实现Runnable接口

实际使用中显然实现接口更好, 避免了单继承限制。

Runnable vs Callable

  • Runnable:实现run方法,无法抛出受检查的异常,运行时异常会中断主线程,但主线程无法捕获,所以子线程应该自己处理所有异常
  • Callable:实现call方法,可以抛出受检查的异常,可以被主线程捕获,但主线程无法捕获运行时异常,也不会被打断。

需要返回值的话,就用Callable接口
一个实现了Callable接口的对象,需要被包装为RunnableFuture对象, 然后才能被新线程执行, 而RunnableFuture其实还是实现了Runnable接口。

Future, Runnable 和FutureTask的关系如下:

Future, Runnable 和FutureTask的关系

可以看出FutureTask其实是RunnableFuture接口的实现类,下面是使用Future的示例代码

public class Callee implements Callable {
    AtomicInteger counter = new AtomicInteger(0);

    private Integer seq=null;

    public Callee()
    {
        super();
    }

    public  Callee(int seq)
    {
        this.seq = seq;
    }

    /**
     * call接口可以抛出受检查的异常
     * @return
     * @throws InterruptedException
     */
    @Override
    public Person call() throws InterruptedException {
        Person p = new Person("person"+ counter.incrementAndGet(), RandomUtil.random(0,150));
        System.out.println("In thread("+seq+"), create a Person: "+p.toString());
        Thread.sleep(1000);
        return  p;
    }
}
Callee callee1 = new Callee();
FutureTask<Person> ft= new FutureTask<Person>(callee1);
Thread thread = new Thread(ft);
thread.start();

try {
    thread.join();
} catch (InterruptedException e) {
    e.printStackTrace();
    return;
}

System.out.println("ft.isDone: "+ft.isDone());

Person result1;
try {
    result1 = ((Future<Person>) ft).get();
} catch (InterruptedException e) {
    e.printStackTrace();
    result1 = null;
} catch (ExecutionException e) {
    e.printStackTrace();
    result1 = null;
}
Person result = result1;
System.out.println("main thread get result: "+result.toString());

线程调度

  • Thread.yield() 方法:调用这个方法,会让当前线程退回到可运行状态,而不是阻塞状态,这样就留给其他同级线程一些运行机会
  • Thread.sleep(long millis):调用这个方法,真的会让当前线程进入阻塞状态,直到时间结束
  • 线程对象的join():这个方法让当前线程进入阻塞状态,直到要等待的线程结束。
  • 线程对象的interrupt():不要以为它是中断某个线程!它只是线线程发送一个中断信号,让线程在无限等待时(如死锁时)能抛出异常,从而结束线程,但是如果你吃掉了这个异常,那么这个线程还是不会中断的!
  • Object类中的wait():线程进入等待状态,直到其他线程调用此对象的 notify() 方法或 notifyAll() 唤醒方法。这个状态跟加锁有关,所以是Object的方法。
  • Object类中的notify():唤醒在此对象监视器上等待的单个线程。如果所有线程都在此对象上等待,则会选择唤醒其中一个线程。选择是任意性的,并在对实现做出决定时发生。线程通过调用其中一个 wait 方法,在对象的监视器上等待。 直到当前的线程放弃此对象上的锁定,才能继续执行被唤醒的线程。被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争;类似的方法还有一个notifyAll(),唤醒在此对象监视器上等待的所有线程。

同步与锁

内存一致性错误

由于线程在并行时,可能会"同时"访问一个变量, 所以共享变量的时候,会出现值处于一个不确定的状况, 例如下面的代码, c是一个实例变量, 多个线程同时访问increment或decrement方法时,就可能出现一致性错误,最终让c变成"奇怪"的值。

public class Counter {
    private int c = 0;

    public void increment() {
        c++;
    }

    public void decrement() {
        c--;
    }

    public int value() {
        return c;
    }
}

volatile

public class Foo {
    private int x = -1;
    private volatile boolean v = false;
    public void setX(int x) {
        this.x = x;
        v = true;
    }
    public int getX() {
        if (v == true) {
            return x;
        }
        return 0;
    }
}

volatile关键字实际上指定了变量不使用寄存器, 并且对变量的访问不会乱序执行,从而避免了并行访问的不一致问题。但这个方案仅仅对原始类型变量本身生效,如果是++或者--这种“非原子”操作,则不能保证多线程操作的正确性了

原子类型

JDK提供了一系列对基本类型的封装,形成原子类型(Atomic Variables),特别适合用来做计数器

import java.util.concurrent.atomic.AtomicInteger;

class AtomicCounter {
    private AtomicInteger c = new AtomicInteger(0);

    public void increment() {
        c.incrementAndGet();
    }

    public void decrement() {
        c.decrementAndGet();
    }

    public int value() {
        return c.get();
    }
}

原子操作的实现原理,在Java8之前和之后不同

  • Java7
public final int getAndIncrement() {
    for (;;) {
        int current = get();
        int next = current + 1;
        if (compareAndSet(current, next))
            return current;
    }
}
  • Java8
public final int getAndIncrement() {
    return unsafe.getAndAddInt(this, valueOffset, 1);
}

至于Compare-and-Swap,以及Fetch-and-Add两种算法,是依赖机器底层机制实现的。

线程安全的集合类

  • BlockingQueue: 定义了一个先进先出的数据结构,当你尝试往满队列中添加元素,或者从空队列中获取元素时,将会阻塞或者超时
  • ConcurrentMap: 是 java.util.Map 的子接口,定义了一些有用的原子操作。移除或者替换键值对的操作只有当 key 存在时才能进行,而新增操作只有当 key 不存在时。使这些操作原子化,可以避免同步。ConcurrentMap 的标准实现是 ConcurrentHashMap,它是 HashMap 的并发模式。
  • ConcurrentNavigableMap: 是 ConcurrentMap 的子接口,支持近似匹配。ConcurrentNavigableMap 的标准实现是 ConcurrentSkipListMap,它是 TreeMap 的并发模式。

ThreadLocal-只有本线程才能访问的变量

ThreadLoal 变量,它的基本原理是,同一个 ThreadLocal 所包含的对象(对ThreadLocal< String >而言即为 String 类型变量),在不同的 Thread 中有不同的副本(实际是不同的实例,后文会详细阐述)。这里有几点需要注意

因为每个 Thread 内有自己的实例副本,且该副本只能由当前 Thread 使用。这是也是 ThreadLocal 命名的由来
既然每个 Thread 有自己的实例副本,且其它 Thread 不可访问,那就不存在多线程间共享的问题。

它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本。ThreadLocal 变量通常被private static修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。

总的来说,ThreadLocal 适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用,也即变量在线程间隔离而在方法或类间共享的场景。后文会通过实例详细阐述该观点。另外,该场景下,并非必须使用 ThreadLocal ,其它方式完全可以实现同样的效果,只是 ThreadLocal 使得实现更简洁。

synchronized关键字

  • 方法加锁:其实不是加在指定的方法上,而是在指定的对象上,只不过在方法开始前会检查这个锁
  • 静态方法锁:加在类上,它和加在对象上的锁互补干扰
  • 代码区块锁:其实不是加在指定的代码块上,而是加在指定的对象上,只不过在代码块开始前会检查这个锁。一个对象只会有一个锁,所以代码块锁和实例方法锁是会互相影响的

需要注意的是:无论synchronized关键字加在方法上还是对象上,它取得的锁都是对象,而不是把一段代码或函数当作锁――而且同步方法很可能还会被其他线程的对象访问,每个对象只有一个锁(lock)与之相关联

加锁不慎可能会造成死锁

线程池(Java 5)

用途

真正的多线程使用,是从线程池开始的,Callable接口,基本上也是被线程池调用的。

线程池全景图

线程池全景图
线程池类图

线程池的使用

        ExecutorService pool = Executors.newFixedThreadPool(3);

        Callable<Person> worker1 = new Callee();
        Future ft1 = pool.submit(worker1);

        Callable<Person> worker2 = new Callee();
        Future ft2 = pool.submit(worker2);

        Callable<Person> worker3 = new Callee();
        Future ft3 = pool.submit(worker3);

        System.out.println("准备通知线程池shutdown...");
        pool.shutdown();
        System.out.println("已通知线程池shutdown");
        try {
            pool.awaitTermination(2L, TimeUnit.SECONDS);
            System.out.println("线程池完全结束");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

线程池要解决的问题

  • 任务排队:当前能并发执行的线程数总是有限的,但任务数可以很大
  • 线程调度:线程的创建是比较消耗资源的,需要一个池来维持活跃线程
  • 结果收集:每个任务完成以后,其结果需要统一采集

线程池类型

  • newSingleThreadExecutor:创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
  • newFixedThreadPool:创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
  • newCachedThreadPool:创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。
  • newScheduledThreadPool:创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。
  • newSingleThreadScheduledExecutor:创建一个单线程的线程池。此线程池支持定时以及周期性执行任务的需求。

线程池状态

线程池状态迁移图

  • 线程池在构造前(new操作)是初始状态,一旦构造完成线程池就进入了执行状态RUNNING。严格意义上讲线程池构造完成后并没有线程被立即启动,只有进行“预启动”或者接收到任务的时候才会启动线程。这个会后面线程池的原理会详细分析。但是线程池是出于运行状态,随时准备接受任务来执行。
  • 线程池运行中可以通过shutdown()和shutdownNow()来改变运行状态。shutdown()是一个平缓的关闭过程,线程池停止接受新的任务,同时等待已经提交的任务执行完毕,包括那些进入队列还没有开始的任务,这时候线程池处于SHUTDOWN状态;shutdownNow()是一个立即关闭过程,线程池停止接受新的任务,同时线程池取消所有执行的任务和已经进入队列但是还没有执行的任务,这时候线程池处于STOP状态。
  • 一旦shutdown()或者shutdownNow()执行完毕,线程池就进入TERMINATED状态,此时线程池就结束了。
  • isTerminating()描述的是SHUTDOWN和STOP两种状态。
  • isShutdown()描述的是非RUNNING状态,也就是SHUTDOWN/STOP/TERMINATED三种状态。

任务拒绝策略

线程池任务拒绝策略

Fork/Join模型(Java7)

用途

计算密集型的任务,最好很少有IO等待,也没有Sleep之类的,最好是本身就适合递归处理的算法

分析

在给定的线程数内,尽可能地最大化利用CPU资源,但又不会导致其他资源过载(比如内存),或者大量空线程等待。

ForkJoinPool主要用来使用分治法(Divide-and-Conquer Algorithm)来解决问题。典型的应用比如快速排序算法。

这里的要点在于,ForkJoinPool需要使用相对少的线程来处理大量的任务。

比如要对1000万个数据进行排序,那么会将这个任务分割成两个500万的排序任务和一个针对这两组500万数据的合并任务。以此类推,对于500万的数据也会做出同样的分割处理,到最后会设置一个阈值来规定当数据规模到多少时,停止这样的分割处理。比如,当元素的数量小于10时,会停止分割,转而使用插入排序对它们进行排序。

那么到最后,所有的任务加起来会有大概2000000+个。问题的关键在于,对于一个任务而言,只有当它所有的子任务完成之后,它才能够被执行。

所以当使用ThreadPoolExecutor时,使用分治法会存在问题,因为ThreadPoolExecutor中的线程无法像任务队列中再添加一个任务并且在等待该任务完成之后再继续执行。而使用ForkJoinPool时,就能够让其中的线程创建新的任务,并挂起当前的任务,此时线程就能够从队列中选择子任务执行。

以上程序的关键是fork()和join()方法。在ForkJoinPool使用的线程中,会使用一个内部队列来对需要执行的任务以及子任务进行操作来保证它们的执行顺序。

那么使用ThreadPoolExecutor或者ForkJoinPool,会有什么性能的差异呢?

首先,使用ForkJoinPool能够使用数量有限的线程来完成非常多的具有父子关系的任务,比如使用4个线程来完成超过200万个任务。但是,使用ThreadPoolExecutor时,是不可能完成的,因为ThreadPoolExecutor中的Thread无法选择优先执行子任务,需要完成200万个具有父子关系的任务时,也需要200万个线程,显然这是不可行的。

ps:ForkJoinPool在执行过程中,会创建大量的子任务,导致GC进行垃圾回收,这些是需要注意的。

原理与使用

ForkJoinPool首先是ExecutorService的实现类,因此是特殊的线程池。

创建了ForkJoinPool实例之后,就可以调用ForkJoinPool的submit(ForkJoinTask<T> task) 或invoke(ForkJoinTask<T> task)方法来执行指定任务了。

其中ForkJoinTask代表一个可以并行、合并的任务。ForkJoinTask是一个抽象类,它还有两个抽象子类:RecusiveAction和RecusiveTask。其中RecusiveTask代表有返回值的任务,而RecusiveAction代表没有返回值的任务。

ForkJoin Pool 类图

个人认为ForkJoinPool设计不太好的地方在于,ForkJoinTask不是个接口,而是抽象类,实际使用时基本上不是继承RecursiveAction就是继承RecursiveTask,对业务类有限制。

示例

典型的一个例子,就是一串数组求和

public interface Calculator {
    long sumUp(long[] numbers);
}
public class ForkJoinCalculator implements Calculator {
    private ForkJoinPool pool;

    private static class SumTask extends RecursiveTask<Long> {
        private long[] numbers;
        private int from;
        private int to;

        public SumTask(long[] numbers, int from, int to) {
            this.numbers = numbers;
            this.from = from;
            this.to = to;
        }

        @Override
        protected Long compute() {
            // 当需要计算的数字小于6时,直接计算结果
            if (to - from < 6) {
                long total = 0;
                for (int i = from; i <= to; i++) {
                    total += numbers[i];
                }
                return total;
            // 否则,把任务一分为二,递归计算
            } else {
                int middle = (from + to) / 2;
                SumTask taskLeft = new SumTask(numbers, from, middle);
                SumTask taskRight = new SumTask(numbers, middle+1, to);
                taskLeft.fork();
                taskRight.fork();
                return taskLeft.join() + taskRight.join();
            }
        }
    }

    public ForkJoinCalculator() {
        // 也可以使用公用的 ForkJoinPool:
        // pool = ForkJoinPool.commonPool()
        pool = new ForkJoinPool();
    }

    @Override
    public long sumUp(long[] numbers) {
        return pool.invoke(new SumTask(numbers, 0, numbers.length-1));
    }
}

这个例子展示了当数组被拆分得足够小(<6)之后,就不需要并行处理了,而更大的数组就拆为两半,分别处理。

Stream(Java 8)

概念

别搞混了,跟IO的Stream完全不是一回事,可以把它看做是集合处理的声明式语法,类似数据库操作语言SQL。当然也有跟IO类似的地方,就是Stream只能消费一次,不能重复使用。

看个例子:

int sum = widgets.stream()
.filter(w -> w.getColor() == RED)
 .mapToInt(w -> w.getWeight())
 .sum();

流提供了一个能力,任何一个流,只要获取一次并行流,后面的操作就都可以并行了。
例如:

Stream<String> stream = Stream.of("a", "b", "c","d","e","f","g");
String str = stream.parallel().reduce((a, b) -> a + "," + b).get();
System.out.println(str);

流操作

流操作示意图

生成流

  • Collection.stream()
  • Collection.parallelStream()
  • Arrays.stream(T array) or Stream.of()
  • java.io.BufferedReader.lines()
  • java.util.stream.IntStream.range()
  • java.nio.file.Files.walk()
  • java.util.Spliterator
  • Random.ints()
  • BitSet.stream()
  • Pattern.splitAsStream(java.lang.CharSequence)
  • JarFile.stream()

示例

// 1. Individual values
Stream stream = Stream.of("a", "b", "c");
// 2. Arrays
String [] strArray = new String[] {"a", "b", "c"};
stream = Stream.of(strArray);
stream = Arrays.stream(strArray);
// 3. Collections
List<String> list = Arrays.asList(strArray);
stream = list.stream();

需要注意的是,对于基本数值型,目前有三种对应的包装类型 Stream:

IntStream、LongStream、DoubleStream。当然我们也可以用 Stream<Integer>、Stream<Long> >、Stream<Double>,但是 boxing 和 unboxing 会很耗时,所以特别为这三种基本数值型提供了对应的 Stream。

Intermediate

一个流可以后面跟随零个或多个 intermediate 操作。其目的主要是打开流,做出某种程度的数据映射/过滤,然后返回一个新的流,交给下一个操作使用。这类操作都是惰性化的(lazy),就是说,仅仅调用到这类方法,并没有真正开始流的遍历。

已知的Intermediate操作包括:map (mapToInt, flatMap 等)、 filter、 distinct、 sorted、 peek、 limit、 skip、 parallel、 sequential、 unordered。

Terminal

一个流只能有一个 terminal操作,当这个操作执行后,流就被使用“光”了,无法再被操作。所以这必定是流的最后一个操作。Terminal 操作的执行,才会真正开始流的遍历,并且会生成一个结果,或者一个 side effect。

已知的Terminal操作包括:forEach、 forEachOrdered、 toArray、 reduce、 collect、 min、 max、 count、 anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 iterator

  • reduce解析: reduce本质上是个聚合方法,它的作用是用流里面的元素生成一个结果,所以用来做累加,字符串拼接之类的都非常合适。它有三个参数

    • 初始值:最终结果的初始化值,可以是一个空的对象
    • 聚合函数:一个二元函数(有两个参数),第一个参数是上一次聚合的结果,第二个参数是某个元素
    • 多个部分结果的合并函数:如果流并发了,那么聚合操作会分为多段进行,这里显示了多段之间如何配合
  • collect: collect比reduce更强大:reduce最终只能得到一个跟流里数据类型相同的值, 但collect的结果可以是任何对象。简单的collect也有三个参数:

    • 最终要返回的数据容器
    • 把元素并入返回值的方法
    • 多个部分结果的合并

两个collect示例

//和reduce相同的合并字符操作
String concat = stringStream.collect(StringBuilder::new, StringBuilder::append,StringBuilder::append).toString();
//等价于上面,这样看起来应该更加清晰
String concat = stringStream.collect(() -> new StringBuilder(),(l, x) -> l.append(x), (r1, r2) -> r1.append(r2)).toString();
//把stream转成map
Stream stream = Stream.of(1, 2, 3, 4).filter(p -> p > 2);

List result = stream.collect(() -> new ArrayList<>(), (list, item) -> list.add(item), (one, two) -> one.addAll(two));
/* 或者使用方法引用 */
result = stream.collect(ArrayList::new, List::add, List::addAll);

协程

协程,英文Coroutines,也叫纤程(Fiber)是一种比线程更加轻量级的存在。正如一个进程可以拥有多个线程一样,一个线程也可以拥有多个协程。
协程概念

协程实际上是在语言底层(或者框架)对需要等待的程序进行调度,从而充分利用CPU的方法, 其实这完全可以通过回调来实现, 但是深层回调的代码太变态了,所以发明了协程的写法。理论上多个协程不会真的"同时"执行,也就不会引起共享变量操作的不确定性,不需要加锁(待确认)。

pythone协程示例

Pythone版协程示例

Pythone, Golang和C#都内置了协程的语法,但Java没有,只能通过框架实现,常见的框架包括:Quasar,kilim和ea-async

Java ea-async 协程示例

import static com.ea.async.Async.await;
import static java.util.concurrent.CompletableFuture.completedFuture;

public class Store
{
    //购物操作, 传一个商品id和一个价格
    public CompletableFuture<Boolean> buyItem(String itemTypeId, int cost)
    {
        //银行扣款(长时间操作)
        if(!await(bank.decrement(cost))) {
            return completedFuture(false);
        }
        try {
            //商品出库(长时间操作)
            await(inventory.giveItem(itemTypeId));
            return completedFuture(true);
        } catch (Exception ex) {
            await(bank.refund(cost));
            throw new AppException(ex);
        }
    }
}

参考资料


查看原文

赞 4 收藏 4 评论 0

songofhawk 发布了文章 · 2018-08-17

FabricJS 前端绘图库使用总结(一)

使用一段时间了,总得来说,是一款非常强大的矢量图工具,官方文档也算丰富,但文档组织形式不太好,而且少部分api设计不太一致,还是需要整理一下。

Canvas

canvas是最基础的对象,针对<canvas>标签做的封装,可以管理内部绘制的所有对象。
这个canvas对象,并不是DOM里的元素,如果需要控制dom,或者对应的context,还是需要自己获取的。

canvasElement = document.getElementById(canvasEle);
ctx = canvasElement.getContext("2d");

新建canvas对象的时候,可以指定宽高:

canvas = new fabric.Canvas(canvasElement, { 
            selection: false,
            width: 800,
            height:600
});

这里指定的宽高会覆盖css中设置的。注意这种创建对象的形式,Fabric.js里基本上都是类似的,类名表示要创建的对象类型,第一个参数是必要的数据,第二个参数是各种选项。

所有对canvas的修改,包括在其中添加删除对象,以及对象参数的修改,都需要调用渲染方法才能显示出来:

canvas.renderAll();

基本形状

线条-Line, 圆-Circle, 矩形-Rectangel等几何图形,都属于基本形状。

所有基本形状,都有对应的类,这样就可以通过类实例的属性和方法来控制它们的位置、颜色、大小等样式。所有类都继承自Object类,有一些公共的属性和方法。

创建

下面是画线的例子(给出两个顶点坐标):

        var line =  new fabric.Line([x1, y1, x2, y2], {
            strokeWidth: 2, //线宽
            stroke: rgba(255,0,0,0.8), //线的颜色
            selectable: false
        });
        canvas.add(line);

画圆的例子(顶点和半径是在选项里的),这里left和top其实就是(x,y),应该是借用了css里的命名。

        var circle =  new fabric.Circle({
            radius: 2,
            left: left,
            top: top,
            originX: 'center',
            originY: 'center',
            fill: rgba(0,200,0,0.8), 
            strokeWidth: 1,
            stroke: rgba(255,0,0,0.8),
            selectable: false
        };
        canvas.add(circle);

从这里可以看出,和创建canvas类似,第一个参数是这个类专用的(比如画直线的时候传的起止点坐标),第二个参数是通用选项,如果没有专用参数,那么第一个参数就直接是通用选项。所有创建完的形状,只有通过canvas的add方法加入创景,才能显示出来。

控制

left和top是每种Object都有的属性,至于它到底指图形中哪一个点的坐标,由originX和originY这组参数决定,它们相当于文本编辑软件里的对齐方式,originX有三种可选值:left,center, right;originY也有三种可选值:top, center, bottom。

它们的示意图如下:
image
http://fabricjs.com/test/misc...

如果希望每种对象缺省原点都在中心,可以这样设置:

fabric.Object.prototype.originX = fabric.Object.prototype.originY = 'center'

width和height也是可以直接存取的属性,顾名思义,表示长和宽(所有形状都是有外接矩形的,所以可以用长和宽来控制大小)。

除了上面那几个可以直接存取的属性,大部分属性需要使用get/set方法读写,比如:

line.left = pointer.x;
line.top = pointer.y;
line.set('stroke', startColor);
line.set('height', 20);

网上有些文章会写成line.stroke=color,或者line.setProperty('stroke',color)这样的形式,都是不能生效的,可能是早期版本的表达方式。

Image

Image跟其他形状类似,都是Object的子类,最大的区别在于,图像文件的加载是异步的,所以对Image的后续操作,都要在回调中完成。

var bkImage = fabric.Image.fromURL(imgUrl,function(img) {
    canvas.add(img);
}
查看原文

赞 1 收藏 1 评论 3

songofhawk 发布了文章 · 2018-08-10

图片标注工具选型

项目里需要做一个图片标注工具,就是在一张底图上绘制特定的图形,连线,或者标注长度。如下图所示

图片描述

核心需求如下:

  • 浏览器PC端使用
  • 绘制的点、线、图形,应该是矢量图,可以移动、缩放和旋转
  • 可以定制需要的标注工具,比如绘制坐标轴,需要跟随鼠标移动
  • 可以加载底图
  • 可以把绘制完成的矢量图+底图一起导出

基础技术没啥可选的,肯定是基于Canvas实现,关键是选一个好用的库,搜索了一下,并没有像三维引擎那样出现“大一统”的局面(Three.js),只有几款不太热门的选项,根据github热度粗选,剩下两个:

Fabric.js vs Two.js

一开始因为Two.js和Three.js命名接近,而且界面风格更现代而倾向于它,但调研后发现,Two.js竟然完全不支持加载image,虽说是专注于矢量图,这也有点过分了;而且它明显是为了做动画设计的,所有的example都是动画;它还没有自己的事件体系,借用了Backbone的。

这样看起来,老旧的Fabric.js就显得很可爱了,特别是它还直接支持鼠标拖拽、缩放、旋转矢量图对象。

不过引用Fabric的时候,出现了一点问题——我们的项目是基于iView的,模块化开发,一开始也想用npm安装,然后import的方式引用Fabric.js,谁知道引进来以后总是出错;后来发现只要用npm管理,Fabric就认为自己跑在服务器端的Node.js环境中,所以需要依赖一些后端解析dom的库——崩溃,我明明只是在浏览器中使用啊。Github上有人提过这个issue,建议提供前端版本的npm管理配置,但开源方好像没有精力搞这个事儿。

最后无奈,直接在最外层的html里加<script>标签引用,终于成功加载。

查看原文

赞 6 收藏 5 评论 1

songofhawk 关注了用户 · 2017-05-18

清蒸不是水煮 @evaaz

2017.02.22 - 2018.03.09
我不只是一个 SFer
从 2018.03.10 开始
我只是一个 SFer
(^o^)/
已离职

关注 818

songofhawk 回答了问题 · 2017-03-01

Ubuntu16 Tomcat9 设置开机自动重启时为什么起不来?

这个问题好奇怪,顶一下,等大神回复

关注 4 回答 3

认证与成就

  • 获得 22 次点赞
  • 获得 4 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 4 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2015-08-01
个人主页被 554 人浏览