Go语言:两种常见的并发模型

Go语言:两种常见的并发模型

在并发编程中,须要精确地控制对共享资源的访问,Go语言将共享的值通过通道传递

并发版"Hello World"

使用goroutine来打印"Hello World"

package main

import "fmt"

func main() {
	done := make(chan int, 10) // 缓冲通道

	for i := 0; i < cap(done); i++ {
		go func() {
			fmt.Println("Hello World")
			done <- 1
		}()
	}

	for i := 0; i < cap(done); i++ {
		<-done
	}
}

上述代码使用了一个大小为10的缓冲通道,使用一个循环启用了10个goroutine来打印一句"Hello World",利用了通道的特性,当goroutine没有全部完成时,势必会有一个<-done被阻塞,于是基于这一点来等待这10个goroutine的结束。

利用WaitGroup可以达成同样的目的:

package main

import (
	"fmt"
	"sync"
)

func main() {
	var wg sync.WaitGroup

	for i := 0; i < 10; i++ {
		wg.Add(1)
		go func() {
			fmt.Println("Hello World")
			wg.Done()
		}()
	}
	wg.Wait()
}

必须确保在后台线程启动之前执行wg.Add(1),用于增加等待事件的个数。当后台线程完成打印工作之后,调用wg.Done表示完成一个事件,wg.Wait等待全部时间完成。

生产者/消费者模型

生产者生产一些数据,然后放到产品队列中,同时消费者从产品队列中取得这些数。生产和消费是两个异步的过程,当产品队列中没有数据时,消费者就进入饥饿等待中,当产品队列中数据已满时,生产者则面临因产品积压导致CPU被剥夺的问题。

package main

import (
	"fmt"
	"time"
)

func producer(factor int, out chan<- int) {
	for i := 0; ; i++ {
		out <- factor * i
	}
}

func consumer(in <-chan int) {
	for v := range in {
		fmt.Println(v)
	}
}

func main() {
	ch := make(chan int, 64)

	go producer(3, ch)
	go producer(5, ch)
	go consumer(ch)

	time.Sleep(5 * time.Second)
}

上述代码模拟了这一过程,定义了一个producer函数作为生产者,定义了一个consumer函数作为消费者,在主函数中,创建了一个64个int大小的队列,用于存放producer生产好的"产品",producer会不断向该队列发送数据,consumer会迭代这个队列,打印出队列中的数据。

上述程序中是采用sleep让主线程沉睡来让producer线程和consumer线程运行一段时间,可以考虑利用信号来退出

func main() {
	ch := make(chan int, 64)

	go producer(3, ch)
	go producer(5, ch)
	go consumer(ch)

	sig := make(chan os.Signal, 1)
	signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM) // 挂接信号
	fmt.Printf("quit (%v)\n", <-sig)
}

按下Ctrl+C就会产生一个SIGINT中断信号发送到sig通道中,使该通道不再阻塞,使得程序正常退出。

发布/订阅模型

pub/sub模型,在该模型中,消息生产者为发布者,消息消费者为订阅者,生产者和消费者是M:N关系。在上述的生产者/消费者模型中,是将消息发送到一个队列中,而发布/订阅模型则是将消息发布给一个主题。

具体看代码:

package main

import (
	"fmt"
	"strings"
	"sync"
	"time"
)

type (
	subscriber chan interface{}         // 订阅者为一个通道
	topicFunc  func(v interface{}) bool // 主题为一个过滤器
)

type Publisher struct {
	mutex       sync.RWMutex             // 读写锁
	buffer      int                      // 订阅队列的缓存大小
	timeout     time.Duration            // 发布超时时间
	subscribers map[subscriber]topicFunc // 订阅者信息
}

// NewPublisher 构建一个发布者对象 可以设置发布超时时间和缓存队列长度
func NewPublisher(publishTimeout time.Duration, buffer int) *Publisher {
	return &Publisher{
		buffer:      buffer,
		timeout:     publishTimeout,
		subscribers: make(map[subscriber]topicFunc),
	}
}

// SubscribeTopic 添加一个新的订阅者,订阅过滤器筛选后的主题
func (p *Publisher) SubscribeTopic(topic topicFunc) chan interface{} {
	ch := make(chan interface{}, p.buffer)
	p.mutex.Lock()
	p.subscribers[ch] = topic
	p.mutex.Unlock()
	return ch
}

// Subscribe 添加一个新的订阅者,订阅全部主题
func (p *Publisher) Subscribe() chan interface{} {
	return p.SubscribeTopic(nil)
}

