Some brief notes and summaries are made according to the reference article. Please refer to the references section for more information

Concurrency is a common trait between Concurrency and Parallel

Concurrency and parallelism are similar concepts. Like concurrency, parallelism refers to two or more tasks being executed simultaneously. But strictly speaking, the concepts of concurrency and parallelism are not the same, there are big differences between the two.

A simple diagram makes it easy to understand parallelism and concurrency

Intuitively, concurrency is two people in a queue competing for a coffee machine. In reality, it could be alternating between two teams, or it could be fighting over each other — that’s competition.

In parallel, each queue has its own coffee machine. There is no competition between the two queues. A queuer in the queue only needs to wait for the person in front of the queue to finish using the coffee machine, and then it is his turn to use the coffee machine.

Concurrency is when one processor processes multiple tasks simultaneously, whereas parallel processors or multi-core processors process multiple different tasks simultaneously. The former is logically simultaneous while the latter is physically simultaneous.

Second, the process

A process (English: process) is a program that is already running in a computer. Processes were once the basic operating unit of time-sharing systems.

It includes specific things such as separate memory, separate Pids in the system, etc.

In TDM systems, the operating system performs context switching between different processes to achieve the effect of “concurrency”.

We simply need to know that it is a basic unit, its switching is within the operating system, and its context switching is quite time consuming and memory consuming.

Some Ruby frameworks work through process +fork, such as Sidekiq. Forking has all the drawbacks:

  • Context switching takes a long time
  • Context memory is relatively large
  • If the parent process dies, it becomes a zombie process waiting to be reclaimed.

Three, thread

A thread (English: thread) is the smallest unit in which an operating system can schedule operations. In most cases, it is contained within a process and is the actual operational unit of the process. A thread is a single sequential flow of control in a process, and multiple threads can be concurrent in a process, each performing a different task in parallel.

And then the operating system will also schedule internal threads within the process, let’s say multiple threads, and switch between them.

In contrast to processes, threads make up for any problems with process switching

3.1 The meaning of multithreading

Advantages of multithreading

  • The Shared memory
  • Context switching time period
  • Less memory
  • When the parent process shuts down, the child process shuts down automatically
  • Task fragmentation

Multithreading is the equivalent of operating system time slicing. What would our programs look like without multithreading?

Take the movement of cars as an example, the following two colors of cars, A and B, if we want to move them, we can only move them in order.

If A multithreaded program is supported, A and B can be moved simultaneously. For example, some games have tanks fighting multiple tank movements; You can chat and play music in an application like a browser and so on.

3.2 Problems with Threads

  • Competition brings up complex issues involving locks

Just like the coffee machine model mentioned earlier in parallel concurrency, multiple threads, they are using the CPU and there is a race problem, to whom? It is usually handed over to the operating system to schedule. But when they try to access the same memory read or write, there is often a problem because the order cannot be guaranteed — that is, the thread is not safe.

Take Ruby as an example

a = 0

threads = (1.10).map do |i|
  Thread.new(i) do |i|
    c = a
    sleep(rand(0.1))
    c += 10
    sleep(rand(0.1))
    a = c
  end
end

threads.each { |t| t.join }

puts a
Copy the code

All this code does is add variable A 10 times, each time by 10, and open 10 threads to do the job. Normally we would expect a == 100, but it turns out to be


> ruby a.rb
10

> ruby a.rb
10

> ruby a.rb
30

> ruby a.rb
20
Copy the code

The reason for this is that in the middle of our operation another thread intervenes, causing data chaos. In order to highlight the problem, we use the sleep method to transfer control to other threads. However, in reality, context switch between threads is scheduled by the operating system, so it is difficult for us to analyze its specific behavior.

In reality, to solve the problem, we need to lock


a = 0
mutex = Mutex.new

threads = (1.10).map do |i|
  Thread.new(i) do |i|
    # lock
    mutex.synchronize do
      c = a
      sleep(rand(0.1))
      c += 10
      sleep(rand(0.1))
      a = c
    end
  end
end

threads.each { |t| t.join }

puts a
Copy the code

This will guarantee the results

> ruby a.rb
100

> ruby a.rb
100
Copy the code

Four, GIL

In both Python and Ruby there is a thing called a GIL (global parser lock).

What role does GIL play? Take The MRI interpreter of Ruby as an example, MRI with GIL can only realize concurrency, and cannot make full use of the multi-core features of CPU to realize parallel tasks, thus reducing the running time of the program.

