Тестуємо віртуальні потоки в Java 21

💡 Усі статті, обговорення, новини про тестування — в одному місці. Приєднуйтесь до QA спільноти!

Усім привіт. Я Сергій Моренець, розробник, викладач, спікер і технічний письменник, хочу поділитися з вами досвідом свого тестування та використання в Java такої крутої фічі, як віртуальні потоки (проєкт Loom). П’ять років розробки, два роки публічного тестування — і тепер ви можете використовувати їх у своїх проєктах. Але які переваги та недоліки такого переходу?

У цій статті я взяв три популярні вебфреймворки (Spring MVC, Spring WebFlux, Micronaut) і протестував їхню продуктивність з і без віртуальних потоків. Також я перевірив їхню швидкодію в проєктах, упакованих у GraalVM Native images. Для тестування було обрано досить інноваційний підхід — Jmeter без UI на базі спеціального DSL.

Сподіваюся, що ця стаття буде корисною для всіх, хто хоче дізнатися на практиці, як провести таку міграцію і яка від цього практична користь.

Підготовка до тестування

У поточній версії є фіча, яку ніяк не можна залишити без уваги і вивчення — віртуальні потоки. Я вже кілька разів розповідав про цю функціональність. Якщо говорити коротко, то вони дозволяють писати простіший код у багатозадачних проєктах, не вдаючись до реактивного API. Важливо розуміти, що це не інфраструктурні зміни (як, наприклад, збирачі сміття), а зміна API. Тобто необхідно конкретно вказати JVM, що ви хочете використовувати їх. Водночас зараз у проєктах основні потоки, що працюють (або пули потоків) — це не ті, що розробник створює сам, а ті, що створюються готовими бібліотеками, фреймворками або вебсерверами. Використання віртуальних потоків може дозволити досягти деякого приросту в продуктивності, хоча це і не є їхнім головним завданням.

Чи буде такий приріст чи ні — відповідь на це питання може дати лише performance testing. Але перш ніж перейти до тестування, потрібно зрозуміти, а чи підтримують віртуальні потоки технології, що існують? На щастя, Micronaut 4, який вийшов улітку, вже заявив про таку підтримку. Це цікаво, тому що їхній REST API спочатку був побудований на реактивному підході. Про підтримку віртуальних потоків у Jersey (JAX-RS) нічого не відомо, а ось два мікрофрейморки Quarkus і Helidon уже заявили про їхню підтримку. Spring також може використовувати нову функціональність, правда у версії 3.2, яка ще не вийшла на ринок, але вже є RC-версії, які можна буде протестувати.

Для тестування можна вибрати будь-яку зі знайомих технологій — K6, JMeter, Gatling чи Wrk. Але цікавіше познайомитися з новим підходом у роботі з JMeter. Раніше ми створювали тестові сценарії в GUI, а потім за допомогою командного рядка запускали тести. Але в 2020 році компанія Abstracta, яка займається тестуванням ПЗ, створила новий проєкт — Jmeter-java-dsl.

Мета проєкту — писати performance-тести без необхідності використовувати GUI, створювати скрипти-конфігурації на XML, які не так просто підтримувати. Розробники й раніше могли використовувати API від JMeter, але це було досить низькорівневе програмування, яке вимагало тривалого вивчення цього API. JMeter-java-dsl пропонує високорівневий DSL, що дозволяє писати прості функціональні тести. Крім того, ви можете:

  • конвертувати JMX-файли, що існують, у тестовий Java код;
  • зберігати тестовий код як JMX-файл;
  • налагоджувати тести (що раніше було неможливо);
  • використовувати DSL Recorder;
  • писати розподілені тести (за допомогою JMeter, BlazeMeter чи Azure Load Testing).

Зараз підтримуються лише деякі connectors — HTTP, GraphQL, JDBC, але для нас цього достатньо. Для тестування оберемо два REST-сервіси:

  1. Повертає перелік статичних даних.
  2. Повертає дані із СУБД (причому ми будемо використовувати драйвер, що блокує, для всіх платформ).

