Рубрики
Без рубрики

Что такое потоки?

Потоки всегда были сложной темой, но с небольшой помощью некоторых примеров, надеюсь, мы… С пометкой “Информатика”, “потоки”, “эликсир”, “java”.

Потоки всегда были сложной темой, но с небольшой помощью некоторых примеров, надеюсь, мы сможем сделать это правильно сегодня.

Потоки – это, по сути, последовательность элементов данных, которые доступны с течением времени. Точно так же, как настоящий поток воды, данные текут/становятся доступными, вместо того, чтобы иметь все это с самого начала. В этом есть много преимуществ, хотя два наиболее важных – это значительное повышение производительности и тот факт, что данные не всегда доступны немедленно.

Как только что указывалось, одна из основных причин использования потоков заключается в том, что иногда данные не сразу доступны . Например, если вы слушаете API потоковой передачи погоды, текущая температура вычисляется в текущий момент, поэтому (1) данные бесконечны до тех пор, пока вы продолжаете прослушивать сервис, и (2) новые данные доступны каждую минуту по мере их получения. Это невозможно смоделировать с помощью конечного объема данных, поэтому мы решаем смоделировать это с помощью бесконечного потока данных.

Поскольку размер потока не определен, они потенциально неограниченны, важно помнить, что мы не можем работать с ними в целом, как мы привыкли при работе со списками или массивами. Таким образом, функции, которые применяются к потокам, обычно возвращают другой поток с измененными данными. Они называются filters , и при соединении в цепочку они образуют конвейеры.

Еще одним упомянутым преимуществом была производительность . При обработке очень больших объемов данных это обычно может привести к очень большому объему памяти нашего компьютера. Если вы попытаетесь прочитать файл объемом 20 ГБ с данными о нем, обработать его, а затем отправить другу, это будет означать, что помимо любого объема памяти, используемого нашим приложением, мы собираемся загрузить дополнительные 20 ГБ в память. Большинство ноутбуков умрут! Но это не значит, что это невыполнимо.

Если вместо чтения файла в целом мы смоделируем его как поток данных, мы сможем читать по одной строке за раз, обрабатывать эти строки и отправлять их также в виде потока. Это заставило бы наше приложение просто использовать дополнительные несколько байтов вместо ужасного 20 ГБ. Когда мы думаем о потоках, мы всегда должны думать о них как о приятной последовательности управляемых фрагментов данных, которые может обрабатывать наше приложение – как только оно закончит с одним фрагментом, оно сможет прочитать и обработать следующий.

Упрощенный ввод-вывод по строке в Elixir

Нашим первым практическим подходом к потокам будет модуль StringIO в Elixir. A СтрингИО на самом деле это не настоящий поток, а просто оболочка вокруг строки, которая позволяет нам применять некоторые стандартные операции ввода-вывода над потоками к строке. Для нас это будет идеально, потому что мы можем использовать его для ознакомления с операциями:

  1. Открыть: получает эксклюзивный доступ к ресурсу.
  2. Чтение/запись: в основном считывает или записывает фрагменты данных из/в поток.
  3. Закрыть: возвращает ресурсы операционной системе (операционной системе).

Сначала мы открываем строку ввода-вывода. Из ответа функции open/1 мы видим, что она дает нам ссылку на поток.

