CodeGym /Kurslar /JAVA 25 SELF /Fayllarla asinxron işləyərkən səhvlərin təhlili

Fayllarla asinxron işləyərkən səhvlərin təhlili

JAVA 25 SELF
Səviyyə, Dərs
Mövcuddur

1. Buferlərlə bağlı səhvlər: ByteBuffer, position və limit

Asinxron oxuma və yazma metodları ByteBuffer obyektləri ilə işləyir. Adi massivlərdən fərqli olaraq, buferin “daxili kursoru” — positionlimit var, hansı baytların oxunacağını və ya yazılacağını müəyyən edir. Bu xassələri düzgün idarə etməsəniz, gözlədiyiniz nəticəni əldə etməyə bilərsiniz və ya ümumiyyətlə məntiqi poza bilərsiniz.

Bu kodda necə görünür?

Səhv istifadə nümunəsi:

ByteBuffer buffer = ByteBuffer.allocate(1024);
channel.read(buffer, 0, buffer, new CompletionHandler<Integer, ByteBuffer>() {
    @Override
    public void completed(Integer result, ByteBuffer buf) {
        // Səhv: sətiri birbaşa buferdən oxumağa cəhd edirik:
        String str = new String(buf.array()); // Bu yanlışdır!
        // ...
    }
    // ...
});

Nə səhvdir?

  • Oxunuşdan sonra bufer “yazma” rejimində qalır: position oxunan məlumatların sonuna, limit isə buferin ölçüsünə işarə edir. Dərhal buferdən oxusanız, çoxlu “zibil” alacaqsınız (həqiqətən 10 bayt oxunsa belə, bütün 1024 bayt).
  • buf.array() çağırışı daxili massivinin hamısını qaytarır, yalnız oxunmuş hissəni deyil. Bundan əlavə, birbaşa buferlər üçün (allocated via ByteBuffer.allocateDirect) array() UnsupportedOperationException atacaq.

Düzgün necə?

Buferdən məlumatı oxumazdan əvvəl buffer.flip() çağırmaq lazımdır — bu, buferi “oxuma” rejiminə keçir:

public void completed(Integer result, ByteBuffer buf) {
    buf.flip(); // Indi position = 0, limit = oxunan baytların sayı
    String str = StandardCharsets.UTF_8.decode(buf).toString();
    // ... sətrin işlənməsi
}

Buferin təkrar istifadəsi

Buferi növbəti əməliyyat üçün yenidən istifadə etmək istəyirsinizsə, məlumatları emal etdikdən sonra mütləq buffer.clear() və ya buffer.compact() çağırın:

  • clear() — sərhədləri tam “sifirləyir”: position=0, limit=capacity(), köhnə məlumatlar zibil sayılır.
  • compact() — oxunmamış baytları buferin əvvəlinə köçürür və onu əlavə yazma üçün hazırlayır.

Nyans: əgər result -1-ə bərabərdirsə, faylın sonuna çatılıb — əlavə emal olmayacaq.

2. Paralel giriş: yarışlar və qeyri-konsistentlik

AsynchronousFileChannel eyni anda bir neçə əməliyyatı başlatmağa imkan verir. Lakin bu prosesi nəzarətsiz buraxsaz, asanlıqla “korlanmış” məlumatlar və ya proqramın çökməsi ilə üzləşə bilərsiniz.

Problem 1: Eyni buferə eyni vaxtda oxuma

// Iki eyni vaxtlı oxuma eyni buferə yazır
channel.read(buffer, 0, buffer, handler1);
channel.read(buffer, 1024, buffer, handler2);

Hər iki oxuma əməliyyatı eyni buferə yazır! Əgər onlar demək olar ki, eyni anda başa çatsa, buferin məzmunu qeyri-müəyyən olacaq.

Problem 2: Eyni fayla eyni vaxtda yazma

İki axın eyni fayl sahəsinə eyni vaxtda yazarsa, nəticə hansının daha tez bitməsindən asılı olacaq. Bu, klassik bir race condition-dır və məlumatların korlanmasına gətirib çıxara bilər.

Necə qarşısını almaq olar?

  • Hər asinxron əməliyyat üçün ayrı buferdən (ByteBuffer) istifadə edin — axınlar bir-birinin yaddaşına “girə” bilməsin.
  • Eyni faylın eyni diapazonuna paralel yazmaları işə salmayın; offsetləri ayırın və ya girişi sinxronlaşdırın.
  • Sərt ardıcıllıq vacibdirsə, yeni əməliyyatı yalnız əvvəlki bitdikdən sonra başladın — məsələn, CompletionHandler-də completed(...) daxilindən.