Додамо JMeter-java-dsl-залежність для Maven-конфігурації:

              <dependency>
                     <groupId>us.abstracta.jmeter</groupId>
                     <artifactId>jmeter-java-dsl</artifactId>
                     <version>1.23</version>
                     <scope>test</scope>
              </dependency>

Почнемо з Micronaut. Для тестування виберемо PaymentProviderController, який бере дані з MonoDB. Для тестування додамо метод getStaticStrings:

@Controller("/providers")
public class PaymentProviderController {
 
       @Inject
       private PaymentProviderRepository paymentProviderRepository;
 
       @Inject
       private Transformer transformer;
 
       @Get("/static")
       public List<PaymentProviderDTO> getStaticList() {
              return List.of(new PaymentProviderDTO("1", "STRIPE", "default"), new PaymentProviderDTO("2", "PAYPAL", "config"));
       }
 
       @Get
       public List<PaymentProviderDTO> findAll() {
              return paymentProviderRepository.findAll().stream()
              .map(payment -> transformer.transform(payment, PaymentProviderDTO.class)).collect(Collectors.toList());
       }

Тепер перейдемо до тесту. Створимо клас PaymentProviderControllerPerformanceTest:

public class PaymentProviderControllerPerformanceTest {
 
       @ParameterizedTest
       @ValueSource(ints = { 5, 100, 500, 2500 })
       void getStaticList_success(int threadCount) throws IOException {
              TestPlanStats stats = testPlan(
                           threadGroup(threadCount, 10, httpSampler("http://localhost:8030/providers/static")),
                           jtlWriter("target/jtls").logOnly(SampleStatus.ERROR)).run();
              double errorPercentage = (double) stats.overall().errorsCount() / stats.overall().samplesCount();
              assertAll(() -> assertTrue(stats.overall().sampleTime().mean().compareTo(Duration.ofMillis(1000)) < 0),
                           () -> assertTrue(errorPercentage <= 0.1),
                           () -> assertTrue(stats.overall().sampleTime().perc90().compareTo(Duration.ofSeconds(1)) < 2));
       }

Код такого тесту досить простий і зрозумілий, особливо якщо ви вже знайомі з JMeter:

  1. Ми використовуємо десять ітерацій.
  2. Кількість потоків (віртуальних користувачів) змінюється від пʼяти до 2500.
  3. Усі запити записуються до папки target/jtls. Але оскільки запитів багато, і це гальмуватиме тест, ми вказали варіант запису тільки тих запитів, які повернули помилку.
  4. Після виконання тесту ми перевіряємо результати (latency, відсоток помилок і так званий 90% Line).

Такий тест разюче відрізняється від того, що ми писали раніше. Насамперед він перевіряє не функціональність, а performance, і тому його немає сенсу запускати після кожної найменшої зміни. Зараз він запускається поряд з усіма, а працює досить довго, тому гальмуватиме збірку. Потрібно перевести його з категорії дефолтних до опціональної групи. Для цього додамо тег Performance:

@Tag("performance")
public class PaymentProviderControllerPerformanceTest {

Тепер налаштуємо Maven так, щоб цей тест запускався тільки за певного параметра. На щастя, в Maven Surfire плагіні є інтеграція з Junit 5, тому додамо його до pom.xml:

       <plugin>
              <groupId>org.apache.maven.plugins</groupId>
              <artifactId>maven-surefire-plugin</artifactId>
              <configuration>
                     <groups>!${tags}</groups>
                     <excludedGroups>${tags}</excludedGroups>
              </configuration>
       </plugin>
 
       <properties>
              <tags>performance</tags>
       </properties>

Виникає питання, навіщо потрібно вказувати обидва атрибути (groups і exludedGroups)? Справа в тому, що якщо написати так:

