Truyền bá stdin sang các quá trình song song


13

Tôi có một nhiệm vụ xử lý một danh sách các tập tin trên stdin. Thời gian khởi động của chương trình là đáng kể và lượng thời gian mỗi tệp sẽ thay đổi rất nhiều. Tôi muốn sinh ra một số lượng đáng kể các quy trình này, sau đó gửi công việc tới bất kỳ nơi nào không bận rộn. Có một số công cụ dòng lệnh khác nhau gần như làm những gì tôi muốn, tôi đã thu hẹp nó thành hai tùy chọn gần như hoạt động:

find . -type f | split -n r/24 -u --filter="myjob"
find . -type f | parallel --pipe -u -l 1 myjob

Vấn đề là splitthực hiện một vòng tròn thuần túy, vì vậy một trong các quy trình bị tụt lại phía sau và ở lại phía sau, trì hoãn việc hoàn thành toàn bộ hoạt động; trong khi parallelmuốn sinh ra một quy trình cho mỗi N dòng hoặc byte đầu vào và tôi sẽ dành quá nhiều thời gian cho việc khởi động.

Có một cái gì đó như thế này sẽ sử dụng lại các quy trình và dòng cấp dữ liệu cho bất kỳ quy trình nào đã bỏ chặn các tiêu chuẩn?


splitLệnh đó đến từ đâu? Tên xung đột với tiện ích xử lý văn bản tiêu chuẩn .
Gilles 'SO- ngừng trở nên xấu xa'

@Gilles, đó là GNU: "split (GNU coreutils) 8.13" . Sử dụng nó như một sự thay thế kỳ lạ cho xargs có lẽ không phải là mục đích sử dụng mà là gần nhất với những gì tôi muốn tôi đã tìm thấy.
BCoates

2
Tôi đã suy nghĩ về điều đó, và một vấn đề cơ bản là biết rằng một ví dụ myjobđã sẵn sàng để nhận thêm đầu vào. Không có cách nào để biết rằng một chương trình đã sẵn sàng để xử lý nhiều đầu vào hơn, tất cả những gì bạn có thể biết là một số bộ đệm ở đâu đó (bộ đệm ống, bộ đệm stdio) đã sẵn sàng để nhận thêm đầu vào. Bạn có thể sắp xếp để chương trình của bạn gửi một số loại yêu cầu (ví dụ: hiển thị lời nhắc) khi nó sẵn sàng không?
Gilles 'SO- ngừng trở nên xấu xa'

Giả sử rằng chương trình không sử dụng bufering trên stdin, một hệ thống tập tin FUSE phản ứng với readcác cuộc gọi sẽ thực hiện thủ thuật. Đó là một nỗ lực lập trình khá lớn.
Gilles 'SO- ngừng trở nên xấu xa'

Tại sao bạn sử dụng -l 1trong các đối số parallel? IIRC, cho biết song song để xử lý một dòng đầu vào cho mỗi công việc (nghĩa là một tên tệp cho mỗi ngã ba của myjob, vì vậy rất nhiều chi phí khởi động).
cas

Câu trả lời:


1

Điều đó không có vẻ trong trường hợp chung như vậy. Nó ngụ ý rằng bạn có một bộ đệm cho mỗi quy trình và bạn có thể xem các bộ đệm từ bên ngoài để quyết định nơi đặt mục tiếp theo (lập lịch) ... Tất nhiên bạn có thể viết một cái gì đó (hoặc sử dụng một hệ thống hàng loạt như slurm)

Nhưng tùy thuộc vào quy trình là gì, bạn có thể xử lý trước đầu vào. Ví dụ: nếu bạn muốn tải xuống các tệp, cập nhật các mục từ DB hoặc tương tự, nhưng 50% trong số chúng cuối cùng sẽ bị bỏ qua (và do đó bạn có sự khác biệt lớn về xử lý tùy thuộc vào đầu vào), sau đó, chỉ cần thiết lập bộ xử lý trước xác minh mục nào sẽ mất nhiều thời gian (tệp tồn tại, dữ liệu đã được thay đổi, v.v.), do đó, bất cứ điều gì đến từ phía bên kia đều được đảm bảo mất một khoảng thời gian khá bằng nhau. Ngay cả khi heuristic không hoàn hảo, bạn vẫn có thể có một sự cải thiện đáng kể. Bạn có thể đổ những thứ khác vào một tệp và xử lý sau đó theo cách tương tự.

Nhưng điều đó phụ thuộc vào trường hợp sử dụng của bạn.


1