3. Resurs sızmaları: kanalı bağlamağı unutmaq

Asinxron kanal sistem resursudur. Onu bağlamasanız (channel.close()), fayl sistemdə “məşğul” qalacaq, yaddaş sızmaları yarana bilər, Windows-da isə digər proqramlar üçün fayl bloklana bilər.

Tipik səhv:

AsynchronousFileChannel channel = AsynchronousFileChannel.open(path, ...);
// ... əməliyyatı başladıq
// bütün əməliyyatlar bitdikdən sonra channel.close() çağırmağı unutduq!

Düzgün necə?

try-with-resources istifadə edin və blokdan çıxmazdan əvvəl mütləq bütün əməliyyatların tamamlanmasını gözləyin:

CountDownLatch latch = new CountDownLatch(1);

try (AsynchronousFileChannel channel =
         AsynchronousFileChannel.open(path, StandardOpenOption.READ)) {

    ByteBuffer buf = ByteBuffer.allocate(4096);
    channel.read(buf, 0, buf, new CompletionHandler<Integer, ByteBuffer>() {
        @Override public void completed(Integer r, ByteBuffer b) {
            // emal...
            latch.countDown();
        }
        @Override public void failed(Throwable ex, ByteBuffer b) {
            ex.printStackTrace();
            latch.countDown();
        }
    });

    latch.await(); // Asinxron əməliyyatın bitməsini gözləyirik
}
// Bağlanma avtomatik baş verəcək

4. İstisnaların işlənməsi: CompletionHandler-də səhvlərin görməməzliyə vurulması

Asinxron kodda səhvlər əsas axına “atılmır” — onlar failed(...) metoduna, CompletionHandler interfeysinə düşür. Onu reallaşdırmasanız və ya boş saxlasanız, səhvlər sadəcə itib gedəcək və proqram qəribə davranacaq.

“Görünməz” xəta nümunəsi:

channel.read(buffer, 0, buffer, new CompletionHandler<Integer, ByteBuffer>() {
    @Override
    public void completed(Integer result, ByteBuffer buf) {
        // ... nəticənin işlənməsi
    }
    @Override
    public void failed(Throwable exc, ByteBuffer buf) {
        // Eh, boşdur! Xəta itib
    }
});

Düzgün necə?

@Override
public void failed(Throwable exc, ByteBuffer buf) {
    System.err.println("Fayl oxunarkən xəta: " + exc.getMessage());
    exc.printStackTrace();
    // Mümkündür: kanalı bağlamaq, metrikləri yeniləmək, istifadəçini xəbərdar etmək və s.
}

5. Future/CompletionHandler-ə istinadların itirilməsi

Asinxron əməliyyatı Future vasitəsilə başladıb istinadı saxlamasanız, əməliyyatı ləğv edə və ya onun başa çatmasını gözləyə bilməyəcəksiniz. Oxşar şəkildə, CompletionHandler istifadə edirsinizsə, lakin bütün əməliyyatların tamamlanmasını sinxronlaşdırmırsınızsa, proqram vaxtından əvvəl bağlana bilər.

Nümunə:

channel.read(buffer, 0, buffer, handler); // handler anonimdir, heç yerdə saxlanmır
// Proqram oxunuşun bitməsini gözləmədən dərhal sona çatdı

Düzgün necə?

  • Future<Integer> ilə işləyərkən: istinadı saxlayın və lazım olduqda future.get() və ya future.cancel(true) istifadə edin.
  • CompletionHandler istifadə edərkən: sinxronizasiya mexanizmlərindən (CountDownLatch, Semaphore) istifadə edin və kanalın/proqramın bağlanmasından əvvəl bütün əməliyyatların başa çatmasını düzgün şəkildə gözləyin.

6. Kodlaşdırmalarla bağlı səhvlər

Mətn fayllarını oxumaq və yazmaq düzgün kodlaşdırma işlənməsini tələb edir. Baytları sadəcə “birbaşa” sətir kimi oxusanız, qarışıq simvollar əldə edə və ya məlumatların bir hissəsini itirə bilərsiniz, xüsusən faylı hissə-hissə oxuyarkən.

Problem:

// Faylı 1024 bayt-bayt oxuyuruq, sonra sətirə çeviririk
String chunk = new String(buffer.array(), "UTF-8");