In MRI, threads can only run when they acquire the GIL lock. Even if we create multiple threads, there is essentially only one thread instance that can acquire the GIL, so there can only be one thread running at a time.

Consider the following scenario:

The teacher arranged a weeding task for Blue, blue in order to speed up the call of a friend Zhang, but weeding task needs a hoe to carry out. For this reason, even with the help of a friend, there is only one hoe so two people can’t do the weeding task at the same time, only the one who has the right to use the hoe can do it. This hoe is like the parser GIL, imagine zhang with little blue as the two threads that were created, when two people work efficiency as restricted to hoe the constraints and cannot to weed control tasks at the same time, can only be used interchangeably hoe, essentially does not reduce the work time, will be at the time of substitution (context switching) take away a certain amount of time.

Creating more threads in some scenarios does not really reduce the running time of the program, but may increase the overhead of context switching as the number of processes increases, making the program slower.

Fifth, coroutines

Threads are switched by the operating system. While system switching is a general strategy, it is unnecessary to waste time in some scenarios.

A coroutine is a way for programmers to manually switch, as threads can work together to complete tasks without unnecessary switching. How to switch and whom to assign depends on the actual task of the programmer.

For example, Xiao Ming does Chinese, math and English in three classes. System scheduling uses a common policy, and its possible choice is to balance between three tasks. The system is constantly switching, which actually wastes a lot of time.

We actually do it because it’s the better choice. This reduces meaningless switching and improves efficiency.

For example, when we encounter IO, there are several cases:

  1. A single thread can only wait for the I/O to complete before continuing.

  2. System undifferentiated scheduling, switching between worker threads and IO threads.

  3. Coroutines are manually scheduled, and when IO is encountered, control is transferred directly to other code. This allows you to purposefully write non-blocking, high-throughput programs.

Make full use of multi-core to liberate GIL

Ruby, for example, wants to remove the awkwardness of GIL, who has four people working with one hoe. You can choose to use an interpreter that removes the GIL. In addition to the GIL implementation, Rubinius and jruby are implemented in C++ and Java respectively. Besides removing the GIL lock, they also made other optimizations. In some cases they had better performance than MRI.

This requires that you avoid race conditions in your program.

Some good examples are Python’s Flask framework, which cleverly implements thread isolation by setting up a map of different thread ids to store request and response contexts, thus achieving thread safety in dealing with the Web.

There are also models that specifically address multithreading

6.1 Actor Model Ractor (compound word Ruby Actors) concurrency model

  • Ractor multithreaded Ruby program guide

Ractor is similar to the concurrency models of Go and Erlang

Concrete implementation

  • concurrent-ruby

  • celluloid

6.2 Guilds Concurrency model

  • Ruby 3 Guilds concurrency model

6.3 Event-driven model

Event-driven this is an implementation of a JavaScript like principle that uses a single-threaded Eventloop to do a lot of IO without worrying about threads.

The specific implementation

  • EventMachine

6.4 Process +fork, let the operating system complete scheduling

This is a supplement, this solution is rooted in Unix, Linux operating system. Can take full advantage of multi-core new. This can be done through the standard library. But it will be heavier than the thread scheme above. The process does not compete because its memory is isolated.

Principle can refer to

  • Understanding Unix processes

The specific implementation

The famous Unicorn

  • unicorn

Puma also uses this model. But Puma also has multiple threads

  • puma

reference

Concurrent part

  • 1.1 What is Concurrency?

Process part

  • Process the wiki

The thread part

  • Thread wiki
  • Talk about Ruby threads

GIL part

  • No one knows GIL
  • nobody-understands-the-gil

Coroutines part

  • Talk about Ruby threads

other

  • Ruby Puma allows multi-threading in each process, with each process having its own thread pool. Most of the time you don’t encounter the race problem described above, because each HTTP request is handled on a different thread.

  • The Flask design of Python is clever because it uses a map to achieve thread isolation, with each thread having its own request and response context, and can be handled subtly in different fields.

  • Tornado of Python used coroutines to write non-blocking web servers with higher throughput.

  • GIL removal interpreter in Ruby: in addition to GIL implementation, including Rubinius and jruby which are implemented in C++ and Java respectively, in addition to GIL lock removal, they also made other aspects of optimization. In some cases they had better performance than MRI.

My BLOG