Không, không có một giải pháp chung chung. Người điều phối của bạn cần biết khi nào mỗi chương trình sẵn sàng để đọc một dòng khác và không có tiêu chuẩn nào tôi biết về điều đó cho phép điều đó. Tất cả những gì bạn có thể làm là đặt một dòng trên STDOUT và chờ đợi một cái gì đó để tiêu thụ nó; không thực sự là một cách tốt để nhà sản xuất trên một đường ống dẫn để biết người tiêu dùng tiếp theo đã sẵn sàng hay chưa.


0

Tôi không nghĩ vậy. Trong tạp chí yêu thích của tôi là một bài viết một lần về lập trình bash đã làm những gì bạn muốn. Tôi sẵn sàng tin rằng nếu có công cụ để làm điều đó thì họ đã đề cập đến chúng. Vì vậy, bạn muốn một cái gì đó dọc theo dòng:

set -m # enable job control
max_processes=8
concurrent_processes=0

child_has_ended() { concurrent_processes=$((concurrent_processes - 1)) }

trap child_has_ended SIGCHLD # that's magic calling our bash function when a child processes ends

for i in $(find . -type f)
do
  # don't do anything while there are max_processes running
  while [ ${concurrent_processes} -ge ${max_processes}]; do sleep 0.5; done 
  # increase the counter
  concurrent_processes=$((concurrent_processes + 1))
  # start a child process to actually deal with one file
  /path/to/script/to/handle/one/file $i &
done

Rõ ràng bạn có thể thay đổi lời mời thành kịch bản làm việc thực tế theo ý thích của bạn. Tạp chí tôi đề cập ban đầu làm những việc như thiết lập đường ống và thực sự bắt đầu các luồng công nhân. Kiểm tra xem mkfifo, nhưng tuyến đường đó phức tạp hơn nhiều vì các quy trình công nhân cần báo hiệu cho quy trình tổng thể rằng họ đã sẵn sàng nhận thêm dữ liệu. Vì vậy, bạn cần một fifo cho mỗi quy trình công nhân để gửi dữ liệu và một fifo cho quy trình chính để nhận nội dung từ các công nhân.

TUYÊN BỐ TỪ CHỐI Tôi đã viết kịch bản đó từ đỉnh đầu của tôi. Nó có thể có một số vấn đề cú pháp.


1
Điều này dường như không đáp ứng các yêu cầu: bạn đang bắt đầu một phiên bản khác nhau của chương trình cho từng mục.
Gilles 'SO- ngừng trở nên xấu xa'

Nó thường được sử dụng find . -type f | while read ihơn là sử dụng for i in $(find . -type f).

0

Đối với GNU Parallel, bạn có thể đặt kích thước khối bằng cách sử dụng --block. Tuy nhiên, nó yêu cầu bạn có đủ bộ nhớ để giữ 1 khối trong bộ nhớ cho mỗi quy trình đang chạy.

Tôi hiểu đây không phải là chính xác những gì bạn đang tìm kiếm, nhưng nó có thể là một công việc chấp nhận được cho đến bây giờ.

Nếu các tác vụ của bạn trung bình mất cùng thời gian, thì bạn có thể sử dụng mbuffer:

find . -type f | split -n r/24 -u --filter="mbuffer -m 2G | myjob"

0

Thử đi:

mkfifo cho từng quá trình.

Sau đó treo tail -f | myjobtrên mỗi fifo.

Ví dụ: thiết lập công nhân (quy trình myjob)

mkdir /tmp/jobs
for X in 1 2 3 4
do
   mkfifo pipe$X
   tail -f pipe$X | myjob &
   jobs -l| awk '/pipe'$X'/ {print $2, "'pipe$X'"}' >> pipe-job-mapping
done

Tùy thuộc vào ứng dụng của bạn (myjob), bạn có thể sử dụng các công việc -s để tìm các công việc đã dừng. Mặt khác, liệt kê các quy trình được sắp xếp theo CPU và chọn một tài nguyên tiêu thụ ít nhất. Có bản báo cáo công việc, ví dụ bằng cách đặt cờ trong hệ thống tệp khi nó muốn làm việc nhiều hơn.

Giả sử công việc dừng lại khi chờ nhập liệu, sử dụng

jobs -sl để tìm ra pid của một công việc đã dừng và giao nó làm việc, ví dụ

grep "^$STOPPED_PID" pipe-to-job-mapping | while read PID PIPE
do
   cat workset > $PIPE
done

Tôi đã thử nghiệm điều này với

