草庐IT

我的Go并发之旅、01 并发哲学与并发原语

小能的博客 CanAngle's Blog 2023-03-28 原文

注:本文所有函数名为中文名,并不符合代码规范,仅供读者理解参考。

上下文

上下文(Context)代表了程序(也可以是进程,操作系统,机器)运行时的环境和状态,联系程序整个生命周期与资源调用,是程序可以访问到的所有资源的总和,资源可以是一个变量,也可以是一个对象的引用。

上下文切换

所谓的上下文切换(context switch),指的是发生进程调度(进程切换)时,内核(kernel)要把当前进程的状态和数据保存起来以备以后使用,同时把之前保存的进程的相关状态调出来,这样新调度出来的进程才能运行。

原语

原语,一般是指由若干条指令组成的程序段,用来实现某个特定功能,在执行过程中不可被中断。

计算机是一门人造科学,因此真正意义上的“原语”(Primitive)是不存在的。操作系统层面上的“原语”(比如 write 之类的系统调用)对程序员来讲的确是不可分割的最小单位,但是这些系统调用本身还是用好几句汇编语句组成的(对于 Linux 来说是 C 语言)。可能有人要说到了机器代码这一级就不能再分了,但事实上一条机器指令也是由好几个组合逻辑信号构成的。同样的道理,控制信号也不过是无数电子在器件内部漂移的结果。

因此定义“原语”的前提是观察者所处的位置上下文)。一旦规定了观察者的位置和观察的角度,比如就在操作系统的这层上,read,write,wait这些个系统调用自然就是最“原始”的词汇,这也是为什么“原语”会在操作系统中频繁出现的缘故。

上下文也可以被定义为一个操作被认为是原子性的界限。

竞争条件

两个或多个操作必须按正确的顺序执行,而程序并未保证这个顺序,就会发生竞争条件。

大多数情况下,竞争条件会出现在数据竞争中,一个并发操作尝试读取一个变量,而在某个不确定的时间,另一个并发操作视图写入同一个变量。因为开发人员总是用顺序性的思维来思考问题,他们总假设某行代码会先于另一行代码执行。

func 竞争条件() {
	var data int
	go func() {
		data++
	}()
	if data == 0 {
		fmt.Println("Value", data)
	}
}

在这段代码中会出现三种可能性,可以根据 go程(goroutine)、if判断、打印输出执行的顺序判断。

  • if、打印、go程 => Value 0
  • if、go程、打印 => Value 1
  • go程、if => 跳过打印,无输出

仅仅几行代码就给程序带来了巨大的不确定性。

有时,某些程序员会写出一种看似解决了问题的“方案”,就是添加 time.Sleep,但是这种方案并不可靠!通过显式休眠的方式只是在概率上增加了逻辑的正确性,但不会真正变成逻辑上的正确。休眠时间也会影响程序的运行效果!

原子性

某些东西被认为是原子的,或者具有原子性的时候,这意味着在它运行的环境中,它是不可分割或不可中断的。在你所定义的上下文中,原子的东西将被完整的运行,在这种情况下不会同时发生任何事情。

在考虑原子性时,经常第一件需要做的事就是定义上下文或范围,然后再考虑这些操作是否是原子性的。

当一个东西是原子的,说明它在并发环境中是安全的。而大多数语句不是原子的,更不用说函数、方法和程序了,所以为了构建逻辑正确的程序,需要我们做内存访问同步,使用一系列操作来强制保持原子性。

内存访问同步

临界区:程序中需要独占访问共享资源的部分。(可以理解成读写共享资源的代码段)

正如前面的例子,保护程序临界区的一个方法是在临界区之间内存访问做同步。具体做法是添加一个互斥锁。

func 竞争条件_互斥锁() {
	var lock sync.Mutex
	var data int
	go func() {
		lock.Lock()
		data++
		lock.Unlock()
	}()
	lock.Lock()
	if data == 0 {
		fmt.Println("Value", data)
	} else {
		fmt.Println("Value", data)
	}
	lock.Unlock()
}

如果你发现你的代码中有临界区,那就添加互斥锁,保证各个临界区对共享资源(data)的独占访问权,从而对内存的访问进行了同步。但这仅仅解决了数据竞争,没有解决竞争条件 !这种方式同步对内存的访问有性能上的问题。

死锁

所有并发进程彼此等待。

活锁

正在主动执行并发操作的程序,但无法向前推进程序状态。

饥饿

在任何情况下,并发进程都无法获得执行工作所需的所有资源

