Recently, I was implementing two requirements and wanted to decouple them using a queue since there is no dependency between them; however, there is no readily available and concurrency-safe data structure in Go’s standard library; however, Go provides a more elegant solution, which is channel.

Using Channel

One of the major differences between Go and Java is the different concurrency model; Go uses the CSP (Communicating sequential processes) model; in Go’s official words.

Do not communicate by sharing memory; instead, share memory by communicating.

And the communication mentioned here is the channel in Go.

Just talking about the concept is not fast enough to understand and apply, so we will combine a few practical examples to make it easier to understand.

futrue task

Go does not officially provide support for FutureTask similar to Java’s.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
public static void main(String[] args) throws InterruptedException, ExecutionException {
        ExecutorService executorService = Executors.newFixedThreadPool(2);
        Task task = new Task();
        FutureTask<String> futureTask = new FutureTask<>(task);
        executorService.submit(futureTask);
        String s = futureTask.get();
        System.out.println(s);
        executorService.shutdown();
    }
}
class Task implements Callable<String> {
    @Override
    public String call() throws Exception {
        // Simulation http
        System.out.println("http request");
        Thread.sleep(1000);
        return "request success";
    }
}

However, we can use the channel in conjunction with the goroutine to achieve similar functionality: the

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
func main() {
    ch := Request("https://github.com")
    select {
    case r := <-ch:
        fmt.Println(r)
    }
}
func Request(url string) <-chan string {
    ch := make(chan string)
    go func() {
        // Simulation http
        time.Sleep(time.Second)
        ch <- fmt.Sprintf("url=%s, res=%s", url, "ok")
    }()
    return ch
}

The goroutine makes the request and returns the channel directly, and the caller blocks until the goroutine gets the response.

goroutine communicate with each other

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
/**
  * Even threads
  */
 public static class OuNum implements Runnable {
     private TwoThreadWaitNotifySimple number;
     public OuNum(TwoThreadWaitNotifySimple number) {
         this.number = number;
     }
     @Override
     public void run() {
         for (int i = 0; i < 11; i++) {
             synchronized (TwoThreadWaitNotifySimple.class) {
                 if (number.flag) {
                     if (i % 2 == 0) {
                         System.out.println(Thread.currentThread().getName() + "+-+even" + i);
                         number.flag = false;
                         TwoThreadWaitNotifySimple.class.notify();
                     }
                 } else {
                     try {
                         TwoThreadWaitNotifySimple.class.wait();
                     } catch (InterruptedException e) {
                         e.printStackTrace();
                     }
                 }
             }
         }
     }
 }
 /**
  * Odd  threads
  */
 public static class JiNum implements Runnable {
     private TwoThreadWaitNotifySimple number;
     public JiNum(TwoThreadWaitNotifySimple number) {
         this.number = number;
     }
     @Override
     public void run() {
         for (int i = 0; i < 11; i++) {
             synchronized (TwoThreadWaitNotifySimple.class) {
                 if (!number.flag) {
                     if (i % 2 == 1) {
                         System.out.println(Thread.currentThread().getName() + "+-+odd" + i);
                         number.flag = true;
                         TwoThreadWaitNotifySimple.class.notify();
                     }
                 } else {
                     try {
                         TwoThreadWaitNotifySimple.class.wait();
                     } catch (InterruptedException e) {
                         e.printStackTrace();
                     }
                 }
             }
         }
     }
 }

The code for “two threads printing alternate parity numbers” is captured here.

Java provides a wait notification mechanism like object.wait()/object.notify() to enable communication between two threads.

go achieves the same effect via channel.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
func main() {
    ch := make(chan struct{})
    go func() {
        for i := 1; i < 11; i++ {
            ch <- struct{}{}
            //奇数
            if i%2 == 1 {
                fmt.Println("odd:", i)
            }
        }
    }()
    go func() {
        for i := 1; i < 11; i++ {
            <-ch
            if i%2 == 0 {
                fmt.Println("even:", i)
            }
        }
    }()
    time.Sleep(10 * time.Second)
}

