Song song hóa tập lệnh Bash với số lượng quy trình tối đa


86

Giả sử tôi có một vòng lặp trong Bash:

for foo in `some-command`
do
   do-something $foo
done

do-somethinglà CPU bị ràng buộc và tôi có một bộ xử lý 4 lõi sáng bóng đẹp mắt. Tôi muốn có thể chạy tối đa 4 do-somethinggiây cùng một lúc.

Cách tiếp cận ngây thơ dường như là:

for foo in `some-command`
do
   do-something $foo &
done

Điều này sẽ chạy tất cả do-something cùng một lúc, nhưng có một vài nhược điểm, chủ yếu là việc làm gì đó cũng có thể có một số I / O quan trọng khiến việc thực hiện tất cả cùng một lúc có thể chậm lại một chút. Vấn đề khác là khối mã này trả về ngay lập tức, vì vậy không có cách nào để thực hiện công việc khác khi tất cả các do-somethings đã hoàn thành.

Bạn sẽ viết vòng lặp này như thế nào để luôn có X do-somethingchạy cùng một lúc?


2
Là một sidenode, tôi đã mơ ước thêm tùy chọn make's -j vào bash cho nguyên thủy. Nó sẽ không hoạt động luôn luôn, nhưng đối với một số trường hợp đơn giản mà bạn biết phần thân của vòng lặp sẽ thực hiện một điều gì đó duy nhất cho mỗi lần lặp, sẽ khá dễ dàng nếu chỉ nói "for -j 4 ...".
thư giãn

1
Tham khảo chéo tới stackoverflow.com/questions/1537956/… để có giải pháp cơ bản giúp giảm thiểu các vấn đề về hiệu suất và cho phép các nhóm quy trình phụ, được giữ riêng biệt.
paxdiablo

1
Tôi muốn giới thiệu giải pháp của mình stackoverflow.com/a/28965927/340581
Tuttle

Câu trả lời:


62

Tùy thuộc vào những gì bạn muốn làm, xargs cũng có thể trợ giúp (ở đây: chuyển đổi tài liệu bằng pdf2ps):

cpus=$( ls -d /sys/devices/system/cpu/cpu[[:digit:]]* | wc -w )

find . -name \*.pdf | xargs --max-args=1 --max-procs=$cpus  pdf2ps

Từ các tài liệu:

--max-procs=max-procs
-P max-procs
       Run up to max-procs processes at a time; the default is 1.
       If max-procs is 0, xargs will run as many processes as  possible  at  a
       time.  Use the -n option with -P; otherwise chances are that only one
       exec will be done.

9
Phương pháp này, theo tôi, là giải pháp thanh lịch nhất. Ngoại trừ, vì tôi hoang tưởng, tôi luôn thích sử dụng find [...] -print0xargs -0.
amphetamachine

7
cpus=$(getconf _NPROCESSORS_ONLN)
mr.spuratic

1
Từ hướng dẫn sử dụng, tại sao không sử dụng --max-procs=0để có được nhiều quy trình nhất có thể?
EverythingRightPlace

@EverythingRightPlace, câu hỏi yêu cầu rõ ràng không có nhiều quá trình hơn các bộ xử lý có sẵn. --max-procs=0giống như nỗ lực của người hỏi hơn (bắt đầu càng nhiều quy trình như đối số).
Toby Speight

39

Với GNU Parallel http://www.gnu.org/software/parallel/ bạn có thể viết:

some-command | parallel do-something

GNU Parallel cũng hỗ trợ chạy các công việc trên máy tính từ xa. Điều này sẽ chạy một trên mỗi lõi CPU trên các máy tính từ xa - ngay cả khi chúng có số lõi khác nhau:

some-command | parallel -S server1,server2 do-something

Một ví dụ nâng cao hơn: Đây là danh sách các tệp mà chúng tôi muốn my_script chạy trên đó. Các tệp có phần mở rộng (có thể là .jpeg). Chúng tôi muốn đầu ra của my_script được đặt bên cạnh các tệp trong basename.out (ví dụ: foo.jpeg -> foo.out). Chúng tôi muốn chạy my_script một lần cho mỗi lõi mà máy tính có và chúng tôi cũng muốn chạy nó trên máy tính cục bộ. Đối với các máy tính từ xa, chúng tôi muốn tệp được xử lý được chuyển đến máy tính nhất định. Khi my_script kết thúc, chúng tôi muốn foo.out được chuyển trở lại và sau đó chúng tôi muốn foo.jpeg và foo.out bị xóa khỏi máy tính từ xa:

