Vim で簡単に非同期処理を行うラッパを書いた

バックエンドに vimproc を使用して、Vim script で簡単に外部コマンドの非同期処理が行えるようなラッパを書いてみました。
これで比較的簡単に非同期処理を行うことが出来ると思います。
例によって updatetime に依存しているので、その値が大きいと時間は不適格かも知れません。

[使い方]

function! s:echo(str)
    echo a:str
endfunction

function! s:async()
    " 2秒後に "mami" を出力する
    call g:thread("ruby -e \" sleep 2; puts 'mami' \"", function("s:echo"))
endfunction

call s:async()


g:thread にコマンドと終了時に呼ばれる関数を指定していして使用します。
上記の場合は、:source した2秒後に "mami" を表示します。
また、複数同時に使用する事も出来ます。

function! s:echo(str)
    echo a:str
endfunction

function! s:multi_thread()
    echo "=== start ==="

    call g:thread("ruby -e \" sleep 2; puts 'mami' \"", function("s:echo"))
    call g:thread("ruby -e \" sleep 3; puts 'mado' \"", function("s:echo"))
    call g:thread("ruby -e \" sleep 1; puts 'homu' \"", function("s:echo"))

    " 全てのスレッドが終了するまで待つ
    call g:join_all()

    echo "=== finish ==="
endfunction

call s:multi_thread()

" output:
" === start ===
" homu
" 
" mami
" 
" mado
" 
" === finish ===


上記の場合は、最後に待ち処理が入るので、:source が終了するまで時間がかかります。
応用すれば、sleep sort なども行うことが出来ます。

function! s:echo(str)
    echo a:str
endfunction

function! s:sleep_sort()
    let data = [3, 6, 2, 8, 1, 9, 7, 4, 5]
    for n in data
        let thread = g:thread(printf("ruby -e 'sleep %d; puts %d'", n*2, n), function("s:echo"))
    endfor

    " 全てのスレッドの終了待ち
    call g:join_all()

    echo "=== finish ==="

    " 出力を確認する為
    sleep 3
endfunction

call s:sleep_sort()


若干ばらつきはがありますが、一応は問題なく sort されて出力されていますね。
こんな感じで簡単に非同期で外部コマンドを使用することができます。
今はエラーとか全く考慮していないのでそこら辺をもうちょっとどうにかしたい。
しかし、thread という名前がいまいち微妙。

[example]

function! s:echo(str)
    echo a:str
endfunction


function! s:async()
    " 2秒後に "mami" を出力する
    call g:thread("ruby -e \" sleep 2; puts 'mami' \"", function("s:echo"))
endfunction


function! s:multi_thread()
    echo "=== start ==="

    call g:thread("ruby -e \" sleep 2; puts 'mami' \"", function("s:echo"))
    call g:thread("ruby -e \" sleep 3; puts 'mado' \"", function("s:echo"))
    call g:thread("ruby -e \" sleep 1; puts 'homu' \"", function("s:echo"))

    " 全てのスレッドが終了するまで待つ
    call g:join_all()

    echo "=== finish ==="
endfunction


function! s:join()
    echo "=== start ==="

    let thread = g:thread("ls", function("s:echo"))
    " 終了待ち
    call thread.join()

    echo "=== finish ==="
endfunction


function! s:thread_group()
    
    call g:thread("ruby -e \" sleep 4; puts 'mami' \"", function("s:echo"))

    let data = [3, 6, 2, 8, 1, 9, 7, 4, 5]
    let s:result = []

    let group = g:thread_group()
    for n in data
        let thread = group.create_thread(printf("ruby -e 'sleep %d; puts %d'", n, n))
        function! thread.apply(result)
            let s:result += [ a:result ]
        endfunction
    endfor

    " グループスレッドの終了待ち
    call group.join_all()

    echo s:result
    
    echo "=== finish ==="
endfunction