通常意味着有一个或多个贪婪的并发进程,不公平地阻止(贪婪地抢占锁,以完成整个工作循环(通常是不必要地扩大其持有共享锁上的临界区))一个或多个并发进程以尽可能有效地完成工作。

饥饿也可能产生于 CPU、内存、文件句柄、数据库连接,任何必须共享的资源都是有可能产生饥饿的原因。

找到同步访问内存的平衡点

同步访问内存代价是昂贵的,所以将锁扩展到临界区外是有利的,但是会产生前面的饥饿问题。

需要在粗粒度和细粒度同步之间找到一个平衡点。

一般经验是将内存访问同步限制在关键部分,不扩展到临界区外;直到同步成为性能问题,再扩展范围。

给并发函数注释

  • 谁负责并发,是调用者负责,还是函数自己负责。
  • 如何利用并发原语解决这个问题的。比如递归调用。
  • 谁负责同步,调用者负责内存访问同步还是结构体内部处理。

函数可以采用纯函数式方法,尽可能消除同步的问题。

函数最好返回一个只读的channel,而不是传入参数的指针,这样更具有明确性。

并发与并行

并发属于代码,并行属于一个运行中的程序的属性。

并行的"同时"是同一时刻可以多个进程在运行(处于running),并发的"同时"是经过上下文快速切换,使得看上去多个进程同时都在运行的现象,是一种OS欺骗用户的现象

实际上,当程序中写下多进程或多线程代码时,这意味着的是并发而不是并行。并发是因为多进程/多线程都是需要去完成的任务,不并行是因为并行与否由操作系统的调度器决定,可能会让多个进程/线程被调度到同一个CPU核心上。只不过调度算法会尽量让不同进程/线程使用不同的CPU核心,所以在实际使用中几乎总是会并行,但却不能以100%的角度去保证会并行。也就是说,并行与否程序员无法控制,只能让操作系统决定

CSP

通信顺序进程。一个进程的输出应该直接流向另一个进程的输入。

Go魅力

一般编程语言会把它们的抽象链结束在系统线程和内存访问同步的层级

但是Go语言采取了不同的路线,使用goroutine和channel代替这些概念。goroutine把我们从必须按照并行的思考方式中解放出来,作为替代,他允许我们按照更为自然的等级对问题进行建模。Go语言的运行时自动地将goroutine映射到系统的线程上,并为我们管理它们之间的调度(智能分配OS线程)。

Go并发哲学

不要通过共享内存进行通信,通过通信来共享内存。在面对不同场景时,选择不同的方式。

数据所有权:并发程序安全就是保证同时只有一个并发上下文拥有数据的所有权,通过channel可以把数据传递给其他go程,解耦生产者和消费者。

追求简洁,尽量使用channel,并且认为goroutine的使用是没有成本的。

参考书籍

  • 《Go语言并发之道》Katherine CoxBuday

  • 《Go语言核心编程》李文塔

  • 《Go语言高级编程》柴树彬、曹春辉

