Wielowątkowy zipper z single thread executor dodaje tylko jeden plik i nie czeka na główny wątek

0

Witajcie!

Stworzyłem sobie małego konsolowego zippera z użyciem wielowątkowości, którą aktualnie poznaję.
Mam dwa problemy:

  1. W zipie zostaje tylko jeden plik - ostatni z kolejki
  2. ExecutorService (konkretnie SingleThreadExecutor) nie zatrzymuje głównego wątku programu, przez co wiadomość o poprawnym utworzeniu zipa jest wyświetlana na początku

Klasa ZipFile, która ma za zadanie dodać tylko jeden plik do zipa:

package com.burdzi0;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Observable;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

public class ZipFile extends Observable{
	
	private File file;
	private File target;
	private long status = 0;
	private long fileLength;
	
	/*
	 * `lastSentStatus` and `currentStatus` are indicators
	 * for the Observable's update methods. The file's length 
	 * can be big enough to indicate the same value a few times.
	 * That makes the terminal/command line flicker.
	 * In order to prevent that situation the program checks
	 * if the value has changed and then decides to execute
	 * update method or not.
	 */
	private int lastSentStatus = 0;
	private int currentStatus = 0;
	
	public ZipFile(File file, File target) {
		this.file = file;
		this.target = target;
		fileLength = file.length();
	}
	
	private ZipEntry createZipEntry() {
		return new ZipEntry(file.getName());
	}
	
	public void zipIt() {
		try (FileInputStream fileIn = new FileInputStream(file);
				ZipOutputStream zipOut = new ZipOutputStream(new FileOutputStream(target, true))) {
			
			zipOut.putNextEntry(createZipEntry());

			int len;
			byte[] buffer = new byte[1024];
			while ((len = fileIn.read(buffer)) != -1) {
				zipOut.write(buffer);

				status += len;
				
				// Here is being calculated the percentage of
				// the file's length already sent
				currentStatus = (int) (status * 100.0 / fileLength);
				
				// If the currentStatus value is the same as
				// the lastSentStatus the update method
				// won't be executed
				if (currentStatus > lastSentStatus) {
					setChanged();
					notifyObservers(new Integer(currentStatus));
					lastSentStatus = currentStatus;
				}
			}
			System.out.println("\n[DONE] Zipped file " + file.getName() + " successfully!");
			
		} catch (FileNotFoundException e) {
			System.out.println("[ERROR] Couldn't find file " + file.getName());
		} catch (IOException e) {
			System.out.println("[ERROR] The I/O system is invalid");
		}
	}
}

Klasa ZipRunnable, której zadaniem jest zaimplementowanie interfejsów Runnable i Observer.

package com.burdzi0;

import java.io.File;
import java.util.Observable;
import java.util.Observer;

public class ZipRunnable implements Runnable, Observer{

	private File file;
	private File target;
	
	public ZipRunnable(File file, File target) {
		this.file = file;
		this.target = target;
	}
	
	@Override
	public void run() {
		ZipFile zipper = new ZipFile(file, target);
		zipper.addObserver(this);
		System.out.println("[INFO] Starting zipping file " + file.getName());
		zipper.zipIt();
	}
	
	@Override
	public void update(Observable o, Object arg) {
		System.out.print("\r[STATUS] Zipping file " + file.getName() + ": " + ((Integer)(arg)).intValue() + " %");
	}
}

Klasa Test do testowania aplikacji.

package com.burdzi0;

import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Test {
	
	public static void main(String[] args) {
		List<File> files = new ArrayList<File>();
		files.add(new File("I:\\Muzyka\\Wolves At The Gate - Types & Shadows (2016)\\01. Asleep.mp3"));
		files.add(new File("I:\\Muzyka\\Wolves At The Gate - Types & Shadows (2016)\\02. Flickering Flame.mp3"));
		File target = new File("I:\\Programowanie\\ONE.zip");
		
		ExecutorService executor = Executors.newSingleThreadExecutor();
		for (File file: files) {
			executor.execute(new ZipRunnable(file, target));
		}
		executor.shutdown();
		
		System.out.println("[DONE] Successfully created " + target.getName() + "!");
	}
}

Wszelkie pomysły mile widziane ;)

0

Mnie się wydaje, że robisz jedno archiwum (Z jednym plikiem, pierwszym), a później drugie z drugim plikiem (pierwsze archiwum jest chyba nadpisywane).

Mogę się mylić, ale to następne osoby mnie poprawią :)

0

Jeszcze dodam, że tu znalazłem działający kod:

http://www.pixeldonor.com/2013/oct/12/concurrent-zip-compression-java-nio/

Biały Terrorysta

0

@Biały Terrorysta Widziałem ten przykład, ale on nic nie wnosi do tego co ja robię. Nie chcę korzystać z dodatkowych bibliotek i chcę, aby każdy kolejny plik był obsługiwany przez nowy wątek. Nie mam pomysłu za bardzo jak otworzyć ZipOutputStream i korzystać z niego jednocześnie tworząc kolejne wątki dla plików.

1

Zrobiłeś kulawy misz-masz i ci nie działa. W podanym przez ciebie linku są dwie wersje. Wersja na samych strumieniach działa tylko jednowątkowo, a dopiero wersja z systemem plików opartym na pliku ZIP działa wielowątkowo. To co ty zrobiłeś to tworzenie archiwum ZIP od nowa w każdym zadaniu. Metoda zipIt tworzy plik od nowa. Plików ZIP nie można tak po prostu łączyć i oczekiwać, że magicznie powstanie nam archiwum będące połączeniem dwóch poprzednich. ZIP ma określony format - ma pojedynczy nagłówek i końcówkę, a dane w środku podzielone są na segmenty itd Jak skleisz dwa ZIPy to dostaniesz plik co będzie miał dwa nagłówki ZIPowe - jeden na początku pliku tak jak tego programy do obsługi ZIP oczekują, a drugi w środku co jest kompletnym WTFem w przypadku ZIPa. To co możesz skleić to np pliki bzip2, ale te nie są archiwami tylko zwykłymi plikami skompresowanymi - nie zawierają żadnych atrybutów kompresowanego pliku.

0

O to mi chodziło @Wibowit - każdy plik ma się dodawać do zipa tylko na jednym wątku (jeden po drugim, synchronicznie). Rozumiem, że mam przebudować program tak, aby wątki powstawały już w otwartym strumieniu (tj. dodawanie plików nie ma otwierać ZipOutputStream za każdym razem, tylko korzystać z jednego już otwartego)?
P.S. Link nie jest ode mnie

1

Aaa dobra pomieszałem piszących :]
Tak, otwórz strumień ZIP raz i zamknij dopiero jak dodasz wszystkie pliki. Pamiętaj, że ZipOutputStream nie jest thread-safe (strzelam), więc nie używaj go z wielu wątków jednocześnie.

1

Czyli @Wibowit potwierdził to co napisałem wcześniej.

Co do linku, który podałem to pierwszy kod jest "klasyczny" a dopiero drugi pokazuje jak uzyskać kompresję równolegle. Autor uzyskał (na wielu małych zdjęciach) około 50% przyspieszenie wykonania programu.

Biały Terrorysta

1 użytkowników online, w tym zalogowanych: 0, gości: 1