Потоки всегда были сложной темой, но с небольшой помощью некоторых примеров, надеюсь, мы сможем сделать это правильно сегодня.
Потоки – это, по сути, последовательность элементов данных, которые доступны с течением времени. Точно так же, как настоящий поток воды, данные текут/становятся доступными, вместо того, чтобы иметь все это с самого начала. В этом есть много преимуществ, хотя два наиболее важных – это значительное повышение производительности и тот факт, что данные не всегда доступны немедленно.
Как только что указывалось, одна из основных причин использования потоков заключается в том, что иногда данные не сразу доступны . Например, если вы слушаете API потоковой передачи погоды, текущая температура вычисляется в текущий момент, поэтому (1) данные бесконечны до тех пор, пока вы продолжаете прослушивать сервис, и (2) новые данные доступны каждую минуту по мере их получения. Это невозможно смоделировать с помощью конечного объема данных, поэтому мы решаем смоделировать это с помощью бесконечного потока данных.
Поскольку размер потока не определен, они потенциально неограниченны, важно помнить, что мы не можем работать с ними в целом, как мы привыкли при работе со списками или массивами. Таким образом, функции, которые применяются к потокам, обычно возвращают другой поток с измененными данными. Они называются filters , и при соединении в цепочку они образуют конвейеры.
Еще одним упомянутым преимуществом была производительность . При обработке очень больших объемов данных это обычно может привести к очень большому объему памяти нашего компьютера. Если вы попытаетесь прочитать файл объемом 20 ГБ с данными о нем, обработать его, а затем отправить другу, это будет означать, что помимо любого объема памяти, используемого нашим приложением, мы собираемся загрузить дополнительные 20 ГБ в память. Большинство ноутбуков умрут! Но это не значит, что это невыполнимо.
Если вместо чтения файла в целом мы смоделируем его как поток данных, мы сможем читать по одной строке за раз, обрабатывать эти строки и отправлять их также в виде потока. Это заставило бы наше приложение просто использовать дополнительные несколько байтов вместо ужасного 20 ГБ. Когда мы думаем о потоках, мы всегда должны думать о них как о приятной последовательности управляемых фрагментов данных, которые может обрабатывать наше приложение – как только оно закончит с одним фрагментом, оно сможет прочитать и обработать следующий.
Упрощенный ввод-вывод по строке в Elixir
Нашим первым практическим подходом к потокам будет модуль StringIO
в Elixir. A СтрингИО
на самом деле это не настоящий поток, а просто оболочка вокруг строки, которая позволяет нам применять некоторые стандартные операции ввода-вывода над потоками к строке. Для нас это будет идеально, потому что мы можем использовать его для ознакомления с операциями:
- Открыть: получает эксклюзивный доступ к ресурсу.
- Чтение/запись: в основном считывает или записывает фрагменты данных из/в поток.
- Закрыть: возвращает ресурсы операционной системе (операционной системе).
Сначала мы открываем строку ввода-вывода. Из ответа функции 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 и обобщить некоторые ключевые моменты, я бы просто пошел с:
- Потоки управляют неопределенными объемами данных – иногда бесконечными.
- Потоки обеспечивают огромное повышение производительности приложений, поскольку им не нужно загружать все данные в память перед их обработкой.
- Точно так же, как потоки должны быть открыты, они должны быть закрыты, в противном случае ресурсы операционной системы не освобождаются, и это потенциально может быть очень дорогостоящим, будь то сокеты или дескрипторы файлов.
- Некоторые языки предоставляют конструкции для обработки распоряжения ресурсами, например
с использованием
в C# илиtry
в Java. - Потоки имеют курсор, который указывает на последний прочитанный байт. В некоторых случаях мы можем перемотать этот курсор назад, чтобы прочитать те же данные во второй раз. Это называется поиском.
В заключение, на случай, если вы захотите немного подробнее разобраться в сокетах, как в концепции, не стесняйтесь ознакомиться с этим другим моим сообщением о них: URL .
Первоначально опубликовано на: Первоначально опубликовано на:
Оригинал: “https://dev.to/manzanit0/wtf-are-streams-1e9h”