Essentially they both take advantage of the thread (goroutine) blocking and wake-up feature, except that Java does it through the wait/notify mechanism.

The channel provided by go has a similar feature:

Sending data to the channel (ch<-struct{}{}) is blocked until the channel is consumed (<-ch). (For unbuffered channels)

The channel itself is guaranteed to be concurrency-safe by go natively, so you can use it without additional synchronization measures.

Broadcast Announcement

Not only can two goroutines communicate with each other, but they can also broadcast notifications, similar to the following Java code.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
public static void main(String[] args) throws InterruptedException {
    for (int i = 0; i < 10; i++) {
        new Thread(() -> {
            try {
                synchronized (NotifyAll.class){
                    NotifyAll.class.wait();
                }
                System.out.println(Thread.currentThread().getName() + "done....");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
    }
    Thread.sleep(3000);
    synchronized (NotifyAll.class){
        NotifyAll.class.notifyAll();
    }
}

The main thread wakes up all the waiting child threads, which is essentially the same thing as the wait/notify mechanism, except that it notifies all the waiting threads.

The difference is that it notifies all the waiting threads. In contrast, the go implementation is

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
func main() {
    notify := make(chan struct{})
    for i := 0; i < 10; i++ {
        go func(i int) {
            for {
                select {
                case <-notify:
                    fmt.Println("done.......",i)
                    return
                case <-time.After(1 * time.Second):
                    fmt.Println("wait notify",i)
                }
            }
        }(i)
    }
    time.Sleep(1 * time.Second)
    close(notify)
    time.Sleep(3 * time.Second)
}

When a channel is closed, it causes all goroutines that get the channel to return directly without blocking, which is the purpose of broadcasting notifications to all goroutines.

Note that the same channel cannot be closed repeatedly, otherwise panic will occur.

channel Decoupling

The above examples are based on unbuffered channels, which are usually used for synchronization between goroutines; the channels also have the characteristics of buffering.

1
ch :=make(chan T, 100)

It can be directly understood as a queue, precisely because of the buffering capability, so we can decouple the business between the production side just throw data into the channel, the consumer just take the data out and do their own logic.

It also has the characteristics of a blocking queue.

  • Producers will block when the channel is full.
  • Consumers will also block when the channel is empty.

As you can see from the example above, it is much simpler and more straightforward to implement the same functionality in go, as opposed to Java, which is much more complex (of course, this is also related to the underlying api used here).

BlockingQueue in Java

These features are very similar to the BlockingQueue in Java, and they have the following similarities.

  • Can be used for goroutine/thread communication through both.
  • Features a queue to decouple operations.
  • Supports concurrency security.

Again they are very different

  • channel supports select syntax, which makes channel management more concise and intuitive.
  • channel supports closing, and cannot send messages to closed channels.
  • The channel supports defining directions, which allows for more accurate semantic description of behavior with the help of the compiler.

The essential difference is that channel is the core of the go-recommended CSP model, which has compiler support and can be used to achieve concurrent communication at a very low cost.

BlockingQueue, on the other hand, is just a data structure for Java that implements concurrency safety, and there are other ways to communicate even without it; it’s just that they both have the characteristics of a blocking queue, so it’s easy to get confused when you first encounter channel.

Same channel Special
Blocking Strategy Support select
Set size Support Close
Concurrent Security Custom Orientation
General Data Structures Compiler Support

Summary

For example, if you have written Java before and then look at Go, you will find many similarities, but the implementation is different.

In the case of concurrent communication, this is essentially because of the difference in the concurrency model.

Go prefers to use communication to share memory, while Java uses shared memory for communication in most scenarios (so you have to add locks to synchronize).

It’s really more efficient to learn with doubts.


Reference https://crossoverjie.top/2021/07/02/go/go%20channel%20vs%20java%20BlockingQueue/