共计 4635 个字符,预计需要花费 12 分钟才能阅读完成。
前言
最近在实现两个需要,因为两者之间并没有依赖关系,所以想利用队列进行解耦;但在 Go
的规范库中并没有现成可用并且并发平安的数据结构;但 Go
提供了一个更加优雅的解决方案,那就是 channel
。
channel 利用
Go
与 Java
的一个很大的区别就是并发模型不同,Go 采纳的是 CSP(Communicating sequential processes)
模型;用 Go 官网的说法:
Do not communicate by sharing memory; instead, share memory by communicating.
翻译过去就是:不必应用共享内存来通信,而是用通信来共享内存。
而这里所提到的 通信,在 Go 里就是指代的 channel
。
只讲概念并不能疾速的了解与利用,所以接下来会联合几个理论案例更不便了解。
futrue task
Go
官网没有提供相似于 Java
的 FutureTask
反对:
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 {
// 模仿 http
System.out.println("http request");
Thread.sleep(1000);
return "request success";
}
}
但咱们能够应用 channel
配合 goroutine
实现相似的性能:
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() {
// 模仿 http 申请
time.Sleep(time.Second)
ch <- fmt.Sprintf("url=%s, res=%s", url, "ok")
}()
return ch
}
goroutine
发动申请后间接将这个 channel
返回,调用方会在申请响应之前始终阻塞,直到 goroutine
拿到了响应后果。
goroutine 相互通信
/**
* 偶数线程
*/
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() + "+-+ 偶数" + i);
number.flag = false;
TwoThreadWaitNotifySimple.class.notify();}
} else {
try {TwoThreadWaitNotifySimple.class.wait();
} catch (InterruptedException e) {e.printStackTrace();
}
}
}
}
}
}
/**
* 奇数线程
*/
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() + "+-+ 奇数" + i);
number.flag = true;
TwoThreadWaitNotifySimple.class.notify();}
} else {
try {TwoThreadWaitNotifySimple.class.wait();
} catch (InterruptedException e) {e.printStackTrace();
}
}
}
}
}
}
这里截取了”两个线程交替打印奇偶数“的局部代码。
Java 提供了 object.wait()/object.notify()
这样的期待告诉机制,能够实现两个线程间通信。
go
通过 channel
也能实现雷同成果:
func main() {ch := make(chan struct{})
go func() {
for i := 1; i < 11; i++ {ch <- struct{}{}
// 奇数
if i%2 == 1 {fmt.Println("奇数:", i)
}
}
}()
go func() {
for i := 1; i < 11; i++ {
<-ch
if i%2 == 0 {fmt.Println("偶数:", i)
}
}
}()
time.Sleep(10 * time.Second)
}
实质上他们都是利用了线程 (goroutine
) 阻塞而后唤醒的个性,只是 Java 是通过 wait/notify 机制;
而 go 提供的 channel 也有相似的个性:
- 向
channel
发送数据时 (ch<-struct{}{}
) 会被阻塞,直到 channel 被生产(<-ch
)。
以上针对于
无缓冲 channel
。
channel
自身是由 go
原生保障并发平安的,不必额定的同步措施,能够放心使用。
播送告诉
不仅是两个 goroutine
之间通信,同样也能播送告诉,相似于如下 Java
代码:
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();
}
}
主线程将所有期待的子线程全副唤醒,这个实质上也是通过 wait/notify
机制实现的,区别只是告诉了所有期待的线程。
换做是 go
的实现:
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)
}
当敞开一个 channel
后,会使得所有获取 channel
的 goroutine
间接返回,不会阻塞,正是利用这一个性实现了播送告诉所有 goroutine
的目标。
留神,同一个 channel 不能重复敞开,不然会呈现 panic。
channel 解耦
以上例子都是基于无缓冲的 channel
,通常用于 goroutine
之间的同步;同时 channel 也具备缓冲的个性:
ch :=make(chan T, 100)
能够间接将其了解为队列,正是因为具备缓冲能力,所以咱们能够将业务之间进行解耦,生产方只管往 channel
中丢数据,消费者只管将数据取出后做本人的业务。
同时也具备阻塞队列的个性:
- 当
channel
写满时生产者将会被阻塞。 - 当
channel
为空时消费者也会阻塞。
从上文的例子中能够看出,实现雷同的性能 go 的写法会更加简略间接,绝对的 Java 就会简单许多(当然这也和这里应用的偏底层 api 无关)。
Java 中的 BlockingQueue
这些个性都与 Java 中的 BlockingQueue
十分相似,他们具备以下的相同点:
- 能够通过两者来进行
goroutine/thread
通信。 - 具备队列的特色,能够解耦业务。
- 反对并发平安。
同样的他们又有很大的区别,从体现上看:
channel
反对select
语法,对channel
的治理更加简洁直观。channel
反对敞开,不能向已敞开的channel
发送音讯。channel
反对定义方向,在编译器的帮忙下能够在语义上对行为的形容更加精确。
当然还有实质上的区别就是 channel 是 go 举荐的 CSP
模型的外围,具备编译器的反对,能够有很轻量的老本实现并发通信。
而 BlockingQueue
对于 Java
来说只是一个实现了并发平安的数据结构,即使不应用它也有其余的通信形式;只是他们都具备阻塞队列的特色,所有在初步接触 channel
时容易产生混同。
相同点 | channel 特有 |
---|---|
阻塞策略 | 反对 select |
设置大小 | 反对敞开 |
并发平安 | 自定义方向 |
一般数据结构 | 编译器反对 |
总结
有过一门编程语言的应用经验在学习其余语言是的确是要不便许多,比方之前写过 Java
再看 Go
时就会发现许多类似之处,只是实现不同。
拿这里的并发通信来说,实质上是因为并发模型上的不同;
Go
更举荐应用通信来共享内存,而 Java
大部分场景都是应用共享内存来通信(这样就得加锁来同步)。
带着疑难来学习的确会事倍功半。
最近和网友探讨后再补充一下,其实 Go channel
的底层实现也是通过对共享内存的加锁来实现的,这点任何语言都不可避免。
既然都是共享内存那和咱们本人应用共享内存有什么区别呢?次要还是 channel 的形象层级更高,咱们应用这类高形象层级的形式编写代码会更易了解和保护。
但在一些非凡场景,须要谋求极致的性能,升高加锁颗粒度时用共享内存会更加适合,所以 Go 官网也提供有 sync.Map/Mutex
这样的库;只是在并发场景下更举荐应用 channel
来解决问题。