CodeGym /Kurslar /C# SELF /C#-da aşağı səviyyəli yaddaş idarəetməsi

C#-da aşağı səviyyəli yaddaş idarəetməsi

C# SELF
Səviyyə , Dərs
Mövcuddur

1. Giriş

Əvvəlki mühazirədə C# və .NET Runtime-ın yaddaş idarəetməsinin çox hissəsini necə öz üzərinə götürdüyündən danışdıq. Lakin bəzən performansın optimizasiyası və ya unmanaged kodla qarşılıqlı əlaqə (məsələn, C++-da yazılmış sistem API-ləri) üçün yaddaşa daha aşağı səviyyəli nəzarət etmək lazım gəlir. C# bu məqsədlər üçün bir sıra imkanlar təklif edir ki, onlar güclüdür, amma ehtiyatlı istifadə tələb edir.

C#-da "aşağı səviyyəli yaddaş idarəetməsi" dedikdə adətən nəzərdə tuturuq:

  • Göstəricilər ilə birbaşa iş: C/C++-dakı kimi yaddaşa ünvanla çıxış.
  • GC heap-dən kənar yaddaş ayırma: Garbage collector tərəfindən izlənməyən yaddaşdan istifadə.
  • Managed/unmanaged sərhədində resursların idarə edilməsi: native kitabxanalarla effektiv qarşılıqlı əlaqə.

C#-da adi kodun birbaşa yaddaş ünvanları (göstəricilər) ilə işləməsinə icazə verilmir ki, bu da təhlükəsizlik və stabillik üçün edilir. Amma aşağı səviyyəli əməliyyatlar üçün göstəricilər unsafe kontekstində istifadə oluna bilər.

unsafe konteksti nədir?

unsafe sözü ilə işarələnmiş blok və ya metod sizə göstərici sintaksisini istifadə etməyə və CLR tərəfindən təhlükəsizliyi yoxlanılmayan əməliyyatlar yerinə yetirməyə imkan verir. unsafe kontekstindəki kod native koddan heç də daha az təhlükəsiz deyil. unsafe koddan istifadə etmək üçün layihə /unsafe seçimi ilə kompilyasiya olunmalıdır.

Nümunə: unsafe metod və blok elan etmək


public unsafe class UnsafeExamples
{
    // Unsafe metod
    public static unsafe void ManipulatePointer(int* ptr)
    {
        Console.WriteLine($"Qiymət göstəricidən: {*ptr}");
        *ptr = 200; // Ünvan üzrə qiyməti dəyişdiririk
    }

    public static void DemoUnsafeBlock()
    {
        int value = 100;
        unsafe // adi metod daxilində unsafe blok
        {
            int* ptr = &value; // Dəyişənin ünvanını alırıq
            ManipulatePointer(ptr); // Göstəricini unsafe metoda ötürürük
            Console.WriteLine($"Yeni qiymət: {value}"); // Çap: Yeni qiymət: 200
        }
    }
}

Göstərici tipləri

C#-da qiymət tiplərinə göstəricilər elan etmək olar (int*, bool*, MyStruct*) və həmçinin void-a (void* universal göstərici üçün). Reference tiplərə birbaşa göstərici elan etmək olmaz, amma əgər reference tipin sahəsi qiymət tipidirsə, ona göstərici almaq mümkündür.

Nümunə: Müxtəlif göstərici tipləri


unsafe
{
    double d = 123.45;
    double* dPtr = &d; // double üçün göstərici

    int[] numbers = { 1, 2, 3 };
    fixed (int* arrPtr = numbers) // 'fixed' obyektin GC tərəfindən köçürülməsini əngəlləyir
    {
        Console.WriteLine($"Massivin ilk elementi: {arrPtr[0]}");
        Console.WriteLine($"Massivin ikinci elementi: {*(arrPtr + 1)}");
    }
}

Göstərici operatorları

  • & (ünvan): Dəyişənin ünvanını alır.
  • * (dereference): Ünvan üzrə qiyməti alır.
  • -> (üzvə çıxış): Göstərici vasitəsilə struktur/klasın üzvünə çıxış (yalnız qiymət tipləri üçün).
  • [] (index): Göstərici vasitəsilə massiv elementlərinə çıxış (C/C++-dakı kimi).

2. Pinlənmiş və ayrılmış yaddaş blokları