       <configuration>
              <excludedGroups>${tags}</excludedGroups>
       </configuration>

То Maven Surfire-плагін взагалі не запустить тестів. Тут немає такої конвенції, що тести без тегів завжди запускаються. Тепер можна запустити звичайні тести:

mvn</i><i> test

або performance-тести, перевизначивши властивість tags:

mvn clean test -Dtags="!performance"

Цікаво, що в документації з плагіна вказується, що можна перевизначити атрибути groups і excludedGroups, але такий варіант не спрацював:

mvn test -DexcludeGroups="!performance" -Dgroups=performance

Запускаємо випробування. Це результати для статичного списку рядків:



Threads


Time(avg), ms


Errors(%)


Req/sec


5


6


0


259


100


33


0


1628


500


545


4.7


452


2500


1287


11.8


770

Потім результати для динамічного списку, який береться з БД:



Threads


Time(avg), ms


Errors(%)


Req/sec


5


8


0


234


100


47


0


1283


500


663


5.8


438


2500


871


6.4


1109

Micronaut оголосив про підтримку віртуальних потоків ще в 2022 році, як тільки вийшла JDK 19. Спочатку в ньому використовувався стандартний I/O-пул потоків від Netty, але якщо помістити анотацію @ExecuteOn(BLOCKING) на REST-сервіс, то використовуватиме інша концепція — один (віртуальний) потік на кожне завдання:

       @Get("/static")
       @ExecuteOn(TaskExecutors.BLOCKING)
       public List<PaymentProviderDTO> getStaticList() {

Внутрішньо для цього використовується метод newThreadPerTaskExecutor у класі Executors:

    public static ExecutorService newThreadPerTaskExecutor(ThreadFactory threadFactory) {
        return ThreadPerTaskExecutor.create(threadFactory);
    }

Знову запустимо тести для статичного списку:



Threads


Time(avg), ms


Errors(%)


Req/sec


5


7


0


213


100


38


0


1524


500


148


0


2321


2500


751


3.24


1778

Потім результати для динамічного списку, який береться з БД:



Threads


Time(avg), ms


Errors(%)


Req/sec


5


14


0


177


100


60


0


1101


500


671


5.82


436


2500


835


5.71


1820

Під час порівняння результатів можна побачити таке:

  1. Для невеликої кількості потоків (5-100) система з віртуальними потоками працює на 15-20% повільніше, можливо через накладні витрати.
  2. Для великої (500+) кількості потоків перший REST-сервіс показав видатні результати з віртуальними потоками, обігнавши дефолтний варіант у 2-2,5 рази. Сервіс із динамічним списком показав приблизно ідентичні результати для 500 потоків і трохи краще для 2500 потоків. Якщо час відповіді приблизно такий самий, то кількість запитів на секунду в півтора рази вища.

Використовуємо GraalVM Native image

Ще одна приємна новина — GraalVM, починаючи з жовтня 2022 року, підтримує віртуальні потоки. Цікаво перевірити, як вони працюють у native images, тим паче Micronaut із самого початку конструювався як GraalVM-friendly. Спочатку зберемо Native image з використанням звичайних потоків:

docker compose -f docker-compose.yml -f docker-compose-graalvm.yml build payment

Слід зазначити, що ми будемо використовувати GraalVM 22.3.3 на базі Oracle Linux 9 та JDK 21:

FROM ghcr.io/graalvm/native-image-community:21 as graalvm

Знову запустимо тести для статичного списку:



Threads


Time(avg), ms


Errors(%)


Req/sec


5


5


0


238


100


29


0


1763


500


112


0


2839


2500


844


8.84


1134

Тепер спробуємо запустити цей код як native image, але на віртуальних потоках. Знову запустимо тести для статичного списку:



Threads


Time(avg), ms


Errors(%)


Req/sec


5


9


0


226


100


29


0


1798


500


552


4.9


452


2500


1054


8.7


1103

А ось для динамічного списку протестувати не вийшло через досить дивну помилку в Mongo-драйвері:

org.bson.codecs.configuration.CodecConfigurationException: Can’t find a codec for CodecCacheKey{clazz=class model.PaymentProvider, types=null}.

at org.bson.internal.ProvidersCodecRegistry.lambda$get$0(ProvidersCodecRegistry.java:87)

at [email protected]/java.util.Optional.orElseGet(Optional.java:364)

at org.bson.internal.ProvidersCodecRegistry.get(ProvidersCodecRegistry.java:80)

at org.bson.internal.ProvidersCodecRegistry.get(ProvidersCodecRegistry.java:50)

at com.mongodb.internal.operation.Operations.createFindOperation(Operations.java:186)

at com.mongodb.internal.operation.Operations.find(Operations.java:176)

at com.mongodb.internal.operation.SyncOperations.find(SyncOperations.java:98)

at com.mongodb.client.internal.FindIterableImpl.asReadOperation(FindIterableImpl.java:246)

at com.mongodb.client.internal.FindIterableImpl.asReadOperation(FindIterableImpl.java:41)

at com.mongodb.client.internal.MongoIterableImpl.execute(MongoIterableImpl.java:130)

at com.mongodb.client.internal.MongoIterableImpl.iterator(MongoIterableImpl.java:90)

at com.mongodb.client.internal.MongoIterableImpl.forEach(MongoIterableImpl.java:116)

at com.mongodb.client.internal.MongoIterableImpl.into(MongoIterableImpl.java:125)

at persistence.repository.MongoSyncPaymentProviderRepository.findAll(MongoSyncPaymentProviderRepository.java:65)

at web.PaymentProviderController.findAll(PaymentProviderController.java:49)

Я додав тикет на цю помилку і відповідь від розробників прийшла досить швидко. Оскільки у нашому випадку йде спроба виклику Reflection API, а всі такі спроби повинні бути суворо задокументовані, потрібно лише додати анотацію @ReflectiveAccess на PaymentProvider:

public class PaymentProvider extends BaseEntity {

Перезбираємо застосунок, запускаємо та отримуємо ще одну помилку:

Caused by: java.lang.NoSuchMethodException: dto.PaymentProviderDTO.<init>()
                at [email protected]/java.lang.Class.checkMethod(DynamicHub.java:1065)
                at [email protected]/java.lang.Class.getConstructor0(DynamicHub.java:1228)
                at [email protected]/java.lang.Class.getDeclaredConstructor(DynamicHub.java:2930)
                at common.infra.util.ReflectionUtil.createInstance(ReflectionUtil.java:58)

Ця проблема також пов’язана з попередньою, але ми вже не можемо поставити анотацію @ReflectiveAccess, оскільки PaymentProviderDTO розташований в іншому модулі. Тому використовуємо іншу інструкцію — @ReflectionConfig:

@ReflectionConfig(type = PaymentProviderDTO.class, accessType = { ALL_DECLARED_CONSTRUCTORS,
              AccessType.ALL_DECLARED_FIELDS })
public class GraalVMConfiguration {
}

Запускаємо тест для динамічного списку:



Threads


Time(avg), ms


Errors(%)


Req/sec


5


7


0


236


100


34


0


1587


500


135


0


2516


2500


921


6.4


1078

І той самий REST-сервіс, але на віртуальних потоках:



Threads


Time(avg), ms


Errors(%)


Req/sec


5


8


0


227


100


33


0


1631


500


593


5.2


446


2500


1335


11.8


1051

Якщо порівняти результати, то віртуальні потоки не додали швидкодії, або погіршили її. Особливо це виявилося на 500 потоках — падіння продуктивності в пʼять (!) разів. Важко сказати, з чим це пов’язано. Можливо, з тим, що код native image і так оптимізований, а може підтримка віртуальних потоків у GraalVM зроблена недостатньо добре або ще не закінчена.

Spring MVC

Тепер перейдемо до Spring та Spring Boot. Тут уже давно ведеться робота над підтримкою віртуальних потоків, яка буде остаточно завершена до виходу Spring Boot 3.2. Поки що ця версія не вийшла, але вже є 3.2 RC1, на якій ми спробуємо запустити performance-тести.

Як здійснюється ця підтримка? Насамперед за рахунок глобального власності spring.threads.virtual.enabled. Якщо його встановити в true, то починається справжня магія:

  1. Tomcat використовує пул віртуальних потоків замість звичайних. Це можливо, тільки якщо у вас є Tomcat 10.1.x або вище. Аналогічна історія з Jetty.
  2. Запуск асинхронних методів здійснюється через TaskExecutor на базі віртуальних потоків. Це може призвести до незвичайних side effects. Якщо у вас є клас з анотацією @Scheduled, то він запускав non-deamon-потік для своєї роботи. Тепер буде запускатися deamon-потік, а це означає, що програма може завершитися, тільки-но розпочавшись. Тому в 3.2 додали нову властивість spring.main.keep-alive, єдине завдання якої — просто запускати новий non-daemon-потік.
  3. Деякі обробники повідомлень у messaging systems (Pulsar, RabbitMQ, Kafka) також використовують віртуальні потоки.
  4. Java клієнти для Redis використовують віртуальні потоки

Якщо вам необхідно якийсь зі своїх бінів вмикати / вимикати залежно від цієї властивості, то для цього є нова зручна анотація @<b>ConditionalOnThreading</b>:

 @Bean
@ConditionalOnThreading(Threading.VIRTUAL)
TomcatVirtualThreadsWebServerFactoryCustomizer tomcatVirtualThreadsProtocolHandlerCustomizer() {

А наш REST-контролер виглядає так:

@RestController
@RequestMapping("providers")
@RequiredArgsConstructor
public class PaymentProviderController {
    private final PaymentProviderRepository paymentProviderRepository;
    @GetMapping("static")
    public List<PaymentProviderDTO> getStaticList() {
        return List.of(new PaymentProviderDTO("1", "STRIPE", "default"), new PaymentProviderDTO("2", "PAYPAL", "config"));
    }
    @GetMapping
    public List<PaymentProviderDTO> findAll() {
        return paymentProviderRepository.findAll().stream().map(provider -> new PaymentProviderDTO(provider.getId().toHexString(),
                provider.getProviderType().name(), provider.getName())).toList();
    }

Запускаємо тест для статичного списку:



Threads


Time(avg), ms


Errors(%)


Req/sec


5


7


0


217


100


42


0


1400


500


163


0


2153


2500


700


3.25


1875

І для динамічного списку:



Threads


Time(avg), ms


Errors(%)


Req/sec


5


8


0


228


100


51


0


1191


500


514


4.3


445


2500


1086


8.6


1086

Тепер увімкнемо підтримку віртуальних потоків у application.properties:

 spring.threads.virtual.enabled=true

Запускаємо додаток і одразу одержуємо помилку:

Caused by: java.lang.ClassNotFoundException: org.springframework.boot.SpringApplication$AbandonedRunException
    at java.base/java.net.URLClassLoader.findClass(Unknown Source)
    at java.base/java.lang.ClassLoader.loadClass(Unknown Source)
    at org.springframework.boot.loader.net.protocol.jar.JarUrlClassLoader.loadClass(JarUrlClassLoader.java:104)
    at org.springframework.boot.loader.launch.LaunchedClassLoader.loadClass(LaunchedClassLoader.java:91)
    at java.base/java.lang.ClassLoader.loadClass(Unknown Source)

Я створив тикет на цю проблему, виявилося, що це відома помилка в Spring Boot, і потрібно обновитись і перейти з 3.2.0 RC1 на SNAPSHOT-версію (майбутній RC2).

Запускаємо тест для статичного списку:



Threads


Time(avg), ms


Errors(%)


Req/sec


5


7


0


218


100


38


0


1000


500


142


0


2276


2500


946


6.2


1078

І для динамічного списку:



Threads


Time(avg), ms


Errors(%)


Req/sec


5


8


0


239


100


50


0


1240


500


394


2.9


453


2500


1308


11.6


1061

Якщо порівняти результати, то для статичного списку перехід на віртуальні потоки не дав жодних переваг. У деяких прикладах було зниження продуктивності на 20-50%. Для динамічного списку ситуація краща. Віртуальні потоки показали приблизно рівну швидкодію, десь навіть на 10-15% краще.

Spring WebFlux

Тепер спробуємо той самий тест, але для Spring Web Flux. Інженери Spring спеціально розробили цей реактивний вебфреймворк, щоб разом з реактивними драйверами баз даних він дозволив повністю позбутися викликів, що блокують, у багатопотокових застосунках.

@GetMapping("static")
public Flux<PaymentProviderDTO> getStaticList() {
    return Flux.just(new PaymentProviderDTO("1", "STRIPE", "default"), new PaymentProviderDTO("2", "PAYPAL", "config"));
}

Запускаємо тест для статичного списку:



Threads


Time(avg), ms


Errors(%)


Req/sec


5


7


0


240


100


41


0


1335


500


338


2.3


460


2500


1232


10.3


756

І для динамічного списку:



Threads


Time(avg), ms


Errors(%)


Req/sec


5


11


0


205


100


79


0


918


500


500


3.5


442


2500


996


7.2


1096

Якщо порівняти результати, то для статичного списку Spring MVC (з віртуальними потоками) обійшов Spring Web Flux на 20-50%. У динамічному списку ситуація залежить від кількості одночасних потоків. Якщо їх 500 і менше, то Spring MVC показав найкращі результати, для 2500 краще показав себе Spring Web Flux.

Spring Boot підтримує упаковку в GraalVM native image двома способами:

  1. За допомогою Cloud Native buildpacks (інтегрованих у Spring Boot Maven-плагін).
  2. За допомогою GraalVM Native image плагіна.

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

Висновки

Micronaut і Spring — не єдині технології, які вже підтримують віртуальні потоки. У Quarkus це досягається за допомогою анотації @RunOnVirtualThread. Helidon запустив окремий проеєкт Helidon Nima, збудований на віртуальних потоках.

Якщо підбити підсумки, можна відзначити, що Java-індустрія позитивно сприйняла таку фічу, як віртуальні потоки. Практично всі вебфреймворки та вебсервери до виходу JDK 21 додали необхідну підтримку, щоправда, це було зроблено різними способами. Якщо Spring Boot для цього є одна глобальна властивість, то в Micronaut / Quarkus ви також можете вказати, що REST-сервіс (або група REST-сервісів) будуть виконуватися всередині віртуальних потоків.

Якщо проаналізувати результати performance-тестів, то в цілому системи, де використовувалися віртуальні потоки, не поступилися так званим реактивним неблокуючим технологіям (Spring Web Flux, Micronaut).

Результати відрізнялися для різних систем і одночасного навантаження. Тому якщо вам хочеться протестувати віртуальні потоки для вашого проєкту, то потрібно також моніторити системні метрики (кількість одночасних потоків, завантаження CPU, обсяг пам’яті, що використовується) і вибирати відповідний сценарій використання. Єдиний виняток — GraalVM native image, де віртуальні потоки в Micronaut показали себе набагато гірше.

Чи варто зараз переходити на них? Це більш актуально для проєктів із синхронним блокуючим потоком (наприклад, Spring MVC), де такий перехід здійснюється додаванням однієї властивості. Якщо ж у вас проєкт зав’язаний на реактивні технології, то тут потрібно зважити всі за і проти, оскільки міграція може бути витратною за часом і ресурсами.

👍ПодобаєтьсяСподобалось4
До обраногоВ обраному1
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

+, що тести не релавентні

contributors.scala-lang.org/...​n-functional-effects/5821
Непогане самарі. Якщо коротко — то vrtual threads — це тільки одне з айтемів заради яких юзають ZIO або CE. Напишуть підтримку virtual threads у рантаймах систем ефектів — все інше залишиться як було, код не треба буде переписивути. Все буде так само, але трохи швидше.

П’ять років розробки, два роки публічного тестування — і тепер ви можете використовувати їх у своїх проєктах.

Я правильно всё понимаю, что у разрабов джавы ушло 5 лет разработки и 2 года тестирования, чтобы наконец выпустить в подарочной упаковке с красивым бантиком базовый функционал Go?

go func() {
    fmt.Println("hello world!")
}()

Ви розумієте так як вам зручно і читаєте із тією інтонацією якою вам звично. По-перше, в Golang нема жодного іншого стеку для concurrent програмування, окрім випадків, коли вам дуже треба pthread_create, а бо пули з C++ Boost ASIO.

Може давайте ви ще накинете Python-у, чого ж це в них со-програми зʼявилися лише декілька років тому? Хочете поговорити про Golang? Давайте, горутіни зʼявилися майже одразу, але їх відладка в продакшені все ще продовжується, вже як 10й рік. Так що не порівнюйте час, розмір екосистеми та готовність комʼюніті до іновацій. Це Java, тут є все ще ті, хто сидить на JDK 8, а зараз актуальна JDK 21.

Я, як людина дотична до розробки та тестування нових проектів в Java/JDK, можу сказати, що пройде ще не один рік, а віртуальні потоки не будуть впроваджені в більшості фреймворків екосистеми (Spring, Netty, Apache Common). Тому 5 років розробки і 2 роки тестування це ніщо у порівнянні із якістю функціоналу та нових пропускних потужностей на які спроможна Java.

По-перше, в Golang нема жодного іншого стеку для concurrent програмування, окрім випадків, коли вам дуже треба pthread_create, а бо пули з C++ Boost ASIO.
/*
LockOSThread wires the calling goroutine to its current operating system thread. The calling goroutine will always execute in that thread, and no other goroutine will execute in it, until the calling goroutine has made as many calls to UnlockOSThread as to LockOSThread. If the calling goroutine exits without unlocking the thread, the thread will be terminated.
*/
go func() {
    runtime.LockOSThread()
    defer runtime.UnlockOSThread()
    fmt.Println("hello world!")
}()
Вот и всё, достаточно просто залочить горутину на OS-треде. И OS-тред будет выделен только на 1 горутину.

що саме ви цим прикладом хотіли показати? що маючи флот (fleet) ОС потоків ви можете контролювати кількість задач на один поток? чи ви хотіли сказати, що таким чином можете вичерпати GOMAXPROC, і змусити рантайм створювати нові ОС потоки?

В свою ж чергу я писав про те, що в Java ви можете створювати пули ОС тредів незалежно від рантайму, а із впровадженням віртуальних потоків зʼявилася можливість ще й вказувати фабрику потоків. В Golang ви не можете створювати системні потоки напряму, тобто ваш код буде виконуватися однаково навіть якщо в вас там математика чи мульти-блокуючі I/O. В Java в вас є вибір що й як робити, в golang — нема, є лише ось такі способи «pinning» горутин до ОС потоків.

В свою ж чергу я писав про те, що в Java ви можете створювати пули ОС тредів незалежно від рантайму, а із впровадженням віртуальних потоків зʼявилася можливість ще й вказувати фабрику потоків

Этой задачей занимается шедуллер Go, а программисту создавать пул потоков не нужно. Этот шедуллер переключает выполнение горутины (виртуальные потоки / коротины / сопрограммы) на разлоченный блокирующей операцией тред. Как показал выше, тред можно в целом залочить за отдельной горутиной. В общем, пул потоков управляется со стороны рантайма Go, а не вручную, что более логичный подход. Как и управление памяти через GC, а не вручную через вызовы malloc/free. То есть джава-девелоперы до этого ещё просто не дошли.

Ещё второй важный аспект в релизе средств параллельного программирования — это обмен данными между виртуальными потоками. Понятно конечно, что везде есть мьютексы и атомарные операции для синхронизации. Но вот в реализации коротин обычно есть какой-ниубудь yield(a, b, c) для передачи данных из коротины в управляющий поток, и resume(d, e, f) для возобновления коротины. Естественно это работает с доступом к контексту управления. А в реализации горотин на Go обмен происходит через каналы. Если нет ни того, ни другого способа, то это недоделанная версия виртуальных потоков.

Я перепрошую, ви просто накидуєте на вєнтілятор? Ви взагалі Java знаєте?

В общем, пул потоков управляется со стороны рантайма Go, а не вручную, что более логичный подход. Как и управление памяти через GC, а не вручную через вызовы malloc/free. То есть джава-девелоперы до этого ещё просто не дошли.

ВСЕ що ви написали теж робить середовище виконання. Після таких тез від вас я просто не впевнений, що із вам реально є сенс дискутувати.

Ещё второй важный аспект в релизе средств параллельного программирования — это обмен данными между виртуальными потоками. Понятно конечно, что везде есть мьютексы и атомарные операции для синхронизации. Но вот в реализации коротин обычно есть какой-ниубудь yield(a, b, c) для передачи данных из коротины в управляющий поток, и resume(d, e, f) для возобновления коротины. Естественно это работает с доступом к контексту управления. А в реализации горотин на Go обмен происходит через каналы. Если нет ни того, ни другого способа, то это недоделанная версия виртуальных потоков.

Я був впевнений, що ви розумієтеся на тому, що пишете про Java. Відповім коротко: все є, окрім знання Java у вас. Дуже сумно.

Нет, на джаве я ничего не пишу. Ну естественно там есть GC, и это было ключевой фичей лет 25 назад. Если там есть и шедуллер ОС-тредов, тогда вообще не понятно, что тут обсуждать, и почему речь о виртуальных потоках только сейчас зашла. Самая лучшая реализация коротин — это в Lua, также ещё хороший релиз в js. В C++ плохой релиз, и это на самом деле фикция, поскольку в C++ нет передачи контекста исполнения. В джаве его тоже нет, поэтому насчёт реализации коротин там могут возникнуть вопросы. Вопросы могут даже возникнуть по мьютексам: а что они вообще там лочат? Если в виртуальных потоках мьютексы лочат ос-тред, а не только один виртуальный поток, то такие виртуальные потоки можно выкидывать. Ну ок, я не джава-программист, пусть они смотрят всё это.

Ти реально прийшов на вентилятор накинути. То нічого що джаві 100500 років і будь які зміни не робляться так просто?

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

Ні, ти вирішив накинути на вентилятор типу яка джава атстойна, а Го форева. Пиши собі на Го, ніхто ж не забороняє

Нет, на джаве я ничего не пишу.

Так а хулі ти сюди себе приніс, якщо ти дупля не відбиваєш в темі?

Сначала шла дискуссия, какой вариант выбрать, напомню, что 1-м вариантом были «контракты». Когда нашёлся подходящий вариант, то в ближайшей версии и зарелизили. Новые версии раз в полгода выпускают.

Сначала шла дискуссия, какой вариант выбрать

10 років дискутували за дженеріки. Спочатку взагалі казали що женерік ні нужон, крутіть свої костилі до go:generate.

у разрабов джавы ушло 5 лет разработки и 2 года тестирования

Так і у жабістів йшла діскусія. Всього 5 років базарили.

Первое обсуждение начато в 19 году, вот пруф: go.googlesource.com/...​ign/go2draft-contracts.md
Таким образом, обсуждения шли 3 года. Релиз 1.18 с дженериками вышел в марте 22 года, до этого за пол года бета-версия. То есть, с учётом беты 2,5. При этом контракты от финального дизайна отличаются значительно, это не косметические изменения.

Ваш пруф не пруф. Там «Contracts — Draft Design», а не обговорення. Обгорювати дженеріки в go почали набагато раніше. Ось вам пруф «Summary of Go Generics Discussions» docs.google.com/...​X4pi-HnNjkMEgyAHX4N4/edit
Дата створення документа Dec 16, 2014.

То беспредметное обсуждение. Так можно достать какой-нибудь рассказ фантаста, и сказать — вот смотрите, это ещё в 1861 году обсуждать начали. Вон драфт есть — и есть что обсуждать для принятия.

Обговорення залишається обговоренням не залежно від того чи вважаєте ви його предметним чи безпредметним. Ви просто вигадуєте відмазки.

Ви не отримали жодних позитивних результатів тому що ви принципово не зрозуміли для чого були створені віртуальні потоки.

Відповідно, ви написали абсолютно некоректні, непотрібні тест-кейси, які не релевантні проблемі, яку вирішують віртуальні потоки.

Ваші тести не тестують пропускну здатність застосунку, коли всі потоки запитів виконують довготривалі блокуючі задачі. Адже саме це є метою віртуальних потоків, а не дрочіння через жметр «статичного списку» яке ви зробили, і яке відпрацьовує за нотайм.

Вітаю, ви наробили беззмістовної роботи, зате написали ще одну статтю.

Як з язика зняли. 100% надані тести просто не мають жодного сенсу. Ба більше того, інструменти для тестування взагалі взяті зі стелі. Нащо брати jmeter, коли є JFR (+ JMC) і JMH.

Щоб якісно тестувати віртуальні потоки в вас повинні бути відповідні задачі для цього. А в цьому тесті/контексті просто нема, на жаль.

Підписатись на коментарі