garfield:~$ cd /tmp
garfield:/tmp$ mkfifo f1
garfield:/tmp$ mkfifo f2
garfield:/tmp$ tail -f f1 | sed 's/^/1 /' &
[1] 21056
garfield:/tmp$ tail -f f2 | sed 's/^/2 /' &
[2] 21058
garfield:/tmp$ echo hello > f1
1 hello
garfield:/tmp$ echo what > f2
2 what
garfield:/tmp$ echo yes > f1
1 yes

Điều này tôi phải thừa nhận là chỉ được pha chế nên ymmv.


0

Điều thực sự cần thiết để giải quyết điều này là một cơ chế xếp hàng của một số loại.

Có thể có các công việc đọc đầu vào của chúng từ Hàng đợi, chẳng hạn như hàng đợi tin nhắn SYSV, và sau đó các chương trình chạy song song chỉ cần đẩy các giá trị lên hàng đợi?

Một khả năng khác là sử dụng một thư mục cho hàng đợi, như thế này:

  1. đầu ra find tạo ra một liên kết tượng trưng cho mỗi tệp để xử lý trong một thư mục, pending
  2. mỗi quy trình công việc thực hiện một mvtrong các tệp đầu tiên mà nó nhìn thấy trong thư mục cho một thư mục anh chị em của pending, được đặt tên inprogress.
  3. nếu công việc thành công di chuyển tệp, nó thực hiện xử lý; mặt khác, nó quay lại để tìm và di chuyển tên tệp khác từpending

0

giải thích về câu trả lời của @ ash, bạn có thể sử dụng hàng đợi tin nhắn SYSV để phân phối công việc. Nếu bạn không muốn viết chương trình của riêng mình bằng C, có một tiện ích được gọi là ipcmdcó thể trợ giúp. Dưới đây là những gì tôi đặt lại với nhau để vượt qua sản lượng find $DIRECTORY -type fđể $PARALLELsố quy trình:

set -o errexit
set -o nounset

export IPCMD_MSQID=$(ipcmd msgget)

DIRECTORY=$1
PARALLEL=$2

# clean up message queue on exit
trap 'ipcrm -q $IPCMD_MSQID' EXIT

for i in $(seq $PARALLEL); do
   {
      while true
      do
          message=$(ipcmd msgrcv) || exit
          [ -f $message ] || break
          sleep $((RANDOM/3000))
      done
   } &
done

find "$DIRECTORY" -type f | xargs ipcmd msgsnd

for i in $(seq $PARALLEL); do
   ipcmd msgsnd "/dev/null/bar"
done
wait

Đây là bản chạy thử:

$ for i in $(seq 20 10 100) ; do time parallel.sh /usr/lib/ $i ; done
parallel.sh /usr/lib/ $i  0.30s user 0.67s system 0% cpu 1:57.23 total
parallel.sh /usr/lib/ $i  0.28s user 0.69s system 1% cpu 1:09.58 total
parallel.sh /usr/lib/ $i  0.19s user 0.80s system 1% cpu 1:05.29 total
parallel.sh /usr/lib/ $i  0.29s user 0.73s system 2% cpu 44.417 total
parallel.sh /usr/lib/ $i  0.25s user 0.80s system 2% cpu 37.353 total
parallel.sh /usr/lib/ $i  0.21s user 0.85s system 3% cpu 32.354 total
parallel.sh /usr/lib/ $i  0.30s user 0.82s system 3% cpu 28.542 total
parallel.sh /usr/lib/ $i  0.27s user 0.88s system 3% cpu 30.219 total
parallel.sh /usr/lib/ $i  0.34s user 0.84s system 4% cpu 26.535 total

0

Trừ khi bạn có thể ước tính một tệp đầu vào cụ thể sẽ được xử lý trong bao lâu các quy trình worker không có cách nào để báo cáo lại cho bộ lập lịch (như chúng làm trong các tình huống tính toán song song thông thường - thường thông qua MPI ), bạn thường không gặp may - hoặc phải trả tiền phạt cho một số công nhân xử lý đầu vào lâu hơn những người khác (vì sự bất bình đẳng của đầu vào) hoặc trả tiền phạt khi sinh ra một quy trình mới cho mỗi tệp đầu vào.


0

GNU Parallel đã thay đổi trong 7 năm qua. Vì vậy, ngày hôm nay nó có thể làm điều đó:

Ví dụ này cho thấy nhiều khối được đưa ra cho quy trình 11 và 10 hơn quy trình 4 và 5 vì 4 và 5 đọc chậm hơn:

seq 1000000 |
  parallel -j8 --tag --roundrobin --pipe --block 1k 'pv -qL {}0000 | wc' ::: 11 4 5 6 9 8 7 10
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.