Əgər sətir iki bufer arasında parçalanıbsa (məsələn, UTF-8 simvolunun bir baytı bir buferin sonunda, qalanları isə növbəti buferin əvvəlində qalıbsa), səhv simvollar və ya dekodlama xətası alacaqsınız.

Düzgün necə? CharsetDecoder istifadə edin və oxunuşlar arasında tamamlanmamış simvolların “qalıqlarını” saxlayın:

CharsetDecoder decoder = StandardCharsets.UTF_8.newDecoder()
        .onMalformedInput(CodingErrorAction.REPORT)
        .onUnmappableCharacter(CodingErrorAction.REPORT);

ByteBuffer byteBuf = ByteBuffer.allocate(4096);
CharBuffer charBuf = CharBuffer.allocate(4096);

// Hər completed(...): 
byteBuf.flip();
CoderResult cr = decoder.decode(byteBuf, charBuf, false); // false — bu, girişin sonu deyil
if (cr.isError()) {
    cr.throwException();
}
byteBuf.compact();
charBuf.flip();
String text = charBuf.toString();
charBuf.clear();

// Daha giriş olmayanda:
decoder.flush(charBuf);

7. Proqramın vaxtından əvvəl başa çatması

Asinxron əməliyyatlar digər axınlarda icra olunur. Əgər əsas axın bütün əməliyyatlardan əvvəl bitərsə, proqram nəticələri gözləmədən bağlanacaq.

Nümunə:

// Asinxron oxunuşu başladırıq
channel.read(buffer, 0, buffer, handler);
// Əsas axın dərhal bitdi — proqram bağlandı, əməliyyat başa çatmağa macal tapmadı

Düzgün necə?

CountDownLatch, Semaphore və ya heç olmasa Thread.sleep(...) (demo üçün) istifadə edin ki, bütün əməliyyatların tamamlanmasını gözləyəsiniz:

CountDownLatch latch = new CountDownLatch(1);
channel.read(buffer, 0, buffer, new CompletionHandler<Integer, ByteBuffer>() {
    @Override public void completed(Integer result, ByteBuffer buf) { /* ... */ latch.countDown(); }
    @Override public void failed(Throwable exc, ByteBuffer buf) { /* ... */ latch.countDown(); }
});
latch.await(); // Əməliyyatın bitməsini gözləyirik

8. ExecutorService ilə uğursuz inteqrasiya

AsynchronousFileChannel hadisələri emal etmək üçün öz ExecutorService-inizi göstərməyə imkan verir. Əgər az sayda axından ibarət və ya ümumiyyətlə tək axınlı hovuz versəniz, bütün əməliyyatlar paralel deyil, ardıcıl icra olunacaq. Əgər hovuz çox böyükdürsə — kontekst dəyişdirmə üçün əlavə xərclər yaranacaq.

Nümunə:

ExecutorService executor = Executors.newSingleThreadExecutor();
AsynchronousFileChannel channel = AsynchronousFileChannel.open(path, options, executor);
// Bütün asinxron əməliyyatlar mahiyyətcə sinxron olacaq!

Düzgün necə?

  • Hovuzun ölçüsünü real yükə və eyni vaxtda aparılan əməliyyatların sayına uyğun seçin.
  • Əksər tapşırıqlar üçün ForkJoinPool.commonPool() və ya Executors.newCachedThreadPool() uyğundur.
  • Unutmayın ki, ötürülən ExecutorService kolbek çağırışlarını (completed/failed) idarə edir, diskin I/O-sunu isə yox.
1
Tapşırıq
JAVA 25 SELF, səviyyə, dərs
Bağlanıb
Ani mesaj göndərilməsi: etibarlı bağlanma ilə asinxron yazı ✉️
Ani mesaj göndərilməsi: etibarlı bağlanma ilə asinxron yazı ✉️
1
Tapşırıq
JAVA 25 SELF, səviyyə, dərs
Bağlanıb
Sistemin işə salınmasının sinxronlaşdırılması: konfiqurasiyanın yüklənməsini gözləmək ⚙️
Sistemin işə salınmasının sinxronlaşdırılması: konfiqurasiyanın yüklənməsini gözləmək ⚙️
1
Sorğu/viktorina
, səviyyə, dərs
Əlçatan deyil
Fayllarla asinxron əməliyyatlar
Fayllarla asinxron əməliyyatlar
Şərhlər
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION