Pull to refresh
-3
0.2
Дмитрий @qrKot

Энтузиаст

Send message

Хороший продукт это тот который появился в условиях свободного рынка, а не под упряжкой в телеге.

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

не, это не про "возможность роста" в том смысле, что могут вырасти. Там про то, что "бла-бла-бла, все дорожает, но нельзя исключать, что провайдеры просто задирают цены при любой возможности"

Оппортунистический рост цен - это типа "повышаем, потому что можем". Сложились условия на рынке, что у конкурентов либо нет аналога, либо миграция очень дорогая - поднимаем цены, потому что "а куда ты денешься с подводной лодки"

Если вы не против, покритикую:

Думаю, наиболее легкое объяснение горутинам такое: горутина (goroutine) — это легковесный оберточный функционал над потоком.

Что такое "оберточный функционал над потоком"? Оно мало того, что не понятно, так еще и искажает суть явления.

goroutine is a lightweight thread managed by the Go runtime.

Легковесный тред, управляемый рантаймом Go. Это не обертка над потоком. Обертка над потоком - это Machine. Горутина - это штука, которая исполняется на этом потоке.

Планировщик операционной системы, в которой работает программа, переключает Машины.

Планировщик операционной системы не имеет прямого отношения к "Машинам". Он тупо запускает N (GOMAXPROCS) системных тредов, в которых они потом так и живут, ничего он не переключает. А вот планировщик в рантайме Go распределяет горутины по машинам, на которых они исполняются.

Для запуска функции как горутину необходимо написать go func() , где func() - функция, которую хотите запустить.

Не совсем так. go func() не запускает функцию, а всего лишь ставит горутину в очередь исполнения, передает ее планировщику, чтобы он по возможности отдал ее одной из "машин" на исполнение.

Собственно, поэтому следующее тоже не совсем верно:

Ничего не произойдет, ведь горутина выполняется параллельно с "main()"

В вашем случае с некоторой ненулевой вероятностью горутина вообще не выполняется, тупо в очереди находится (тем более в go playground, который, возможно, вообще однопоточный)

 // создаем WaitGroup
    wg := &sync.WaitGroup{}

Вот тут & излишне. Дальше ваша wg передается замыканием, а замыкание в Go проходит по указателю.

 "mutual exclusion", что в переводе означает "взаимное изолирование"

В переводе это означает "взаимное исключение"

это некая примитивная синхронизация

Это вы так synchronization primitive перевели? Не "некая примитивная синхронизация", а "примитив синхронизации". Как, к слову, и WaitGroup и даже, внезапно, канал.

Перед обновлением счетчика, мы вызываем "lock.Lock()" для создания блокировки. Теперь ни одна другая горутина не сможет использовать "counter".

Ну вы бы хоть рассказали, почему не сможет. Это примитив синхронизации. Он заблокирует горутину (да, он не значение блокирует, а горутину), пока инструкция не сможет быть исполнена.

Т.е. если вы лочите мьютекс из горутины №1, а потом пытаетесь сделать то же самое из горутины №2, то вторая горутина заблокируется, пока первая не разлочит мьютекс.

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

Атомарные операции - это низкоуровневые примитивы синхронизации, которые обеспечивают способ выполнения операций чтения-модификации-записи (read-modify-write) над общими переменными без необходимости в блокировке (Атомарная операция — операция, которая либо выполняется целиком, либо не выполняется вовсе; операция, которая не может быть частично выполнена и частично не выполнена).

Ну вот, в формулировке read-modify-write, а в примере ниже save/load. Не надо так.

Ну и вот это "Атомарная операция — операция, которая либо выполняется целиком, либо не выполняется вовсе" - это скорее к теории баз данных формулировка, и это про транзакционность.

В трактовке многопоточных приложений - a sequence of instructions that are executed as a single, indivisible unit of work

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

Например i++ не атомарна. Это получение значения переменной + увеличение значения на 1 + запись значения в переменную - 3 операции, соседний поток может вклиниться между любой из них, что приведет к неконсистентности данных. Атомарные операции отличаются тем, что соседний поток вклиниться не может, они для того и нужны.

Технически - это конвейер, откуда можно считывать или помещать данные. То есть одна горутина может отправить данные в канал, а другая — считать помещенные в этот канал данные.

Ничего не понятно. Технически канал - примитив синхронизации, по сути - семафор.

Канал можно и закрыть.

Канал нужно закрывать. Просто за правило возьмите: как только записали в канал все, что хотели - закройте канал.

Для этого надо вызвать функцию close(Ваш канал), тем самым заблокировав доступ к чтению из него.

В смысле, заблокировав доступ к чтению??? Чтение из закрытого канала возвращает zero-value для типа канала. С чего оно заблокируется-то?

val, ok := <- Ваш канал, где val - переменная, в которую запишется значение с канала, если это возможно, а ok - булева переменная, где true означает, что из канала можно прочитать данные, а false - что он закрыт/невозможно считать.

Ну доку-то почитайте! false означает, что канал закрыт (и все значения из него прочитаны, в случае буферизованного канала). Никаких невозможно читать там нет, даже напротив, читать из него очень даже возможно.

С помощью for range можно читать данные из закрытого буферизированного канала, так как данные остаются в буфере даже после закрытия канала.

А без range'а нельзя? Данные в буфере пропадают? Что вы несете?

Если Вы будете считывать данные из пустого канала, Вы получите ошибку "Deadlock", которая появляется при бесконечном считывании из канала.

Дичь. Эталонная. Дедлок - это ситуация, когда все горутины находятся в спящем состоянии и не могут быть "разбужены", тчк.

select - это почти что switch, но без аргументов.

Гениальное определение. Неверное, ничего не объясняющее, но гениальное.

Вот такое будет ближе к истине:

Примитив синхронизации, блокирующий поток исполнения, пока один из case'ов не сможет быть выполнен.

Я долго писал эту статью, вытаскивая из каждого источника самое важно

Достаточно было одного источника, https://go.dev/doc/

вероятно, будет. Планировщик же.

В любом случае, меньше - абсолютно точно не будет. Насколько больше - мерить надо. Возможно, единицы процентов, а может быть, и десятки.

пользователи меняют процессоры в ноутбуках? А вы точно программист?

Если речь не идет о специфическом ПО, работающем только под Windows, Linux может выполнять все задачи, которые обычно нужны среднестатистическому человеку. Это офис, серфинг по сети, программирование (тут есть специфика, конечно) и все такое прочее.

У вас "(тут есть специфика, конечно)" отклеилась. Правильно вот так:

офис (тут есть специфика, конечно), серфинг по сети, программирование и все такое прочее.

Можно и я покритикую?

В заголовке - про извлечение данных из Linux, а в содержимом - gps, уровень wifi, уровень батареи и свободное место на диске.

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

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

Фигня там, а не e2e. end-to-end с MitM в комплекте

для интереса: чем вызвано "нехотение"? Отторжение cvs как таковых, или просто другая cvs'ка была "ближе к телу"?

доступ к которой выполняется через границу сети.

может, пограничные сети?

не бывает так. Любые отчисления идут последовательно. Если 30% от суммы продаж, то Valve будет брать свою долю от уменьшенной суммы, комиссия за перевод будет от суммы перевода и т.д. С российской стороны налог будет браться уже от того остатка, который пришёл на счёт. Дважды обложить отчислением одну и ту же сумму нельзя

Крутая штука. Интересно, когда до домашнего сегмента 3д печати доберется?

туго связанные

почесал промежность и понюхал руку

Машинный перевод, шуточки за 300 и отсутствие вменяемого ответа на вопрос из первого абзаца.

пошаговый план заработка 100к рублей на чат ботах и автоворонках начиная с нуля.

Срок обучения – 3 часа.

Цена – 990 рублей.

Вы серьезно? Это же просто подборка инфоцыганских курсов! Что это делает на хабре?

По идее, если отстраниться от различия между кооперативной и преемптивной асинхронностью (т.е. наличием/отсутствием явного await), tokio::spawn работает точно так же, как ключевое слово go

Не совсем. tokio::spawn - паттерн actor, go - csp.

Вот тут есть сравнение подходов: https://en.wikipedia.org/wiki/Communicating_sequential_processes

Прям раздел есть Actor vs CSP

Вкратце:

  • актор - управляемый, CSP - нет.

  • csp - обмен сообщениями эксклюзивно через каналы

  • actor позволяет отправку сообщения с отложенной обработкой, csp - блокирует отправку, пока читатель не готов к получению.

Из побочек: Rust, видимо, стектрейс ломать не дает. Т.е., фактически, мы не можем завершить выполнение вызывающего метода (как минимум стек вызова чистить нельзя), пока заспавненный не завершился + мы иерархию задач имеем. В Go - ипанул и забыл.

tokio::spawn работает точно так же, как ключевое слово go.

tokio:spawn сложнее, чем go . Горутины легче.

Неа. tokio::spawn создаёт task-у, которая может быть запущена в любом потоке из имеющегося пула.

Да в смысле "неа"?

У вас async внутри tokyo::spawn! Буквально M асинхронных задач внутри одного потока, M:1 внутри 1:N.

Асинхронные задачи, которые будут выполняться внутри этой таски, исполнятся ровно в том потоке, в котором таска запущена!

В Go - любая асинхронная задача может выполниться на произвольном потоке.

M:N - задачи, закинутые в spawn, распределяются по всем доступным ядрам, если не сказать прямо "работаем только в текущем потоке".

async при этом явно говорит "работаем в текущем потоке".

tokio::spawn(async {

Присмотритесь внимательно:

tokio::spawn - 1:N

async {} - M:1

Это не M:N, это, по сути, от thread::spawn отличается только наличием "легковесного потока".

То, что вы написали - это "запустить поток, внутри которого выполнить асинхронный код". Т.е. это будет N раз по M:1. Это не то же самое, что M:N.

Просто для ясности ситуации:

Допустим, вы спавните (tokio::spawn) 2 потока, должны дождаться выполнения обоих и с полученными данными дальше что-то делать. В первом вы асинхронно запускаете 10 задач, во втором - 2. Допустим, время выполнения асинхронной задачи константно, берем за единицу.

За сколько выполнится ваш код? Очевидно, за 10 (потому что 10 задач на одном потоке выполнялись, а второй выполнил 2 и благополучно сдох).

Теперь представим, что у нас 2 потока доступно гошному рантайму (на самом деле мне, как разработчику, откровенно насрать, сколько их там, я могу и имею право даже не знать). Мы запустили 10 горутин в одном блоке и 2 - в другом. Итого 12. Делим на 2 потока - сделали за 6.

Все, на этом "фишки" языка Go окончены. Он создан ровно ради этой фичи. Во всех остальных сценариях Rust круче, C# круче, Java кручке, все круче (кроме, простихоспади, PHP, Basic'а и "Языка программирования 1С" ).

Я не покажу, я в расте так себе. Но скорее всего, оно M:N, потому что токио - мультитредовый рантайм.

Да, токио - мультитредовый рантайм, и нет, оно не M:N. Оно 1:N.

async/await - M:1, tokyo - 1:N. M:N, кроме Go, из коробки, вроде, Haskell только (или Erlang, не помню, не большой специалист в хаскелях и эрлангах) и умеют. Собственно, весь язык ровно из плясок над "M:N с человеческим лицом" и состоит. Все, что мешает этой концепции - совершенно обоснованно идет лесом. Исключения, например, одна из вещей, которая мешает.

Как типизация влияет на 1:N/M:1/M:N? Разговор был о том, что в го так себе реализованы ошибки.

Типизация, в общем смысле, не мешает. А try/catch, ? и прочие примитивы обработки исключительных/ошибочных ситуаций - мешает. Я вот про это говорю.

И, не смотря на то, что вы считаете, что в других языках так же, то нет, не так же, а получше.

Дык, там и M:N нету. Другие языки решают другие проблемы.

В Go нет исключений и они вряд ли там к месту. Забейте уже на них)

Я бы уже давно забил, если бы вы мне в каждом первом комментарии не рассказывали, какие они классные в C#, и почему в Go они прямо необходимы.

Читаемость роняет мусорный бойлерплейт.

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

А растаманский подход с Result/Optional и т.д... ну, в виде дженериков - хоть сейчас пиши. Полноценная реализация на уровне языка - кортежи мешают (они слишком дофига свободы дают)

Еще раз приведу примеры из стандартной библиотеки, где это делается.

Еще раз говорю, это не проброс ошибки, а возврат значения.

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

И это не хорошо и не плохо, это просто цена 100-тысячных RPS.

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

В том и дело, что Go явным образом утверждает, что любой код может запускаться в горутине. А еще Go настаивает, что ошибка - это значение, а значит, может быть проигнорирована. В случае запуска в отдельной горутине, более того, будет проигнорирована.

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

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

  1. обработать те, с которыми мы знаем, что делать, остальные уйдут по стеку наверх (exception-style) с падением приложения при выходе за границы стека - неприменимо в Go, т.к. наличие "наверх" никто не гарантирует. Мы вырождаемся в обязанность никогда не возвращать ошибки, что выглядит как бред.

  2. гарантировать обработку всех ошибок (назовем это rust-style) - ну, какбэ, бойлерплейт по обработке рискует превратиться в портянку такого размера, что if err != nil {} покажется цветочками.

А потом поняли, что так жить грустно и начали фичи завозить.

Фичи завозят, стоит признать, аккуратно, дозированно, и только те, которые признаны удачными.

Богатый синтаксис - это ведь круто.

А вот тут - не всегда. Смотрел списки фич в Python'е, чейнджлоги того же PHP... А работать когда?

Ошибка возникает в parse_string_to_smth. Что должен делать parse_file? На месте обработать? Кинуть панику или вернуть что-то пустое? 

Если знает, что с ошибкой делать - обработать на месте. Если не знает - обернуть своей ошибкой и вернуть. Панику - а зачем? Ну, если нужна паника - кинуть панику. В чем проблема-то?

Так это почти всегда (если нормально делать) не зона ответственности этой функции.

Ну, я и говорю:
если не зона ответственности

  • if err != nil { return nil, err }

  • if err != nil { return nil, fmt.Errorf("%w: some package error", err) }

  • if err != nil { return nil, errors.New("error parsing string") }

если зона ответственности: if err != nil { // do something }

И в одном случае там может быть просто варнинг, потому что данные из файла вспомогательные и у нас есть какое-то дефолтное значение. А во втором случае надо паниковать, потому что данные из файла необходимы.

А вот тут вы, внезапно, не очень умную вещь сказали.

"потому что данные из файла" - тут, как минимум, "из файла" - лишнее.

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

А дальше, собственно, от требований пляшем.

Если, допустим, вызывающей функции надо четко отличать ситуацию "все пропало" от "говно пришло какое-то (например, не парсится, или еще какая беда)" - ну так у вас функция, которую мы вызываем, должна вернуть соответствующую информацию. Если не возвращает - функция не подходит, ищем/пишем другую.

var ErrShitHappened = errors.New("shit happens")
var ErrWeCanIgnore = errors.New("ignore this")

func readData(someParamsHere...) (any, err) {
  if err := openResource(); err != nil {
    return nil, fmt.Errorf("%w: can't open resourse %w", ErrShitHappens, err)
  }
  if err := parser.ParseResource(); err != nil {
    if errors.Is(err, parser.ErrCritical) {
      return nil, fmt.Errorf("%w: parser failed with %w", ErrShitHappens, err)
    }
    return nil, ErrWeCanIgnore
  }
  if len(result.Rows) == 0 {
    return nil, ErrWeCanIgnore
  }
  .....
  return value, nil
}

func business() {
  data, err := readData(someParams)
  if err != nil && errors.Is(ErrShitHappened) {
    panic(err)
  }
    ...
}

Так что обычно как-раз пробрасывают ошибку от "утилитарной" до какой-то более "сведущей" функции.

Ну вы же понимаете, что идея о том, что где-то наверху есть функция, более "сведущая" в том, что делать с конкретной ошибкой, которую возвращает какой-то парсер из-за ошибки в какой-то строке какого-то файла, находящегося за 4-мя слоями абстракции от этой "сведущей функции"... Ну, мягко говоря, утопичная идея. Вам не кажется?

Я хз, может в го не так, там свой way, надо всегда паниковать или nil вместо значений возвращать

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

, но почему тогда я в го-коде вижу if err != nil { return err } ?

Потому что таков путь, и в 99% случаев этого достаточно. Можно бы было забить, но все еще есть 1%, где этой конструкции не хватает. Ну да, такова жизнь, более удачных решений не наблюдается (и не надо опять поднимать песню про исключения. Они а) сами далеки от идеала, б) полноценно нереализуемы в парадигме Go (а эрзац - нафиг не нужен).

1
23 ...

Information

Rating
2,400-th
Location
Бийск, Алтайский край, Россия
Date of birth
Registered
Activity