1. Problem: bir qovluqdakı çoxlu faylı necə səmərəli emal etmək olar
Müasir tətbiqlərdə tez-tez belə bir vəzifə yaranır: qovluqdakı və onun alt qovluqlarındakı çoxlu faylları emal etmək. Məsələn:
- Layihənin bütün ".java"-fayllarında ümumi sətir sayını hesablamaq.
- Son ayda dəyişdirilmiş bütün faylları tapmaq.
- Müəyyən meyara görə faylları köçürmək və ya silmək.
Fayl azdırsa, adi dövr kifayətdir. Amma minlərlə və on minlərlə fayl olduqda, xüsusən hər fayl üzərində “ağır” əməliyyat (oxuma, parsinq, analiz) yerinə yetiriləndə, vaxt əhəmiyyətli dərəcədə artır.
Sual: çoxlu faylın emalını necə sürətləndirmək olar?
Cavab: paralelləşdirmədən istifadə etmək — faylları eyni vaxtda bir neçə axında emal etmək.
2. Fayl sistemini keçmək üçün alətlər
Files.walk()
Java 8+‑də qovluq ağacını keçmək üçün əlverişli üsul meydana çıxdı — metod Files.walk() java.nio.file paketindən. O, Stream<Path> axını qaytarır — göstərilən qovluqdan başlayaraq bütün fayl və qovluqları.
Nümunə:
import java.nio.file.*;
import java.util.stream.Stream;
Path start = Paths.get("src");
try (Stream<Path> stream = Files.walk(start)) {
stream.forEach(System.out::println);
}
- Files.walk(start) — alt qovluqlar daxil olmaqla bütün fayl və qovluqların axınını qaytarır.
- Keçidin maksimal dərinliyini göstərmək olar: Files.walk(start, 3).
Files.find()
Əgər dərhal əlamətə görə filtrləmək lazımdırsa (məsələn, yalnız ".java"-faylları), Files.find() istifadə edin:
import java.nio.file.*;
import java.util.stream.Stream;
Path start = Paths.get("src");
try (Stream<Path> stream = Files.find(
start,
Integer.MAX_VALUE,
(path, attr) -> path.toString().endsWith(".java"))) {
stream.forEach(System.out::println);
}
- Files.find() yol və fayl atributlarını alan filtri (BiPredicate<Path, BasicFileAttributes>) qəbul edir.
3. Paralel emal: parallel() və ForkJoinPool
Paralel stream-lər: .parallel()
Hər bir Stream üçün parallel() metodu var. Onu çağırdıqda elementlərin emalı bir neçə axında aparılacaq.
Files.walk(start)
.parallel()
.forEach(path -> processFile(path));
Hər fayl paralel emal olunacaq (imkan daxilində), bu isə xüsusən “ağır” əməliyyatlar: oxuma, parsinq, hesablamalar üçün effektivdir.
Daxildə bu necə işləyir? ForkJoinPool
Paralel stream-lər ümumi thread hovuzundan — ForkJoinPool.commonPool() — istifadə edir. Bu, tapşırıqları axınlar arasında bölüşdürən “ağıllı” hovuzdur.
- Defolt olaraq thread sayı mövcud prosessorların sayına bərabərdir: Runtime.getRuntime().availableProcessors().
- “fork/join” paralel modeli müstəqil tapşırıqlar üçün uyğundur — məsələn, ayrı-ayrı faylların emalı.
Nə vaxt .parallel() istifadə etməli?
- Hər faylın emalı digərlərindən asılı olmayanda.
- Əməliyyat “ağır” olanda (CPU‑nu yükləyir və ya IO çox gözləyir).
- Fayl çox olanda (yüzlərlə, minlərlə).
Məsləhət deyil paralel stream-lərdən istifadə etmək:
- Fayl azdırsa (paralelləşdirmənin xərcləri faydadan çox ola bilər).
- Sərt ardıcıllıq tələb olunursa və ya elementlər arasında asılılıqlar varsa.
4. Alternativlər və paralelliyin tənzimi
Nə vaxt ExecutorService daha uyğundur?
Paralel stream-lər sadə hallarda yaxşıdır. Amma əgər lazımdırsa:
- Thread sayını dəqiq idarə etmək ( IO-bound tapşırıqlarda nüvələrdən çox thread saxlamaq sərfəlidir).
- Növbələri, ləğv etməni, təkrar cəhdləri, səhv emalını idarə etmək.
- Daha mürəkkəb tapşırıq konveyerləri qurmaq.
Onda ExecutorService istifadə edin:
import java.nio.file.*;
import java.util.concurrent.*;
ExecutorService executor = Executors.newFixedThreadPool(8);
Files.walk(start)
.filter(Files::isRegularFile)
.forEach(path -> executor.submit(() -> processFile(path)));
executor.shutdown();
Tənzimləmə ForkJoinPool
Defolt olaraq ümumi hovuz prosessorların sayına bərabər thread sayı istifadə edir. Bunu sistem xassəsi vasitəsilə dəyişmək olar (paralel stream-lər ilk dəfə istifadə olunmadan əvvəl):
System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism", "16");
- Bundan sonra bütün paralel stream-lər ən çox 16 thread istifadə edəcək.
CPU-bound vs IO-bound tapşırıqlar
- CPU-bound: prosessoru aktiv yükləyir (riyaziyyat, parsinq, sıxma). Thread sayı ≈ nüvələrin sayı.
- IO-bound: disk/şəbəkə gözləməsi çoxdur. Çox vaxt thread sayını nüvələrdən çox saxlamaq sərfəlidir.
Paralel stream-lər IO-bound tapşırıqlar üçün həmişə optimal deyil — çox vaxt daha böyük hovuzla öz ExecutorService-iniz daha yaxşı nəticə verir.
5. Nümunə: faylların paralel axtarışı və emalı
Paralel keçiddən istifadə edərək layihənin bütün ".java"-fayllarında ümumi sətir sayını hesablayaq.
import java.nio.file.*;
import java.util.stream.*;
import java.io.IOException;
public class LineCounter {
public static void main(String[] args) throws IOException {
Path start = Paths.get("src");
long totalLines = Files.walk(start)
.parallel() // paralel emal!
.filter(p -> p.toString().endsWith(".java"))
.mapToLong(LineCounter::countLines)
.sum();
System.out.println("Kod sətirlərinin ümumi sayı: " + totalLines);
}
// Fayldakı sətirlərin sayını hesablamaq üçün metod
private static long countLines(Path path) {
try (Stream<String> lines = Files.lines(path)) {
return lines.count();
} catch (IOException e) {
System.err.println("Faylı oxuma xətası: " + path);
return 0;
}
}
}
Nə baş verir:
- Files.walk(start) — bütün yolların keçidi.
- parallel() — paralel emalı aktivləşdiririk.
- filter(...) — yalnız ".java"-faylları qalır.
- mapToLong(...) — hər fayldakı sətirləri sayırıq.
- sum() — nəticəni toplayırıq.
Üstünlüklər: bir neçə thread istifadə olunur və bununla belə kod yığcam qalır.
6. Vacib nüanslar və tipik səhvlər
- Bütün tapşırıqlar paralelləşdirmə ilə sürətlənmir. Kiçik fayl dəstləri və ya sürətli əməliyyatlar üçün əlavə xərclər proqramı ləngidə bilər.
- Resursları bağlayın. Fayllarla işləyərkən try-with-resources istifadə edin — beləliklə deskriptorlar “sızmayacaq”. Məsələn, Files.lines(path) try(...) daxilində.
- Daxili paralelləşdirmə. Paralel stream-ləri başqa paralel tapşırıqların içində işə salmaq (nested parallelism) nadir hallarda effektivdir və performansın pisləşməsinə səbəb ola bilər.
- Yan təsirlər. Sinxronizasiya olmadan ümumi strukturlara/fayllara yazmaqdan çəkinin. Elementlər üzərində “təmiz” əməliyyatlara üstünlük verin.
7. Sxem: faylların paralel keçidi necə işləyir
flowchart TD
A["Files.walk(start)"] --> B["Stream<Path>"]
B --> C{".parallel()?"}
C -- Xeyr --> D[Adi forEach]
C -- Bəli --> E["Paralel forEach (ForkJoinPool)"]
E --> F[Faylların bir neçə axında emalı]
8. Faylların paralel emalında tipik səhvlər
Xəta №1: Kiçik işlər üçün paralel stream-lərin istifadəsi — əlavə xərclər qazancı üstələyir.
Xəta №2: Paralel stream-lərin IO-bound tapşırıqları CPU-bound kimi sürətləndirəcəyini gözləmək. IO üçün çox vaxt daha böyük hovuzlu ExecutorService lazımdır.
Xəta №3: Lambda-larda emal olunmamış istisnalar — emal olmadan IOException axın yarıda kəsilə bilər və nəticə natamam olar.
Xəta №4: Ümumi dəyişənlərə və ya fayllara yazarkən yarışmalar — girişi sinxronlaşdırın və ya yan təsirlərdən qaçın.
Xəta №5: Resursları bağlamağı unutmaq — fayllarla bütün əməliyyatlar üçün try-with-resources istifadə edin.
Xəta №6: ForkJoinPool.commonPool() ilk istifadədən sonra onu dəyişməyə cəhd — sazlamanı System.setProperty(...) vasitəsilə əvvəlcədən etmək lazımdır.
Xəta №7: Paralel stream-ləri başqa paralel stream-lərin içində istifadə etmək — çox vaxt performansın pisləşməsinə gətirir.
GO TO FULL VERSION