function! s:sleep_sort()
    let data = [3, 6, 2, 8, 1, 9, 7, 4, 5]
    for n in data
        let thread = g:thread(printf("ruby -e 'sleep %d; puts %d'", n*2, n), function("s:echo"))
    endfor

    " 全てのスレッドの終了待ち
    call g:join_all()

    echo "=== finish ==="

    " 出力を確認する為
    sleep 3
endfunction


function! s:apply()
    echo "=== start ==="

    let thread = g:thread("ls", function("s:echo"))
    function! thread.apply(result)
        echo "result"
        echo a:result
    endfunction

    " 終了待ち
    call thread.join()

    echo "=== finish ==="
endfunction


function! s:ruby()
    call g:thread("ruby test.rb", function("s:echo"))
    echo "=== finish ==="
endfunction

[実装]

set updatetime=100


function! s:release(threads)
    for thread in a:threads
        call thread.release()
    endfor
endfunction

if has_key(g:, "thread_list")
    call s:release(values(g:thread_list))
endif
let g:thread_list = {}

let g:thread_counter = 0


augroup vim-thread
    autocmd!
    autocmd! CursorHold,CursorHoldI * call s:update(values(g:thread_list))
augroup END


function! s:update(threads)
    for thread in a:threads
        if !thread.is_finish
            call thread.update()
        endif
    endfor
endfunction


function! s:join(threads)
"     echo len(filter(copy(a:threads), "!v:val.is_finish"))
    while len(filter(copy(a:threads), "!v:val.is_finish"))
        call s:update(a:threads)
    endwhile
endfunction


function! s:thread_update(thread)
    let thread = a:thread
    let vimproc = thread.vimproc

    try
        if !vimproc.stdout.eof
            let thread.result .= vimproc.stdout.read()
        endif

        if !vimproc.stderr.eof
            let thread.result .= vimproc.stderr.read()
        endif

        if !(vimproc.stdout.eof && vimproc.stderr.eof)
            return 0
        endif
    catch
        echom v:throwpoint
    endtry

    call thread.finish()
endfunction

function! s:thread_entry(thread)
    let g:thread_list[g:thread_counter] = a:thread
    let a:thread.id = g:thread_counter
    let g:thread_counter += 1
endfunction


function! s:thread_release(thread)
    unlet g:thread_list[a:thread.id]
endfunction


function! g:join_all()
    call s:join(values(g:thread_list))
endfunction

function! g:make_thread(cmd, ...)
    let self = {
\       "id" : -1,
\       "is_finish" : 0,
\       "result" : "",
\       "command" : a:cmd,
\   }

    if a:0
        let self.apply = a:1
    endif
    
    function! self.update()
        call s:thread_update(self)
    endfunction
    
    function! self.run()
        call s:thread_entry(self)
        let vimproc = vimproc#pgroup_open(self.command)
        call vimproc.stdin.close()
        let self.vimproc = vimproc
    endfunction
    
    function! self.join()
        while !self.is_finish
            call self.update()
        endwhile
    endfunction

    function! self.release()
        call s:thread_release(self)
        if !has_key(self, "vimproc")
            return
        endif
        call self.vimproc.stdout.close()
        call self.vimproc.stderr.close()
        call self.vimproc.waitpid()
    endfunction
    
    function! self.finish()
        let self.is_finish = 1
        try
            if has_key(self, "apply")
                call self.apply(self.result)
            endif
        finally
            call self.release()
        endtry
    endfunction

    return self
endfunction

function! g:thread(cmd, ...)
    let thread = call("g:make_thread", [a:cmd] + a:000)
    call thread.run()
    return thread
endfunction


function! g:thread_group()
    let self ={
\       "thread_list" : []
\   }
    
    function! self.create_thread(...)
        let thread = call("g:thread", a:000)
        call add(self.thread_list, thread)
        return thread
    endfunction

    function! self.join_all()
        call s:join(self.thread_list)
    endfunction

    return self
endfunction