Göstəricilərlə işləyərkən vacibdir ki, göstəricinin işarə etdiyi obyekt sizin işiniz zamanı GC tərəfindən köçürülməsin. Bunun üçün fixedstackalloc açar sözlərindən istifadə olunur.

fixed operatoru

fixed operatoru reference tipli dəyişəni yaddaşda "pin" edir, yəni onun GC tərəfindən köçürülməsini fixed-blok bitənə qədər əngəlləyir. Bu, managed obyektlərin ünvanlarını unmanaged koda ötürərkən və ya onlarla birbaşa işləyərkən kritikdir.

Nümunə: Massivlərlə fixed istifadəsi


public unsafe class FixedExample
{
    public static void ProcessFixedArray()
    {
        byte[] buffer = new byte[10];
        // Buferi doldururuq
        for (int i = 0; i < buffer.Length; i++)
        {
            buffer[i] = (byte)(i + 1);
        }

        unsafe
        {
            // Massivi yaddaşda pin edirik, ilk elementə göstərici alırıq
            fixed (byte* p = buffer)
            {
                // İndi p ilə təhlükəsiz işləmək olar, massivin GC tərəfindən köçürülməyəcəyini bilirik
                Console.WriteLine($"Birinci bayt üzrə qiymət: {*p}");
                Console.WriteLine($"İkinci bayt üzrə qiymət: {*(p + 1)}");
                // 'p' göstəricisini native funksiyaya ötürmək olar
            } // 'fixed' blokundan çıxanda massiv GC tərəfindən yenidən köçürülə bilər
        }
    }
}

fixed həmçinin struktur sahələri və string-lərlə də istifadə edilə bilər.

Stack-da yaddaş ayırma: stackalloc

stackalloc stack-da yaddaş bloku ayırmağa imkan verir. Bu çox sürətlidir, amma yaddaş yalnız cari metodun müddətində mövcuddur. Ayırılan yaddaş GC tərəfindən idarə olunmur.

  • Üstünlüklər: Çox sürətli ayırma, GC-overhead yoxdur, yaddaşın müəyyən şəkildə sərbəst buraxılması.
  • Çatışmazlıqlar: Məhdud ölçü (stack nisbətən kiçikdir), çox böyük ayırmalarda stack overflow (StackOverflowException) riski.
  • İstifadə: Kiçik, qısa ömürlü buferlər üçün idealdır.

Nümunə: stackalloc istifadəsi


public unsafe class StackAllocExample
{
    public static void ProcessStackAlloc()
    {
        unsafe
        {
            // Stack-da 10 int ayırırıq
            int* numbers = stackalloc int[10];

            for (int i = 0; i < 10; i++)
            {
                numbers[i] = i * 10;
            }

            Console.WriteLine($"Birinci elementin qiyməti: {numbers[0]}");
            Console.WriteLine($"Beşinci elementin qiyməti: {numbers[4]}");
        } // Bu metoddan çıxanda yaddaş avtomatik azad olunur.
    }
}

stackalloc Span<T> ilə istifadə oluna bilər və bu, belə yaddaşa işləməni daha təhlükəsiz edir, çünki Span<T> idarə olunan bir strukturdur və ardıcıl yaddaş bloklarına təhlükəsiz çıxış təmin edir. Span haqqında daha ətraflı növbəti mühazirədə olacaq.

Nümunə: stackallocSpan<T>


using System;

public class StackAllocWithSpan
{
    public static void DemoSpanStackAlloc()
    {
        // Stack-da ayırma, amma təhlükəsiz Span
  
    vasitəsilə işləyirik Span
   
     buffer = stackalloc int[10]; for (int i = 0; i < buffer.Length; i++) { buffer[i] = i * 2; } Console.WriteLine($"Span-ın ilk elementi: {buffer[0]}"); Console.WriteLine($"Span-ın son elementi: {buffer[9]}"); // Span
    
     -ı onu qəbul edən metodlara ötürmək olar PrintSpan(buffer); } public static void PrintSpan(Span
     
       s) { foreach (var item in s) { Console.Write($"{item} "); } Console.WriteLine(); } } 
     
    
   
  

Bu hibrit yanaşmadır: yaddaşı stack-da (aşağı səviyyəli) ayırırsınız, amma Span<T> vasitəsilə təhlükəsiz (yüksək səviyyəli) çıxış əldə edirsiniz.

3. Unmanaged kodla qarşılıqlı əlaqə (P/Invoke)

Platform Invoke (P/Invoke) C# kodunun unmanaged kitabxanalardakı funksiyaları çağırmasına imkan verən mexanizmdir (məsələn, Windows API DLL-ləri və ya Linux .so faylları). Bu aşağı səviyyəli yaddaş idarəetməsinin əsas aspektidir, çünki tez-tez managed və unmanaged dünyalar arasında göstəricilər ötürürsünüz.

Xarici funksiyaların elan edilməsi

Native funksiyalara uyğun gələn static extern metodları elan etmək üçün DllImport atributundan istifadə edirsiniz.

Nümunə 3.1: Windows API funksiyasının çağırılması


using System.Runtime.InteropServices;

public class PInvokeExample
{
    // user32.dll-dən native MessageBox funksiyasını import edirik
    [DllImport("user32.dll", CharSet = CharSet.Unicode)]
    public static extern int MessageBox(IntPtr hWnd, string lpText, string lpCaption, uint uType);

    public static void ShowMessageBox()
    {
        // Native funksiyanı çağırırıq
        MessageBox(IntPtr.Zero, "C#-dan salam!", "Pəncərə başlığı", 0);
    }
}

Marşalinq (marshaling) məlumatları

Native funksiyalar çağırılarkən .NET Runtime marshaling həyata keçirir — managed və unmanaged formatlar arasında tip çevirmələri. Məsələn, C#-dakı string C++-da char* və ya wchar_t* kimi marshalanır.

Daha mürəkkəb marshaling üçün MarshalAs atributu və Marshal sinfindən istifadə etmək olar.

Nümunə: Struktur marshalingi


using System.Runtime.InteropServices;

// Bu struktur native struktura marshalanacaq
[StructLayout(LayoutKind.Sequential)] // Sahələrin ardıcıllığını göstəricisi
public struct NativePoint
{
    public int X;
    public int Y;
}

public class StructMarshalExample
{
    [DllImport("your_native_lib.dll")] // Misal: native kitabxanadakı funksiya
    public static extern void ProcessPoint(NativePoint point);

    [DllImport("your_native_lib.dll")]
    public static extern void FillPoint(out NativePoint point); // Struktur üçün pointer qəbul edir

    public static void DemoStructMarshal()
    {
        NativePoint myPoint = new NativePoint { X = 10, Y = 20 };
        ProcessPoint(myPoint); // Struktur dəyər üzrə marshalanacaq (kopya)

        NativePoint resultPoint;
        FillPoint(out resultPoint); // Native funksiya strukturu dolduracaq
        Console.WriteLine($"Native-dən gələn Point: ({resultPoint.X}, {resultPoint.Y})");
    }
}

4. GCHandle və obyektlərin pinlənməsi

GCHandle obyektə managed heap-də "handle" almağa və lazım gəldikdə onu müvəqqəti pinləməyə imkan verən strukturdur, yəni obyektin köçürülməsinin və ya toplanmasının qarşısını alır. Bu, managed obyektin sabit ünvanını unmanaged koda ötürmək lazım olduqda faydalıdır.

Nümunə: GCHandle ilə obyektin pinlənməsi


using System.Runtime.InteropServices;

public class GCHandleExample
{
    public static void PinObject()
    {
        byte[] data = new byte[100];
        GCHandle handle = GCHandle.Alloc(data, GCHandleType.Pinned); // Massivi yaddaşda pin edirik
        try
        {
            IntPtr pointer = handle.AddrOfPinnedObject(); // Pin edilmiş obyektin ünvanını alırıq
            Console.WriteLine($"Pin edilmiş massiv ünvanı: {pointer:X}");

            // İndi 'pointer' native funksiyaya təhlükəsiz ötürülə bilər.
            // Native funksiya bu yaddaşla birbaşa işləyə bilər.

            Marshal.WriteByte(pointer, 0, 255); // Göstərici vasitəsilə ilk baytı dəyişirik
            Console.WriteLine($"Massivin ilk baytı: {data[0]}"); // Çap: 255
        }
        finally
        {
            if (handle.IsAllocated)
            {
                handle.Free(); // Handle-i azad edirik, GC yenidən obyektə nəzarət edə bilər
            }
        }
    }
}

P/InvokeGCHandle-in detallı izahı bu kursun çərçivəsindən çıxır, amma onlar Windows kitabxanalarını birbaşa çağırmaq istədiyiniz zaman çox faydalı ola bilər. Ən azı, indi nəyi haradan öyrənmək lazım olduğunu bilirsiniz.

Şərhlər
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION