Concurrent programming by language

· Tech· CS
CS

View full PPT presentation

들어가며

I chose concurrent programming by language as the theme for this 2024 Hello World presentation! It was March 30th, so a month has already passed. It was a really difficult topic, but I chose it as my presentation topic because I thought it was a topic that I needed, that other juniors like me needed, and that it was worth thinking about at least once! I received help from many people while preparing the presentation, and I think it was a good opportunity for me to think deeply. And while organizing the presentation, I decided to organize some of the contents on the blog for review.

First, I summarized the words that come up when talking about concurrent programming.

Concurrency is a parallel concept, and although it may seem like things are actually being processed at once, it means that multiple tasks are taking place, such as context switching on a single core. On the other hand, parallelism means that tasks are physically separated and executed, and parallelism is possible with structures such as multi-core and multi-processor. In concurrent programming, concurrency and parallelism are complementary concepts and are used together to increase performance and efficiency. Multiprocess and multithreading literally refer to the concept of multiple processes and multiple threads. Multi-processing has the disadvantage of high resource usage because processes require memory allocation. Conversely, multithreading allows multiple threads to perform fast tasks within one process. The disadvantage is that synchronization problems may occur because it operates within one process. Process flow control methods include synchronous and asynchronous. Synchronous means executing other tasks sequentially when all tasks are completed, while asynchronous means executes other tasks even if they are blocking tasks and reduces dependency on other tasks.

Concurrent Programming with Java

Java implements the Runnable interface, and the overridden run method implements what will be executed in the thread. Synchronization of methods or code blocks is supported by synchronized, which allows parts such as the decrement operator (e.g. availableTickets--;) to be executed as a single atomic operation.

Virtual Thread

Starting with preview versions 19 and 21, Java provides Virtual Thread, which is intended to improve the JVM thread concept of the existing 1:1 mapping, perhaps providing a lightweight thread concept applied in other languages. By introducing a lightweight user-mode thread concept, it connects to the carrier thread rather than directly to the OS thread, lowering context switching costs and blocking time. Virtual Threads can be created using the Executor and Thread class builder concepts. When using synchronized or native methods, performance may deteriorate due to being pinned to the Carrier Thread. Therefore, there is a problem that it locks to a specific platform thread and interferes with the JVM's free thread management. In Spring, parts that use synchronized for synchronization processing are gradually switching to using ReentrantLock. ReentrantLock is also a concept supported by Java, but because it explicitly controls the timing of the lock, it has the disadvantage of not being able to respond to the use of Virtual Threads by synchronized implemented internally in the JVM. - Using ReentrantReadWriteLock, read and write locks can be applied separately, and because it enables parallelism of read operations, blocking processing can be handled more flexibly.

Concurrent programming with Kotlin

Because Kotlin also runs on the JVM, it shares the same basic thread model as Java's Thread class. Let's learn about Kotlin's special lightweight thread coroutine, which does not exist in Java. Coroutine is an important concept in asynchronous programming and has been around since assembly programming. In other words, the word itself is not a concept created in Kotlin.

Corutines

First, if you look at the configuration, you can see that it consists of coroutine scope, coroutine context, and coroutine. Coroutine scope defines the life cycle and manages memory leaks. The coroutine context manages the life cycle of the coroutine and determines the thread on which the coroutine will run. Scope, context, and coroutines themselves may appear completely separate in the picture, but they are closely related. Here, if the coroutine contains the code required for actual execution, the Job in the context has the state value of the coroutine and can be understood as a concept that contains metadata.

Continuation Passing Style

Coroutines are located on the heap and enable scheduling within a single thread without separate context switching. If a thread is assigned to process the coroutine's work and the process is interrupted, the work can be resumed from another thread. What makes this possible is the CPS paradigm, called Continuation Passing Style. Let's look at the CPS aspect in code. In Kotlin, the suspend function performs asynchronous work within a coroutine. At this time, when this interruption function is decompiled, a Continuation object and State Machine are created to allow the work to continue when the coroutine task is interrupted. The Continuation object works with the State Machine to manage the execution of the interruption function and return the result once it is complete. The State Machine controls the execution flow of the interrupt function and maintains the interrupted state. In other words, using a State Machine allows you to resume execution of a task at a breakpoint label.

Structured Concurrency

Kotlin's Structured Concurrency refers to the so-called parent-child hierarchical structure, which means having a consistent pattern within the hierarchical structure. Therefore, if the top-level scope is canceled, all coroutines within it are also canceled.

Flow

Flow is an API for handling asynchronous data streams. It is not a coroutine, but is a cold stream that is used in conjunction with a coroutine to create and release data one by one as subscribers call collect over time. It differs from the suspend function in that it has multiple return values. It is mainly used when processing events that occur asynchronously, such as network calls or database queries.

Concurrent programming with Go

Speaking of Go, the first thing that can be said about its Go-ness is that it has the linguistic characteristics of being stable, lightweight, fast, and supporting concurrency. Stable means supporting type stability, using empty interfaces such as interfaces, and avoiding using type assertions or reflect. While using Java, I have occasionally seen concerns about stability regarding reflection in projects, whereas Go pursues stability through type checking. And rather than heavy development methods such as Spring MVC, we use lightweight frameworks that allow for concise and fast development. Support for concurrency is provided by goroutines and channels, and we will briefly look at this in this post.

Gorutine

If Kotlin has coroutines, Go has goroutines. Like the coroutine we looked at earlier, this is also a lightweight thread concept. So, equally, the cost is significantly lower than that of Process and Thread Context Switching by OS. GMP model refers to how the Go runtime manages threads in Go. In the image above, you can find parts G, M, and P respectively. Here, G stands for goroutine, M stands for machine (OS level thread), and P stands for proccessor. In the picture, it is schematicized like that for easy expression, but it is not an actual 1:1:1 mapping. When a goroutine is assigned to a machine by the goroutine scheduler, it is matched by a processor. At this time, if there is a goroutine in the LRQ (Local Run Queue) for each processor, the goroutine work there is performed, and if not, the goroutine work in the GRQ (Global Run Queue) is performed. For asynchronous processing by goroutines, you can simply use goroutines by notating go in front of the function.

So, what is the difference between Virtual Thread, Corutine, and Gorutine?

We discussed this a lot with various language experts during the preparation period and at the time of the presentation. However, as a result, I was able to clearly suggest that this language's lightweight thread model is somewhat different from others and that it is better to use it in certain situations, but I was not able to come up with an answer as clear as I thought. So, I was disappointed that I couldn't show the comparison table during the presentation. After the presentation, we learned more about this area and concluded that it is more closely related to OS Threads and that it is advantageous to quickly introduce Virtual Threads in languages ​​where existing projects use Java. Corutine and Gorutine are called lightweight threads, but in the end, we came to the conclusion that Virtual Thread is an extension of Child Thread in the asynchronous aspect and should be approached from the server side.

  • Virtual Thread is not a problem because when blocking I/O occurs, a new Virtual Thread is created. This is because it yields the Carrier Thread and uploads its information to the heap.
  • Instead, virtual threads automatically give up (or yield) their carrier thread when a blocking call (such as I/O) is made. This is handled by the library and runtime [...]
  • Coroutines may affect the main thread or prevent other response processing if there is blocking I/O in the state update during event handling.

Channel

The Go language was designed according to the philosophy of "Do not communicate by sharing memory, share memory by communicating." It has a core philosophy of memory sharing through communication, and this principle means that goroutines do not share memory directly, but instead exchange data by sending messages through channels. A channel is a pipeline that safely transmits data between goroutines, reducing the possibility of data contention and enabling safe data exchange through sequential processing. Additionally, the use of channels provides a clear view of the data flow and increases the variety of design pattern implementations. The basic concurrency patterns of the Go language include Worker pool pattern, Pipe pattern, and Fan-out/Fan-in pattern. The characteristics of these three representative patterns are as follows.

Worker pool pattern

Similar in concept to a thread pool, it is a structure that creates a certain number of goroutines (workers) to retrieve tasks from the task queue and process them. This pattern can efficiently manage resource usage and reduce the overhead of creating and terminating goroutines. Worker pools are particularly useful for processing parallel tasks.

Pipe pattern

Since data is delivered sequentially through a series of processing steps, it is useful for understanding the data flow. Each step proceeds by receiving a channel and returning the channel to the next step.