有关我的Go并发之旅、01 并发哲学与并发原语的更多相关文章

  1. ruby-on-rails - 如何在我的 Rails 应用程序 View 中打印 ruby​​ 变量的内容? - 2

    我是一个Rails初学者,但我想从我的RailsView(html.haml文件)中查看Ruby变量的内容。我试图在ruby​​中打印出变量(认为它会在终端中出现),但没有得到任何结果。有什么建议吗?我知道Rails调试器,但更喜欢使用inspect来打印我的变量。 最佳答案 您可以在View中使用puts方法将信息输出到服务器控制台。您应该能够在View中的任何位置使用Haml执行以下操作:-puts@my_variable.inspect 关于ruby-on-rails-如何在我的R

  2. ruby - 我可以将我的 README.textile 以正确的格式放入我的 RDoc 中吗? - 2

    我喜欢使用Textile或Markdown为我的项目编写自述文件,但是当我生成RDoc时,自述文件被解释为RDoc并且看起来非常糟糕。有没有办法让RDoc通过RedCloth或BlueCloth而不是它自己的格式化程序运行文件?它可以配置为自动检测文件后缀的格式吗?(例如README.textile通过RedCloth运行,但README.mdown通过BlueCloth运行) 最佳答案 使用YARD直接代替RDoc将允许您包含Textile或Markdown文件,只要它们的文件后缀是合理的。我经常使用类似于以下Rake任务的东西:

  3. jquery - 我的 jquery AJAX POST 请求无需发送 Authenticity Token (Rails) - 2

    rails中是否有任何规定允许站点的所有AJAXPOST请求在没有authenticity_token的情况下通过?我有一个调用Controller方法的JqueryPOSTajax调用,但我没有在其中放置任何真实性代码,但调用成功。我的ApplicationController确实有'request_forgery_protection'并且我已经改变了config.action_controller.consider_all_requests_local在我的environments/development.rb中为false我还搜索了我的代码以确保我没有重载ajaxSend来发送

  4. java - 我的模型类或其他类中应该有逻辑吗 - 2

    我只想对我一直在思考的这个问题有其他意见,例如我有classuser_controller和classuserclassUserattr_accessor:name,:usernameendclassUserController//dosomethingaboutanythingaboutusersend问题是我的User类中是否应该有逻辑user=User.newuser.do_something(user1)oritshouldbeuser_controller=UserController.newuser_controller.do_something(user1,user2)我

  5. ruby-on-rails - 测试我的 Ruby gem:Shoulda::Matchers:Module (NoMethodError) 的未定义方法 `configure' - 2

    我正在开发我的第一个Rubygem,并捆绑了cucumber、rspec和shoulda-matches进行测试。当我运行rspec时,出现以下错误:/app/my_gem/spec/spec_helper.rb:6:in`':undefinedmethod`configure'forShoulda::Matchers:Module(NoMethodError)这是我的gem规范:#my_gem.gemspec...Gem::Specification.newdo|spec|......spec.add_development_dependency"activemodel"spec.a

  6. ruby-on-rails - Heroku 吃掉了我的自定义 HTTP header - 2

    我正在使用Heroku(heroku.com)来部署我的Rails应用程序,并且正在构建一个iPhone客户端来与之交互。我的目的是将手机的唯一设备标识符作为HTTPheader传递给应用程序以进行身份​​验证。当我在本地测试时,我的header通过得很好,但在Heroku上它似乎去掉了我的自定义header。我用ruby​​脚本验证:url=URI.parse('http://#{myapp}.heroku.com/')#url=URI.parse('http://localhost:3000/')req=Net::HTTP::Post.new(url.path)#boguspara

  7. ruby - 我的 Ruby IRC 机器人没有连接到 IRC 服务器。我究竟做错了什么? - 2

    require"socket"server="irc.rizon.net"port="6667"nick="RubyIRCBot"channel="#0x40"s=TCPSocket.open(server,port)s.print("USERTesting",0)s.print("NICK#{nick}",0)s.print("JOIN#{channel}",0)这个IRC机器人没有连接到IRC服务器,我做错了什么? 最佳答案 失败并显示此消息::irc.shakeababy.net461*USER:Notenoughparame

  8. ruby-on-rails - rspec - 我怎样才能让 "pendings"有我的文本而不仅仅是 "No reason given" - 2

    我有这个代码:context"Visitingtheusers#indexpage."dobefore(:each){visitusers_path}subject{page}pending('iii'){shouldhave_no_css('table#users')}pending{shouldhavecontent('Youhavereachedthispageduetoapermissionic错误')}它会导致几个待处理,例如ManagingUsersGivenapractitionerloggedin.Visitingtheusers#indexpage.#Noreason

  9. ruby-on-rails - 获取并发布相同匹配项的请求 - 2

    在我的路线文件中我有:match'graphs/(:id(/:action))'=>'graphs#(:action)'如果是GET请求(工作)或POST请求(不工作),我想匹配它我知道我可以使用以下方法在资源中声明POST请求:post'/'=>:show,:on=>:member但是我怎样才能为比赛做到这一点呢?谢谢。 最佳答案 如果你同时想要POST和GETmatch'graphs/(:id(/:action))'=>'graphs#(:action)',:via=>[:get,:post]编辑默认值可以设置如下match'g

  10. ruby - 为什么 return 关键字会导致我的 'if block' 出现问题? - 2

    下面的代码工作正常:person={:a=>:A,:b=>:B,:c=>:C}berson={:a=>:A1,:b=>:B1,:c=>:C1}kerson=person.merge(berson)do|key,oldv,newv|ifkey==:aoldvelsifkey==:bnewvelsekeyendendputskerson.inspect但是如果我在“ifblock”中添加return,我会得到一个错误:person={:a=>:A,:b=>:B,:c=>:C}berson={:a=>:A1,:b=>:B1,:c=>:C1}kerson=person.merge(berson

随机推荐