// Evict 退出订阅
func (p *Publisher) Evict(sub chan interface{}) {
	p.mutex.Lock()
	defer p.mutex.Unlock()
	delete(p.subscribers, sub)
	close(sub)
}

// Publish 发布一个主题
func (p *Publisher) Publish(v interface{}) {
	p.mutex.RLock()
	defer p.mutex.RUnlock()

	var wg sync.WaitGroup
	for sub, topic := range p.subscribers {
		wg.Add(1)
		go p.sendTopic(sub, topic, v, &wg)
	}
	wg.Wait()
}

// Close 关闭发布者对象,同时关闭所有的订阅者通道
func (p *Publisher) Close() {
	p.mutex.Lock()
	defer p.mutex.Unlock()

	for sub := range p.subscribers {
		delete(p.subscribers, sub)
		close(sub)
	}
}

// sendTopic 发送主题,可以容忍一定的时限
func (p *Publisher) sendTopic(sub subscriber, topic topicFunc, v interface{}, wg *sync.WaitGroup) {
	defer wg.Done()
	if topic != nil && !topic(v) {
		return
	}
	select {
	case sub <- v:
	case <-time.After(p.timeout):
	}
}

func main() {
	p := NewPublisher(100*time.Microsecond, 10)
	defer p.Close()

	all := p.Subscribe() // 订阅所有主题
    // 订阅golang主题
	golang := p.SubscribeTopic(func(v interface{}) bool {
		if s, ok := v.(string); ok {
			return strings.Contains(s, "golang")
		}
		return false
	})

	p.Publish("Hello world")
	p.Publish("Hello golang")

	go func() {
		for msg := range all {
			fmt.Println("all:", msg)
		}
	}()

	go func() {
		for msg := range golang {
			fmt.Println("golang:", msg)
		}
	}()

	// 运行一段时间后退出
	time.Sleep(3 * time.Second)
}

程序中定义了Publisher结构体,为其定义两种订阅方法,对于SubscribeTopic函数要传入一个函数作为过滤器,该过滤器遇到未订阅的主题消息时会返回false,对于订阅的主题消息则返回true。sendTopic函数的作用是向订阅者的通道发送主题消息,并且调用过滤器判断消息是否需要,如果不需要(false),则丢弃数据,返回该函数。sendTopic函数会被放置在Publish函数中,对每个订阅者都调用sendTopic函数来发送主题消息。在main函数中创建了两个订阅者(通道),分别订阅所有主题和订阅一个主题,之后创建了两个线程,不断打印两个订阅队列的消息。

在上述pub/sub模型中,每条消息都会传送给多个订阅者。发布者通常不会知道,也不关心哪一个订阅者正在接收主题消息。订阅者和发布者可以在运行时动态添加,它们之间是一条松散的耦合关系,这使得系统的复杂性随时间的推移而增长。

安全退出

通知线程停止任务,特别是当它工作在错误的方向上时。

package main

import (
	"fmt"
	"sync"
	"time"
)

func worker(wg *sync.WaitGroup, cancel chan bool) {
	defer wg.Done()

	for {
		select {
		default:
			fmt.Println("working")
		case <-cancel: // 退出信号
			return
		}
	}
}

func main() {
	// 创建通道 传递退出信号
	cancel := make(chan bool)
	var wg sync.WaitGroup
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go worker(&wg, cancel)
	}
	time.Sleep(time.Second)
	close(cancel)
	wg.Wait()
}

这里利用了select关键字,当select有多个分支时会随机选择一个可用的通道分支,如果没有可用的通道分支,则选择default分支,否则会一直阻塞。

在上述代码中,当关闭通道时,也起到了一个广播的作用,所有线程函数中的select中的cancel通道不再阻塞,将线程退出。

context包

利用context包可以达成更理想的效果,以此来实现线程的安全退出,基于此将上述代码修改:

package main

import (
	"context"
	"fmt"
	"sync"
	"time"
)

func worker(ctx context.Context, wg *sync.WaitGroup) error {
	defer wg.Done()

	for {
		select {
		default:
			fmt.Println("Hello")
		case <-ctx.Done():
			return ctx.Err()
		}
	}
}

func main() {
	// 设置超时时间为10秒
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	var wg sync.WaitGroup
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go worker(ctx, &wg)
	}
	time.Sleep(time.Second)
	cancel() // 将所有线程退出
	wg.Wait()
}
posted @ 2022-07-01 23:05  N3ptune  阅读(154)  评论(0编辑  收藏  举报