Fan-out/Fan-in pattern

Fan-out/Fan-in is especially useful when processing large amounts of data or performing complex calculations in parallel and aggregating results, and tends to create and manage goroutines more dynamically than the worker pool pattern.

Concurrent programming with Typescript

Typescript is a typed superset language of JavaScript and follows JavaScript's concurrency model.

Browser operation principle

First, let's look at the general browser operating principles. The browser stores necessary variables in the Heap, and simple executions such as outputting to the console are placed in the Stack and executed one by one. However, when you need to use Web API, tasks are stacked in the Task Queue and executed through an event loop when the stack is empty. In other words, the existence of Task Queue makes asynchronous programming possible under single-thread conditions. (Examples of Web APIs include setTimeout, ajax, etc.) When JavaScript code runs in the browser, Promises and Await can be used to handle asynchronous operations. For example, you can download an image asynchronously when a web page loads, and display it on the screen when the download is complete. A Promise is an object that represents an action to be completed in the future and can be processed with a result of success or failure. Await is syntactic sugar that allows you to write promise-based asynchronous operations in a more synchronous-looking code style. It can improve the readability and maintainability of your code, but it cannot completely rule out promise chaining issues and the possibility of callback hell.

Concurrent programming with Swift

In Swift, it is organized around Swift concurrency mentioned at WWDC.

Grand Central Dispatch

GCD, or Grand Central Dispatch, is named after New York's Grand Central Terminal, a train station. GCD provides an API that makes it easy to manage a variety of multithreading tasks. Simply put, in an asynchronous situation, GCD is responsible for distributing multiple tasks to multiple threads. So is this one type of dispatch queue? no. There are Main, Global, and Custom queues. Among these, the Global queue and Custom queue can be used as a concurrent queue depending on settings. Because the Concurrent Queue can process multiple work items at once, the system loads multiple threads until all CPU cores are saturated. Here, incorrect thread management and code design can cause inefficiency in the creation of threads beyond the core due to thread explosion.

Swift concurrency

It is different from GCD as it is intended to support asynchronous operations at the language level. This is because GCD is a low-level C language-based API that directly uses CPU and threads. Task and Actor handle concurrency at the language level, making it easier to debug and understand the flow. #####Task Task is a high-level abstraction that allows you to write asynchronous code using the async/await syntax introduced in Swift 5.5. Task provides the ability to cancel running tasks. This is useful for aborting asynchronous operations and freeing associated resources. When using only async/await, there were inconveniences such as having to process async processing for the call even at the place where the call is made, but by using Task, it is possible to perform asynchronous work independently for each task.

Actor

It safely encapsulates the state and serializes tasks with a high-level abstraction that manages concurrent access, allowing only one task to access the Actor type at a time. Even when accessing, internal values ​​are isolated and can be accessed through self reference. Therefore, it has the advantage of not causing a data race.

Concurrent programming with Dart

Future

It is an abstract class that holds the values of asynchronous results, i.e. future values. Similar to JavaScript Promises, they provide a chaining mechanism that allows chaining additional operations based on the results of a single asynchronous operation. Dart's Future has then, catchError, and whenComplete methods.

Stream

Used to process sequential events. Stream itself is a concept often used for event processing in other languages, making it possible to process events that occur over time. You can receive and process events with the listen method. #####isolate Each isolate is a context that has an independent memory space and communicates only through channels. I heard that it is often chosen in the field to handle tasks that take a long time or to separate concerns for policy purposes. When using isolate, complete separation is achieved because each child has a different PID, but in cases where work requires this level of separation, it is often composed of different modules or projects, so there are not many cases where isolate is used.

Summarizing the presentation

In my desire to cover the differences between languages, which were too large and diverse as a presentation topic, there was a shortcoming in not being able to cover more about concurrency in individual languages. Personally, I regret not being able to prepare a presentation full of confidence because I had not dealt with all languages ​​in the workplace. However, if I had not had this opportunity, I would have been stuck in the frame of being a back-end developer and would not have been able to think about or worry about other languages. This year, I will try to use various languages ​​and create an opportunity to think more deeply about my stack! 🫡

Comments

No comments yet. Be the first!

    164 posts in 테크

    15 / 164