iex>  {:ok, pid} = StringIO.open("content")
{:ok, #PID<0.470.0>}

Если мы хотим проверить содержимое потока, мы можем сделать это с помощью функции contents/1 , передав ей ранее полученную ссылку. В elixir функция contents/1 всегда будет возвращать кортеж с входным буфером и выходным буфером, например {"in", "out"} . В этом случае выходной буфер будет пуст, так как мы в него ничего не записали.

iex(1)> StringIO.contents(pid)
{"content", ""}

Поскольку StringIO является оболочкой, которая моделирует строку как поток, мы можем использовать стандартные функции для чтения и записи в потоки из модуля IO . В этом случае для записи некоторого содержимого мы можем использовать функцию write/2 . Обратите внимание, что теперь у нас есть данные как во входном буфере, так и в выходном буфере.

iex(2)> IO.write(pid, "written")
:ok

iex(3)> StringIO.contents(pid)
{"content", "written"}

Большинство потоковых модулей на большинстве языков также предоставляют нам способ очистки содержимого, что означает, что он заставляет записывать любые байты в потоке. Это относится и к выходному буферу.

iex(4)> StringIO.flush(pid)
"written"

iex(5)> StringIO.contents(pid)
{"content", ""}

Наконец, если мы хотим выполнить чтение из входного буфера, мы можем использовать функцию read/2 , тем самым опустошая поток данных:

iex(6)> IO.read(pid, :all)
"content"

iex(7)> StringIO.contents(pid)
{"", ""}

Обратите внимание, как в этом конкретном случае Elixir моделирует StringIO как кортеж с входным буфером и выходным буфером, буфером, в который мы можем записывать, и буфером, из которого мы можем читать.

Ввод-вывод в файле на C#/.NET

Переходя к более практическому примеру, мы собираемся проверить, как работать с файлами в потоках. При работе с файлами открытие и закрытие потоков начинает приобретать гораздо большее значение, но мы обсудим это позже.

В .NET land для создания файла мы можем использовать File. Создать функцию. Это предоставит нам FileStream , который моделирует наш файл, чтобы мы могли записывать в него. Как только мы откроем поток и запишем в него, нам придется закрыть его, чтобы сохранить эти изменения и освободить ресурсы, предоставленные нам ОС. Кроме того, чтобы снова прочитать содержимое, мы снова откроем другой поток с помощью File. Откройте Read и считывайте байт за байтом. Фрагмент выглядит следующим образом, ссылки на MSDN:

using System;
using System.IO;
using System.Text;

namespace StreamTime
{
    public class FileTheStream
    {

        public static void Main()
        {
            const string path = @"/Users/jgarcia/Desktop/example.txt";

            //Create the file.
            using (FileStream fs = File.Create(path))
            {
                AddText(fs, "This is some text");
                AddText(fs, "This is some more text,");
                AddText(fs, "\r\nand this is on a new line");
                AddText(fs, "\r\n\r\nThe following is a subset of characters:\r\n");

                for (var i = 1; i < 120; i++)
                {
                    AddText(fs, Convert.ToChar(i).ToString());
                }
            }

            //Open the stream and read it back.
            using (FileStream fs = File.OpenRead(path))
            {
                var b = new byte[1024];
                var temp = new UnicodeEncoding();
                while (fs.Read(b,0,b.Length) > 0)
                {
                    Console.WriteLine(temp.GetString(b));
                }
            }
        }

        private static void AddText(Stream fs, string value)
        {
            var info = new UnicodeEncoding().GetBytes(value);
            fs.Write(info, 0, info.Length);
        }
    }
}

Как вы можете видеть, с точки зрения кода мы просто открываем поток, читаем, записываем и закрываем его. Что ж, вам, возможно, интересно, где происходит закрытие. Примите во внимание, что в C# всякий раз, когда мы используем ключевое слово using с ресурсом, после завершения его использования ресурс закрывается – точно так же, как в Java try with resources . Но давайте поговорим дальше об открытии и закрытии потоков.

Почему потоки должны быть открыты или закрыты?

Мы упоминали, что каждый раз, когда мы открываем поток, ОС должна выделять ресурсы, но мы никогда не говорили о том, какие именно и как. В большинстве случаев это зависит от характера потока, который мы открыли – открыть сокет – это не то же самое, что открыть файл и т.д. На данный момент мы можем сосредоточиться на файлах.

Всякий раз, когда открывается новый поток файлов, ОС выделяет приложению дескриптор файла, обычно известный как дескриптор файла. Дескриптор файла в основном представляет собой число, которое однозначно идентифицирует открытый файл в операционной системе компьютера – оно описывает источник данных и способы доступа к нему. Дескрипторы файлов указывают на глобальную таблицу файлов ядра, которая содержит такую информацию, как смещение и ограничения доступа к потоку.

Как вы можете себе представить, файловые дескрипторы не похожи на память – если вы не вернете их в ОС, ситуация может быстро ухудшиться. Это всего лишь вопрос времени, пока приложение не выйдет из строя в долго работающем приложении. Обычно это называется утечкой дескриптора файла. На компьютерах с Windows, когда вы пытаетесь удалить файл, обычно одним из последствий является сообщение о том, что он используется другой программой. Ресурс не был освобожден должным образом.

К счастью, большинство языков в настоящее время предоставляют нам конструкции, которые позволяют нам соответствующим образом освобождать эти ресурсы. Как уже упоминалось, у нас есть операторы типа , использующие в C# или попробуйте с ресурсами на Java в нашем наборе инструментов. Однако в Elixir вы должны закрыть его с помощью функции IO.close/1 .

Ввод-вывод через сокет в Java

Другой сценарий, в котором используются потоки, – это когда приложение открывает сокет. Для того чтобы два процесса могли взаимодействовать, каждый из них должен открыть сокет, а затем отправлять свои сообщения через него – сокеты отправляют и получают сообщения через потоки. Далее отображается фрагмент кода, содержащий очень простой echo-сервер, разработанный на Java. Обратите внимание, как сокет содержит как входной поток, так и выходной поток, и как мы считываем данные, отправленные через входной поток, чтобы затем записать их в выходной поток.

package com.manzanit0;

import java.io.*;
import java.net.ServerSocket;

public class EchoServer {
    public static void main(String[] args) {
        int portNumber = 4098;

        try (
                var serverSocket = new ServerSocket(portNumber);
                var clientSocket = serverSocket.accept();
                var outputStream = clientSocket.getOutputStream();
                var inputStream = clientSocket.getInputStream()
        ) {
            while(true) {
                var bytes = readAllBytes(inputStream);
                outputStream.write(bytes);
            }

        } catch (IOException e) {
            System.out.println(e.getMessage());
        }
    }

    private static byte[] readAllBytes(InputStream stream) throws IOException {
        StringBuilder data = new StringBuilder();

        // available only returns a value after reading at least 1 character -> do/while.
        do {
            data.append((char) stream.read());
        } while (stream.available() > 0);

        return data.toString().getBytes();
    }
}

Еще одна вещь, которую я хочу упомянуть, – это то, что, поскольку потоки работают с неопределенными объемами данных, потенциально бесконечными, если вы попытаетесь выполнить программу, вы заметите, что она не прекращает работать, пока вы не подадите ей команду прерывания (Ctrl/Cmd + C). Мы можем продолжать вводить данные столько, сколько захотим, и поток продолжит их передавать. В этом конкретном примере я разработал конкретную функцию bytes[] ReadAllBytes(поток входного потока) , которая считывает только доступные байты и возвращает их, но класс InputStream предоставляет нам метод ReadAllBytes() , который блокируется до тех пор, пока поток не будет закрыт, затем возвращает все полученные байты.

Хотя вам, возможно, будет интересно, что делать, если я захочу прочитать данные моего потока во второй раз? Возможно ли это? . Действительно, это возможно, но для понимания того, как это сделать, мы должны ввести одну последнюю концепцию: ищу .

В поисках потока – понимание побочных эффектов чтения

Если вы попытались прочитать поток во второй раз, возможно, вы обнаружили, что не можете прочитать предыдущие данные, а просто читаете новые данные. Некоторые потоки не поддерживают поиск, но, предполагая, что они поддерживают, причина этого заключается в том, что потоки имеют курсор, который указывает на последний прочитанный байт. Каждый раз, когда мы считываем новый байт, этот курсор перемещается в новую позицию. Чтобы прочитать уже обработанные байты, нам нужно было бы перемотать этот курсор назад до самого начала. Это называется поиском.

В Java это можно сделать с помощью mark(int) и сброс() метод класса InputStream в примере C# мы бы просто установили file. Входной поток. Позиция . Это побочные эффекты чтения потока. Если поток не поддерживает поиск, другим решением было бы скопировать наши прочитанные байты в другой массив и сохранить копию. Тем не менее, примите во внимание, что иногда одной из целей использования потоков является снижение потребления памяти, и мы копируем все считанные данные в массив памяти, а затем полностью отменяем это.

Подведение итогов

Мы рассмотрели много вещей, но если бы мы должны были провести действительно быстрый TLDR и обобщить некоторые ключевые моменты, я бы просто пошел с:

  1. Потоки управляют неопределенными объемами данных – иногда бесконечными.
  2. Потоки обеспечивают огромное повышение производительности приложений, поскольку им не нужно загружать все данные в память перед их обработкой.
  3. Точно так же, как потоки должны быть открыты, они должны быть закрыты, в противном случае ресурсы операционной системы не освобождаются, и это потенциально может быть очень дорогостоящим, будь то сокеты или дескрипторы файлов.
  4. Некоторые языки предоставляют конструкции для обработки распоряжения ресурсами, например с использованием в C# или try в Java.
  5. Потоки имеют курсор, который указывает на последний прочитанный байт. В некоторых случаях мы можем перемотать этот курсор назад, чтобы прочитать те же данные во второй раз. Это называется поиском.

В заключение, на случай, если вы захотите немного подробнее разобраться в сокетах, как в концепции, не стесняйтесь ознакомиться с этим другим моим сообщением о них: URL .

Первоначально опубликовано на: Первоначально опубликовано на:

Оригинал: “https://dev.to/manzanit0/wtf-are-streams-1e9h”