cat list_of_files | \
parallel --trc {.}.out -S server1,server2,: \
"my_script {} > {.}.out"

GNU Parallel đảm bảo đầu ra từ mỗi công việc không trộn lẫn, vì vậy bạn có thể sử dụng đầu ra làm đầu vào cho chương trình khác:

some-command | parallel do-something | postprocess

Xem video để biết thêm ví dụ: https://www.youtube.com/playlist?list=PL284C9FF2488BC6D1


1
Lưu ý rằng điều này thực sự hữu ích khi sử dụng findlệnh để tạo danh sách tệp, vì nó không chỉ ngăn chặn sự cố khi có khoảng trống bên trong tên tệp mà for i in ...; dotìm thấy cũng có thể thực hiện điều find -name \*.extension1 -or -name \*.extension2mà GNU song song {.} Có thể xử lý rất tốt.
Leo Izen

Cộng với 1 cat, tất nhiên là vô dụng.
tripleee


Ồ, là bạn! Tình cờ, bạn có thể cập nhật liên kết trên blog đó không? Vị trí partmaps.org đã chết một cách đáng tiếc, nhưng trình chuyển hướng Iki sẽ tiếp tục hoạt động.
tripleee

22
maxjobs = 4
song song hóa () {
        trong khi [$ # -gt 0]; làm
                jobcnt = (`công việc -p`)
                if [$ {# jobcnt [@]} -lt $ maxjobs]; sau đó
                        làm điều gì đó $ 1 &
                        sự thay đổi  
                khác
                        ngủ 1
                fi
        làm xong
        chờ đợi
}

song song hóa arg1 arg2 "5 args đến công việc thứ ba" arg4 ...

10
Nhận ra rằng có một số trích dẫn nghiêm trọng đang diễn ra ở đây nên bất kỳ công việc nào yêu cầu khoảng trắng trong các đối số sẽ thất bại nặng nề; hơn nữa, script này sẽ ăn sống CPU của bạn trong khi nó đang chờ một số công việc kết thúc nếu nhiều công việc được yêu cầu hơn mức maxjobs cho phép.
lhunath

1
Cũng lưu ý rằng điều này giả định rằng script của bạn không làm bất cứ điều gì khác liên quan đến công việc; nếu bạn là bạn, nó cũng sẽ tính những người đó vào maxjobs.
lhunath

1
Bạn có thể muốn sử dụng "công việc -pr" để giới hạn công việc đang chạy.
amphetamachine

1
Đã thêm lệnh ngủ để ngăn vòng lặp while lặp lại mà không có bất kỳ ngắt nào, trong khi đợi lệnh làm việc gì đó chạy xong. Nếu không, vòng lặp này về cơ bản sẽ chiếm một trong các lõi CPU. Điều này cũng giải quyết mối quan tâm của @lhunath.
euphoria83

12

Đây là một giải pháp thay thế có thể được chèn vào .bashrc và được sử dụng cho một lớp lót hàng ngày:

function pwait() {
    while [ $(jobs -p | wc -l) -ge $1 ]; do
        sleep 1
    done
}

Để sử dụng nó, tất cả những gì phải làm là đặt &sau các công việc và một lệnh gọi pwait, tham số cung cấp số lượng các quy trình song song:

for i in *; do
    do_something $i &
    pwait 10
done

Sẽ tốt hơn nếu sử dụng waitthay vì bận rộn chờ đợi kết quả đầu ra của jobs -p, nhưng dường như không có giải pháp rõ ràng là đợi cho đến khi bất kỳ công việc đã cho nào hoàn thành thay vì tất cả chúng.


11

Thay vì bash đơn giản, hãy sử dụng Makefile, sau đó chỉ định số lượng công việc đồng thời với make -jXX là số lượng công việc sẽ chạy cùng một lúc.

Hoặc bạn có thể sử dụng wait(" man wait"): khởi chạy một số tiến trình con, gọi wait- nó sẽ thoát khi các tiến trình con kết thúc.

maxjobs = 10

foreach line in `cat file.txt` {
 jobsrunning = 0
 while jobsrunning < maxjobs {
  do job &
  jobsrunning += 1
 }
wait
}

job ( ){
...
}

Nếu bạn cần lưu trữ kết quả của công việc, thì hãy gán kết quả của chúng cho một biến. Sau khi waitbạn chỉ cần kiểm tra những gì biến chứa.


1
Cảm ơn vì điều này, mặc dù mã chưa hoàn thành, nó đã cho tôi câu trả lời cho vấn đề tôi đang gặp phải tại nơi làm việc.
gerikson

những rắc rối duy nhất là nếu bạn giết kịch bản foreground (một với vòng lặp) các công việc đang chạy sẽ không bị giết chết cùng nhau
Girardi

8

Có thể thử một tiện ích song song hóa thay vì viết lại vòng lặp? Tôi là một fan hâm mộ lớn của xjobs. Tôi sử dụng xjobs mọi lúc để sao chép hàng loạt tệp trên mạng của chúng tôi, thường là khi thiết lập một máy chủ cơ sở dữ liệu mới. http://www.maier-komor.de/xjobs.html


7

Nếu bạn đã quen với makelệnh, hầu hết thời gian bạn có thể thể hiện danh sách các lệnh bạn muốn chạy dưới dạng một tệp trang điểm. Ví dụ: nếu bạn cần chạy $ SOME_COMMAND trên các tệp * .input, mỗi tệp sẽ tạo ra * .output, bạn có thể sử dụng makefile

INPUT = a.input b.input
OUTPUT = $ (INPUT: .input = .output)

%.đầu ra đầu vào
    $ (SOME_COMMAND) $ <$ @

tất cả: $ (OUTPUT)

và sau đó chỉ cần chạy

make -j <NUMBER>

để chạy song song nhiều nhất NUMBER lệnh.


6

Trong khi thực hiện điều này ngay trong bashcó lẽ là không thể, bạn có thể thực hiện bán quyền khá dễ dàng. bstarkđã đưa ra một sự xấp xỉ công bằng về quyền nhưng anh ta có những sai sót sau:

  • Tách từ: Bạn không thể chuyển bất kỳ công việc nào sử dụng bất kỳ ký tự nào sau đây trong các đối số của chúng: dấu cách, tab, dòng mới, dấu sao, dấu chấm hỏi. Nếu bạn làm vậy, mọi thứ sẽ đổ vỡ, có thể bất ngờ.
  • Nó dựa vào phần còn lại của tập lệnh của bạn để không làm nền cho bất kỳ thứ gì. Nếu bạn làm như vậy hoặc sau đó bạn thêm một cái gì đó vào tập lệnh được gửi trong nền vì bạn quên rằng mình không được phép sử dụng các công việc trong nền vì đoạn mã của anh ấy, mọi thứ sẽ hỏng.

Một phép gần đúng khác không có những sai sót này như sau:

scheduleAll() {
    local job i=0 max=4 pids=()

    for job; do
        (( ++i % max == 0 )) && {
            wait "${pids[@]}"
            pids=()
        }

        bash -c "$job" & pids+=("$!")
    done

    wait "${pids[@]}"
}

Lưu ý rằng cái này có thể dễ dàng điều chỉnh để cũng kiểm tra mã thoát của từng công việc khi nó kết thúc, do đó bạn có thể cảnh báo người dùng nếu công việc không thành công hoặc đặt mã thoát scheduleAlltùy theo số lượng công việc bị lỗi hoặc điều gì đó.

Vấn đề với mã này chỉ là:

  • Nó lên lịch cho bốn (trong trường hợp này) công việc cùng một lúc và sau đó đợi cả bốn kết thúc. Một số có thể được thực hiện sớm hơn những công việc khác, điều này sẽ khiến cho lô bốn công việc tiếp theo phải đợi cho đến khi công việc lâu nhất của lô trước được hoàn thành.

Một giải pháp kill -0xử lý vấn đề cuối cùng này sẽ phải sử dụng để thăm dò xem có bất kỳ quy trình nào đã biến mất thay vào đó waitvà lên lịch cho công việc tiếp theo hay không. Tuy nhiên, điều đó dẫn đến một vấn đề nhỏ mới: bạn có một điều kiện chạy đua giữa một công việc kết thúc và kill -0kiểm tra xem nó đã kết thúc hay chưa. Nếu công việc kết thúc và một quy trình khác trên hệ thống của bạn khởi động cùng lúc, lấy một PID ngẫu nhiên giống như công việc vừa hoàn thành, thì kill -0sẽ không thông báo công việc của bạn đã hoàn thành và mọi thứ sẽ lại hỏng.

Một giải pháp hoàn hảo là không thể trong bash.


3

hàm cho bash:

parallel ()
{
    awk "BEGIN{print \"all: ALL_TARGETS\\n\"}{print \"TARGET_\"NR\":\\n\\t@-\"\$0\"\\n\"}END{printf \"ALL_TARGETS:\";for(i=1;i<=NR;i++){printf \" TARGET_%d\",i};print\"\\n\"}" | make $@ -f - all
}

sử dụng:

cat my_commands | parallel -j 4

Việc sử dụng make -jlà thông minh, nhưng không có lời giải thích và khối mã Awk chỉ ghi, tôi không ủng hộ.
tripleee

2

Dự án tôi đang thực hiện sử dụng lệnh chờ để kiểm soát các quy trình trình bao song song (thực tế là ksh). Để giải quyết mối quan tâm của bạn về IO, trên một hệ điều hành hiện đại, có thể thực thi song song sẽ thực sự tăng hiệu quả. Nếu tất cả các quy trình đang đọc các khối giống nhau trên đĩa, thì chỉ quy trình đầu tiên sẽ phải tác động vào phần cứng vật lý. Các tiến trình khác thường có thể truy xuất khối từ bộ nhớ đệm đĩa của hệ điều hành trong bộ nhớ. Rõ ràng, đọc từ bộ nhớ nhanh hơn một số cấp độ so với đọc từ đĩa. Ngoài ra, lợi ích không yêu cầu thay đổi mã hóa.


1

Điều này có thể đủ tốt cho hầu hết các mục đích, nhưng không phải là tối ưu.

#!/bin/bash

n=0
maxjobs=10

for i in *.m4a ; do
    # ( DO SOMETHING ) &

    # limit jobs
    if (( $(($((++n)) % $maxjobs)) == 0 )) ; then
        wait # wait until all have finished (not optimal, but most times good enough)
        echo $n wait
    fi
done

1

Đây là cách tôi quản lý để giải quyết vấn đề này trong một tập lệnh bash:

 #! /bin/bash

 MAX_JOBS=32

 FILE_LIST=($(cat ${1}))

 echo Length ${#FILE_LIST[@]}

 for ((INDEX=0; INDEX < ${#FILE_LIST[@]}; INDEX=$((${INDEX}+${MAX_JOBS})) ));
 do
     JOBS_RUNNING=0
     while ((JOBS_RUNNING < MAX_JOBS))
     do
         I=$((${INDEX}+${JOBS_RUNNING}))
         FILE=${FILE_LIST[${I}]}
         if [ "$FILE" != "" ];then
             echo $JOBS_RUNNING $FILE
             ./M22Checker ${FILE} &
         else
             echo $JOBS_RUNNING NULL &
         fi
         JOBS_RUNNING=$((JOBS_RUNNING+1))
     done
     wait
 done

1

Thực sự muộn đến bữa tiệc ở đây, nhưng đây là giải pháp khác.

Rất nhiều giải pháp không xử lý dấu cách / ký tự đặc biệt trong các lệnh, không giữ cho N công việc chạy mọi lúc, ăn cpu trong các vòng lặp bận, hoặc dựa vào các phụ thuộc bên ngoài (ví dụ GNU parallel).

Với nguồn cảm hứng cho việc xử lý quy trình chết / zombie , đây là một giải pháp bash thuần túy:

function run_parallel_jobs {
    local concurrent_max=$1
    local callback=$2
    local cmds=("${@:3}")
    local jobs=( )

    while [[ "${#cmds[@]}" -gt 0 ]] || [[ "${#jobs[@]}" -gt 0 ]]; do
        while [[ "${#jobs[@]}" -lt $concurrent_max ]] && [[ "${#cmds[@]}" -gt 0 ]]; do
            local cmd="${cmds[0]}"
            cmds=("${cmds[@]:1}")

            bash -c "$cmd" &
            jobs+=($!)
        done

        local job="${jobs[0]}"
        jobs=("${jobs[@]:1}")

        local state="$(ps -p $job -o state= 2>/dev/null)"

        if [[ "$state" == "D" ]] || [[ "$state" == "Z" ]]; then
            $callback $job
        else
            wait $job
            $callback $job $?
        fi
    done
}

Và cách sử dụng mẫu:

function job_done {
    if [[ $# -lt 2 ]]; then
        echo "PID $1 died unexpectedly"
    else
        echo "PID $1 exited $2"
    fi
}

cmds=( \
    "echo 1; sleep 1; exit 1" \
    "echo 2; sleep 2; exit 2" \
    "echo 3; sleep 3; exit 3" \
    "echo 4; sleep 4; exit 4" \
    "echo 5; sleep 5; exit 5" \
)

# cpus="$(getconf _NPROCESSORS_ONLN)"
cpus=3
run_parallel_jobs $cpus "job_done" "${cmds[@]}"

Đầu ra:

1
2
3
PID 56712 exited 1
4
PID 56713 exited 2
5
PID 56714 exited 3
PID 56720 exited 4
PID 56724 exited 5

Đối với mỗi quá trình xử lý đầu ra $$có thể được sử dụng để đăng nhập vào một tệp, ví dụ:

function job_done {
    cat "$1.log"
}

cmds=( \
    "echo 1 \$\$ >\$\$.log" \
    "echo 2 \$\$ >\$\$.log" \
)

run_parallel_jobs 2 "job_done" "${cmds[@]}"

Đầu ra:

1 56871
2 56872

0

Bạn có thể sử dụng một vòng lặp for lồng nhau đơn giản (thay thế các số nguyên thích hợp cho N và M bên dưới):

for i in {1..N}; do
  (for j in {1..M}; do do_something; done & );
done

Điều này sẽ thực thi do_something N * M lần trong M vòng, mỗi vòng thực hiện N công việc song song. Bạn có thể làm cho N bằng số CPU bạn có.


0

Giải pháp của tôi để luôn duy trì một số quy trình nhất định đang chạy, tiếp tục theo dõi các lỗi và xử lý các quy trình ubnterruptible / zombie:

function log {
    echo "$1"
}

# Take a list of commands to run, runs them sequentially with numberOfProcesses commands simultaneously runs
# Returns the number of non zero exit codes from commands
function ParallelExec {
    local numberOfProcesses="${1}" # Number of simultaneous commands to run
    local commandsArg="${2}" # Semi-colon separated list of commands

    local pid
    local runningPids=0
    local counter=0
    local commandsArray
    local pidsArray
    local newPidsArray
    local retval
    local retvalAll=0
    local pidState
    local commandsArrayPid

    IFS=';' read -r -a commandsArray <<< "$commandsArg"

    log "Runnning ${#commandsArray[@]} commands in $numberOfProcesses simultaneous processes."

    while [ $counter -lt "${#commandsArray[@]}" ] || [ ${#pidsArray[@]} -gt 0 ]; do

        while [ $counter -lt "${#commandsArray[@]}" ] && [ ${#pidsArray[@]} -lt $numberOfProcesses ]; do
            log "Running command [${commandsArray[$counter]}]."
            eval "${commandsArray[$counter]}" &
            pid=$!
            pidsArray+=($pid)
            commandsArrayPid[$pid]="${commandsArray[$counter]}"
            counter=$((counter+1))
        done


        newPidsArray=()
        for pid in "${pidsArray[@]}"; do
            # Handle uninterruptible sleep state or zombies by ommiting them from running process array (How to kill that is already dead ? :)
            if kill -0 $pid > /dev/null 2>&1; then
                pidState=$(ps -p$pid -o state= 2 > /dev/null)
                if [ "$pidState" != "D" ] && [ "$pidState" != "Z" ]; then
                    newPidsArray+=($pid)
                fi
            else
                # pid is dead, get it's exit code from wait command
                wait $pid
                retval=$?
                if [ $retval -ne 0 ]; then
                    log "Command [${commandsArrayPid[$pid]}] failed with exit code [$retval]."
                    retvalAll=$((retvalAll+1))
                fi
            fi
        done
        pidsArray=("${newPidsArray[@]}")

        # Add a trivial sleep time so bash won't eat all CPU
        sleep .05
    done

    return $retvalAll
}

Sử dụng:

cmds="du -csh /var;du -csh /tmp;sleep 3;du -csh /root;sleep 10; du -csh /home"

# Execute 2 processes at a time
ParallelExec 2 "$cmds"

# Execute 4 processes at a time
ParallelExec 4 "$cmds"

-1

$ DOMAINS = "danh sách một số tên miền trong các lệnh" để some-command thực hiện

eval `some-command for $DOMAINS` &

    job[$i]=$!

    i=$(( i + 1))

làm xong

Ndomains =echo $DOMAINS |wc -w

cho tôi ở $ (seq 1 1 $ Ndomains) làm echo "đợi $ {job [$ i]}" đợi "$ {job [$ i]}" xong

trong khái niệm này sẽ làm việc cho song song hóa. điều quan trọng là dòng cuối cùng của eval là '&' sẽ đưa các lệnh vào nền.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.