不加香菜

不加香菜 查看完整档案

填写现居城市清华大学  |  电气工程 编辑字节跳动  |  开发 编辑填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 个人简介什么都没有

个人动态

不加香菜 发布了文章 · 3月28日

go语言happens-before原则及应用

了解go中happens-before规则,寻找并发程序不确定性中的确定性。

引言

先抛开你所熟知的信号量、锁、同步原语等技术,思考这个问题:如何保证并发读写的准确性?一个没有任何并发编程经验的程序员可能会觉得很简单:这有什么问题呢,同时读写能有什么问题,最多就是读到过期的数据而已。一个理想的世界当然是这样,只可惜实际上的机器世界往往隐藏了很多不容易被察觉的事情。至少有两个行为会影响这个结论:

  • 编译器往往有指令重排序的优化;例如程序员看到的源代码是a=3; b=4;,而实际上执行的顺序可能是b=4; a=3;,这是因为编译器为了优化执行效率可能对指令进行重排序;
  • 高级编程语言所支持的运算往往不是原子化的;例如a += 3实际上包含了读变量、加运算和写变量三次原子操作。既然整个过程并不是原子化的,就意味着随时有其它“入侵者”侵入修改数据。更为隐藏的例子:对于变量的读写甚至可能都不是原子化的。不同机器读写变量的过程可能是不同的,有些机器可能是64位数据一次性读写,而有些机器是32位数据一次读写。这就意味着一个64位的数据在后者的读写上实际上是分成两次完成的!试想,如果你试图读取一个64位数据的值,先读取了低32的数据,这时另一个线程切进来修改了整个数据的值,最后你再读取高32的值,将高32和低32的数据拼成完整的值,很明显会得到一个预期以外的数据。

看起来,整个并发编程的世界里一切都是不确定的,我们不知道每次读取的变量到底是不是及时、准确的数据。幸运的是,很多语言都有一个happens-before的规则,能帮助我们在不确定的并发世界里寻找一丝确定性。

happens-before

你可以把happens-before看作一种特殊的比较运算,就好像><一样。对应的,还有happens-after,它们之间的关系也好像><一样:

如果a happens-before b,那么b happens-after a

那是否存在既不满足a happens-before b,也不满足b happens-before a的情况呢,就好像既不满足a>b,也不满足b>a(意味着b==a)?当然是肯定的,这种情况称为:a和b happen concurrently,也就是同时发生,这就回到我们之前所熟知的世界里了。

happens-before有什么用呢?它可以用来帮助我们厘清两个并发读写之间的关系。对于并发读写问题,我们最关心的经常是reader是否能准确观察到writer写入的值。happens-before正是为这个问题设计的,具体来说,要想让某次读取r准确观察到某次写入w,只需满足:

  1. w happens-before r;
  2. 对变量的其它写入w1,要么w1 happens-before w,要么r happens-before w1;简单理解就是没有其它写入覆盖这次写入;

只要满足这两个条件,那我们就可以自信地肯定我们一定能读取到正确的值。

一个新的问题随之诞生:那如何判断a happens-before b是否成立呢?你可以类比思考数学里如何判断a > b是否成立的过程,我们的做法很简单:

  1. 基于一些简单的公理;例如自然数的自然大小:3>2>1
  2. 基于比较运算符的传递性,也就是如果a>b且b>c,则a>c

判断a happens-before b的过程也是类似的:根据一些简单的明确的happens-before关系,再结合happens-before的传递性,推导出我们所关心的w和r之间的happens-before关系。

happens-before传递性:如果a happens-before b,且b happens-before c,则a happens-before c

因此我们只需要了解这些明确的happens-before关系,就能在并发世界里寻找到宝贵的确定性了。

go语言中的happens-before关系

具体的happens-before关系是因语言而异的,这里只介绍go语言相关的规则,感兴趣可以直接阅读官方文档,有更完整、准确的说明。

自然执行

首先,最简单也是最直观的happens-before规则:

同一个goroutine里,书写在前的代码happens-before书写在后的代码。

例如:

a = 3; // (1)
b = 4; // (2)

则(1) happens-before (2)。我们上面提到指令重排序,也就是实际执行的顺序与书写的顺序可能不一致,但happens-before与指令重排序并不矛盾,即使可能发生指令重排序,我们依然可以说(1) happens-before (2)。

初始化

每个go文件都可以有一个init方法,用于执行某些初始化逻辑。当我们开始执行某个main方法时,go会先在一个goroutine里做初始化工作,也就是执行所有go文件的init方法,这个过程中go可能创建多个goroutine并发地执行,因此通常情况下各个init方法是没有happens-before关系的。关于init方法有两条happens-before规则:

1.a 包导入了 b包,此时b包的init方法happens-before a包的所有代码;
2.所有init方法happens-beforemain方法;

goroutine

goroutine相关的规则主要是其创建和销毁的:

1.goroutine的创建 happens-before 其执行;
2.goroutine的完成不保证happens-before任何代码;

第一条规则举个简单的例子即可:

var a string

func f() {
    fmt.Println(a) // (1)
}

func hello() {
    a = "hello, world" // (2)
    go f() // (3)
}

因为goroutine的创建 happens-before 其执行,所以(3) happens-before (1),又因为自然执行的规则(2) happens-before (3),根据传递性,所以(2) happens-before (1),这样保证了我们每次打印出来的都是"hello world"而不是空字符串。

第二条规则是少见的否定句式,同样举个简单的例子:

var a string

func hello() {
    go func() { a = "hello" }() // (1)
    fmt.Println(a) // (2)
}

由于goroutine的完成不保证happens-before任何代码,因此(1) happens-before (2)不成立,这样我们就不能保证每次打印的结果都是"hello"。

通道

通道channel是go语言中用于goroutine之间通信的主要渠道,因此理解通道之间的happens-before规则也至关重要。

1.对于缓冲通道,向通道发送数据happens-before从通道接收到数据

结合一个例子:

var c = make(chan int, 10)
var a string

func f() {
    a = "hello, world" // (1)
    c <- 0 // (2)
}

func main() {
    go f() // (3)
    <-c // (4)
    fmt.Println(a) // (5)
}

c是一个缓冲通道,因此向通道发送数据happens-before从通道接收到数据,也就是(2) happens-before (4),再结合自然执行规则以及传递性不难推导出(1) happens-before (5),也就是打印的结果保证是"hello world"。
有趣的是,如果我们把c的定义改为var c = make(chan int)也就是无缓冲通道,上面的结论就不存在了(注1),打印的结果不一定为"hello world",这是因为:

2.对于无缓冲通道,从通道接收数据happens-before向通道发送数据

我们可以将上述例子稍微调整下:

var c = make(chan int)
var a string

func f() {
    a = "hello, world" // (1)
    <- c // (2)
}

func main() {
    go f() // (3)
    c <- 10 // (4)
    fmt.Println(a) // (5)
}

对于无缓冲通道,(2) happens-before (4),再根据传递性,(1) happens-before (5),因此依然可以保证打印的结果是"hello world"。

可以这么理解这两者的差异,缓冲通道的目的是缓冲发送方发送的数据,这就意味着发送方很可能先发送数据,过一段时间后接收方才接收,或者发送方发送的速度超过接收方接收的速度,因为缓冲通道的发送happens-before接收就自然而然了;相反,非缓冲通道是没有缓冲区的,先发起的发送方和接收方都会阻塞至另一方准备好,如果我们使用了非缓冲通道,则意味着我们认为我们的场景下接收发生在发送之前,否则我们就会使用缓冲通道了,因此非缓冲通道的接收happens-before发送。

3.对于缓冲通道,第k次接收happens-beforek+C次发送,C是缓冲通道的容量

这条规则是缓冲通道的通用规则(有趣的是,上面针对非缓冲通道的第2条规则也可以看成这个规则的特例:C取0)。这个规则看起来复杂,我们看个例子就清晰了:

var limit = make(chan int, 3)

func main() {
    // work是一个worker列表,其中的元素w都是可执行函数
    for _, w := range work {
        go func(w func()) {
            limit <- 1 // (1)
            w() // (2)
            <-limit // (3)
        }(w)
    }
    select{}
}

我们先套用一下上面的规则,则:“第1次(3)happens-before第4次(1)”、“第2次(3)happens-before第5次(1)”、“第3次(3)happens-before第6次(1)”……,再结合传递性:“第1次(2)happens-before第1次(3)happens-before第4次(1)happens-before第4次(2)”、“第2次(2)happens-before第2次(3)happens-before第5次(1)happens-before第5次(2)”……,简单地说:“第1次(2)happens-before第4次(2)”、“第2次(2)happens-before第5次(2)”、“第3次(2)happens-before第6次(2)”……这样我们虽然没有做任何分批,却事实上将workers分成三个一批、每批并发地执行。这就是通过这条happens-before规则保证的。

这个规则理解起来其实也很简单,C是通道的容量,如果无法保证第k次接收happens-beforek+C次发送,那通道的缓冲就不够用了。

注1:以上是官方文档给的规则和例子,但是笔者在尝试将第一个例子的c改成无缓冲通道后发现每次打印的依然稳定是"hello world",并没有出现预期的空字符串,也就是看起来happens-before规则依然成立。但既然官方文档说无法保证,那我们开发时还是按照happens-before不成立比较好。

锁也是并发编程里非常常用的一个数据结构。go语言中支持的锁主要有两种:sync.Mutexsync.RWMutex,即普通锁和读写锁(读写锁的原理可以参见另一篇文章)。普通锁的happens-before规则也很直观:

1.对锁实例调用nUnlockhappens-before 调用Lockm次,只要n < m

请看这个例子:

var l sync.Mutex
var a string

func f() {
    a = "hello, world" // (1)
    l.Unlock() // (2)
}

func main() {
    l.Lock() // (3)
    go f() // (4)
    l.Lock() // (5)
    print(a) // (6)
}

上面调用了Unlock一次,Lock两次,因此(2) happens-before (5),从而(1) happens-before (6)

而读写锁的规则为:

2.对读写锁实例的某一次Unlock调用,happens-afterRLock调用对应的RUnlock调用happens-before下一次Lock调用。

其实本质就是读写锁的原理:读写互斥,简单地理解就是写锁释放后先获取了读锁,则读锁的释放会happens-before 下一次写锁的获取。注意上面的规则是“存在”,而不是“任意”。

Once

sync中还提供了一个Once的数据结构,用于控制并发编程中只执行一次的逻辑,例如:

var a string
var once sync.Once

func setup() {
   a = "hello, world"
   fmt.Println("set up")
}

func doprint() {
   once.Do(setup)
   fmt.Println(a)
}

func twoprint() {
    go doprint()
    go doprint()
}

会打印"hello, world"两次和"set up"一次。Oncehappens-before规则也很直观:

第一次执行Once.Dohappens-before其余的Once.Do

应用

掌握了上述的基本happens-before规则,可以结合起来分析更复杂的场景了,来看这个例子:

var a, b int

func f() {
    a = 1 // (1)
    b = 2 // (2)
}

func g() {
    print(b) // (3)
    print(a) // (4)
}

func main() {
    go f()
    g()
}

这里(1) happens-before (2),(3) happens-before(4),但是(1)与(3)、(4)之间以及(2)与(3)、(4)之间并没有happens-before关系,这时候结果是不确定的,一种有趣的结果是2、0,也就是(1)、(2)之间发生了指令重排序。现在让我们修改一下上面的代码,让它按我们预期的逻辑运行:要么打印0、0,要么打印1、2。

使用锁

var a, b int
var lock sync.Mutex

func f() {
    lock.Lock() // (1)
    a = 1 // (2)
    b = 2 // (3)
    lock.Unlock() // (4)
}

func g() {
    lock.Lock() // (5)
    print(b) // (6)
    print(a) // (7)
    lock.Unlock() // (8)
}

func main() {
    go f()
    g()
}

回想下锁的规则:

1.对锁实例调用nUnlockhappens-before 调用Lockm次,只要n < m

这里存在两种可能:要么(4) happens-before (5),要么(8) happens-before (1),会分别推导出两种结果:(6) happens-before (7) happens-before (2) happens-before (3) ,以及(2) happens-before (3) happens-before (6) happens-before (7),也就分别对应“0、0”和“1、2”两种结果。

使用通道

var a, b int
var c = make(chan int, 1)

func f() {
   <- c
   a = 1 // (2)
   b = 2 // (3)
   c <- 1
}

func g() {
   <- c
   print(b) // (6)
   print(a) // (7)
   c <- 1
}

func test() {
   wg := sync.WaitGroup{}
   wg.Add(3)
   go func(){
      defer wg.Done()
      f()
   }()
   go func(){
      defer wg.Done()
      g()
   }()
   go func(){
      defer wg.Done()
      c <- 1
   }()
   wg.Wait()
   close(c)
}

总之,如果无法确定并发读写之间的happens-before关系,那么最好使用同步工具明确它们之间的关系,例如锁或者通道。不要给程序留下不确定的可能,毕竟确定性就是编程的魅力!

查看原文

赞 10 收藏 5 评论 0

不加香菜 发布了文章 · 3月25日

go 读写锁实现原理解读

引言

锁是编程开发中用于并发控制的一种同步机制,提供多线程(或协程)之间并发读写一个共享数据的方法。在go语言中使用锁也很简单:

var loc sync.Mutex
var rwLoc sync.RWMutex
var idx int
var writeRatio = 3

func Inc(){
   loc.Lock()
   defer loc.Unlock()
   timer := time.NewTimer(100 * time.Millisecond)
   select{
   case <- timer.C:
      idx ++
   }
}

func Dec(){
   loc.Lock()
   defer loc.Unlock()
   timer := time.NewTimer(100 * time.Millisecond)
   select{
   case <- timer.C:
      idx --
   }
}

func main(){
   wg := sync.WaitGroup{}
   wg.Add(6)
   for i := 0; i < 3; i++ {
      go func() {
         defer wg.Done()
         Inc()
      }()
      go func(){
         defer wg.Done()
         Dec()
      }()
   }
   wg.Wait()
   fmt.Printf("i: %vn", idx)
}

使用标准包sync.Mutex即可标记一段临界区,保证对数据的并发读写符合预期。
注意到每次读写变量idx的时候都需要加锁,也就是任一时候只有一个goroutine允许读写该变量,而事实上如果并发执行的goroutine都是读的操作,是没有必要加锁的(因为变量的内容并没有改变),加锁是为了处理写操作的goroutine能正确同步变量的值。那么有没有不负如来不负卿的双全法呢,既能正确同步写操作,又能避免读操作的无谓加锁?事实上这就是读写锁的目的。

读写锁

简单地说,读写锁就是一种能保证:

  • 并发读操作之间不互斥;
  • 并发写操作之间互斥;
  • 并发读操作和写操作互斥;

的锁。
go语言的sync包也包含了这一数据结构,即RWMutex,使用方法与普通的锁基本相同,唯一的区别在于读操作的加锁、释放锁用的是RLock方法和RUnlock方法:

var rwLoc sync.RWMutex
var idx int

func ReadRW() {
   rwLoc.RLock()
   defer rwLoc.RUnlock()
   _, _ = fmt.Fprint(ioutil.Discard, idx)
}

func WriteRW() {
   rwLoc.Lock()
   defer rwLoc.Unlock()
   idx = 3
}

那么go是怎么实现读写锁的呢,让我们通过源码分析一下它的实现原理。

源码分析

在看源码之前我们不妨先思考一下,如果自己实现,需要怎么设计这个数据结构来满足上面那三个要求,然后再参看源码会有更多理解。
首先,为了满足第二点和第三点要求,肯定需要一个互斥锁:

type RWMutex struct{
    w Mutex // held if there are pending writers
    ...
}

这个互斥锁是在写操作时使用的:

func (rw *RWMutex) Lock(){
    ...
    rw.w.Lock()
    ...
}

func (rw *RWMutex) Unlock(){
    ...
    rw.w.Unlock()
    ...
}

而读操作之间是不互斥的,因此读操作的RLock()过程并不获取这个互斥锁。但读写之间是互斥的,那么RLock()如果不获取互斥锁又怎么能阻塞住写操作呢?go语言的实现是这样的:
通过一个int32变量记录当前正在读的goroutine数:

type RWMutex struct{
    w           Mutex // held if there are pending writers
    readerCount int32 // number of pending readers
    ...
}

每次调用Rlock方法时将readerCount加1,对应地,每次调用RUnlock方法时将readerCount减1:

func (rw *RWMutex) RLock() {
    if atomic.AddInt32(&rw.readerCount, 1) < 0 {
        // 如果readerCount小于0则通过同步原语阻塞住,否则将readerCount加1后即返回
        runtime_SemacquireMutex(&rw.readerSem, false, 0)
    }
}

func (rw *RWMutex) RUnlock() {
    if r := atomic.AddInt32(&rw.readerCount, -1); r < 0 {
    // 如果readerCount减1后小于0,则调用rUnlockSlow方法,将这个方法剥离出来是为了RUnlock可以内联,这样能进一步提升读操作时的取锁性能
        rw.rUnlockSlow(r)
    }
}

既然每次RLock时都会将readerCount增加,那判断它是否小于0有什么意义呢?这就需要和写操作的取锁过程Lock()参看:

// 总结一下Lock的流程:1. 阻塞新来的写操作;2. 阻塞新来的读操作;3. 等待之前的读操作完成;
func (rw *RWMutex) Lock() {
    // 通过rw.w.Lock阻塞其它写操作
    rw.w.Lock()
    // 将readerCount减去一个最大数(2的30次方,RWMutex能支持的最大同时读操作数),这样readerCount将变成一个小于0的很小的数,
    // 后续再调RLock方法时将会因为readerCount<0而阻塞住,这样也就阻塞住了新来的读请求
    r := atomic.AddInt32(&rw.readerCount, -rwmutexMaxReaders) + rwmutexMaxReaders
    // 等待之前的读操作完成
    if r != 0 && atomic.AddInt32(&rw.readerWait, r) != 0 {
        runtime_SemacquireMutex(&rw.writerSem, false, 0)
    }
}

写操作获取锁时通过将readerCount改成一个很小的数保证新来的读操作会因为readerCount<0而阻塞住;那之前未完成的读操作怎么处理呢?很简单,只要跟踪写操作Lock之前未完成的reader数就行了,这里通过一个int32变量readerWait来做这件事情:

type RWMutex struct{
    w           Mutex // held if there are pending writers
    readerCount int32 // number of pending readers
    readerWait  int32 // number of departing readers
    ...
}

每次写操作Lock时会将当前readerCount数量记在readerWait里。
回想一下,当写操作Lock后readerCount会小于0,这时reader unlock时会执行rUnlockSlow方法,现在可以来看它的实现过程了:

func (rw *RWMutex) rUnlockSlow(r int32) {
    if r+1 == 0 || r+1 == -rwmutexMaxReaders {
        throw("sync: RUnlock of unlocked RWMutex")
    }
    // 每个reader完成读操作后将readerWait减小1
    if atomic.AddInt32(&rw.readerWait, -1) == 0 {
        // 当readerWait为0时代表writer等待的所有reader都已经完成了,可以唤醒writer了
        runtime_Semrelease(&rw.writerSem, false, 1)
    }
}

最后再看写操作的释放锁过程:

func (rw *RWMutex) Unlock() {
    // 将readerCount置回原来的值,这样reader又可以进入了
    r := atomic.AddInt32(&rw.readerCount, rwmutexMaxReaders)
    if r >= rwmutexMaxReaders {
        throw("sync: Unlock of unlocked RWMutex")
    }
    // 唤醒那些等待的reader
    for i := 0; i < int(r); i++ {
        runtime_Semrelease(&rw.readerSem, false, 0)
    }
    // 释放互斥锁,这样新的writer可以获得锁
    rw.w.Unlock()
}

将上面这些过程梳理一下:

  1. 如果没有writer请求进来,则每个reader开始后只是将readerCount增1,完成后将readerCount减1,整个过程不阻塞,这样就做到“并发读操作之间不互斥”;
  2. 当有writer请求进来时首先通过互斥锁阻塞住新来的writer,做到“并发写操作之间互斥”;
  3. 然后将readerCount改成一个很小的值,从而阻塞住新来的reader;
  4. 记录writer进来之前未完成的reader数量,等待它们都完成后再唤醒writer;这样就做到了“并发读操作和写操作互斥”;
  5. writer结束后将readerCount置回原来的值,保证新的reader不会被阻塞,然后唤醒之前等待的reader,再将互斥锁释放,使后续writer不会被阻塞。

这就是go语言中读写锁的核心源码(简洁起见,这里将竞态部分的代码省略,TODO:竞态分析原理分析),相信看到这你已经对读写锁的实现原理了然于胸了,如果你感兴趣,不妨一起继续思考这几个问题。

思考

1. writer lock时在判断是否有未完成的reader时为什么使用r != 0 && atomic.AddInt32(&rw.readerWait, r) != 0

回想一下Lock方法:

func (rw *RWMutex) Lock() {
    rw.w.Lock()
    r := atomic.AddInt32(&rw.readerCount, -rwmutexMaxReaders) + rwmutexMaxReaders
    if r != 0 && atomic.AddInt32(&rw.readerWait, r) != 0 {
        runtime_SemacquireMutex(&rw.writerSem, false, 0)
    }
}

为了判断是否还有未完成的reader,直接判断 r!= 0不就行了吗,为什么还需要判断atomic.AddInt32(&rw.readerWait, r)!=0
这是因为上面第三行和第四行的代码并不是原子的,这就意味着中间很有可能插进其它goroutine执行,假如某个时刻执行完第三行代码,r=1,也就是此时还有一个reader,但执行第四行之前先执行了该reader的goroutine,并且reader完成RUnlock操作,此时如果只判断r!=0就会错误地阻塞住,因为这时候已经没有未完成的reader了。而reader在执行RUnlock的时候会将readerWait减1,所以readerWait+r就代表未完成的reader数。
那么只判断atomic.AddInt32(&rw.readerWait, r)!=0不就可以吗?理论上应该是可以的,先判断r!=0应该是一种短路操作:如果r==0那就不用执行atomic.AddInt32了(注意r==0时readerWait也等于0)。

Benchmark

最后让我们通过Benchmark看看读写锁的性能提升有多少:

func Read() {
   loc.Lock()
   defer loc.Unlock()
   _, _ = fmt.Fprint(ioutil.Discard, idx)
   time.Sleep(1000 * time.Nanosecond)
}

func ReadRW() {
   rwLoc.RLock()
   defer rwLoc.RUnlock()
   _, _ = fmt.Fprint(ioutil.Discard, idx)
   time.Sleep(1000 * time.Nanosecond)
}

func Write() {
   loc.Lock()
   defer loc.Unlock()
   idx = 3
   time.Sleep(1000 * time.Nanosecond)
}

func WriteRW() {
   rwLoc.Lock()
   defer rwLoc.Unlock()
   idx = 3
   time.Sleep(1000 * time.Nanosecond)
}

func BenchmarkLock(b *testing.B) {
   b.RunParallel(func(pb *testing.PB) {
      foo := 0
      for pb.Next() {
         foo++
         if foo % writeRatio == 0 {
            Write()
         } else {
            Read()
         }
      }
   })
}

func BenchmarkRWLock(b *testing.B) {
   b.RunParallel(func(pb *testing.PB) {
      foo := 0
      for pb.Next() {
         foo++
         if foo % writeRatio == 0 {
            WriteRW()
         } else {
            ReadRW()
         }
      }
   })
}

这里使用了go语言内置的Benchmark功能,执行go test -bench='Benchmark.*Lock' -run=none mutex_test.go即可触发benchmark运行,-run=none是为了跳过单测。
结果如下:

cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
BenchmarkLock
BenchmarkLock-12            235062          5025 ns/op
BenchmarkRWLock
BenchmarkRWLock-12          320209          3834 ns/op

可以看出使用读写锁后耗时降低了24%左右。
上面writeRatio用于控制读、写的频率比例,即读:写=3,随着这个比例越高耗时降低的比例也越大,这里作个简单比较:

writeRatio31020501001000
耗时降低24%71.3%83.7%90.9%93.5%95.7%

可以看出当读的比例越高时,使用读写锁获得的性能提升比例越高。

查看原文

赞 1 收藏 0 评论 0

不加香菜 关注了标签 · 3月24日

python

Python(发音:英[ˈpaɪθən],美[ˈpaɪθɑ:n]),是一种面向对象、直译式电脑编程语言,也是一种功能强大的通用型语言,已经具有近二十年的发展历史,成熟且稳定。它包含了一组完善而且容易理解的标准库,能够轻松完成很多常见的任务。它的语法非常简捷和清晰,与其它大多数程序设计语言不一样,它使用缩进来定义语句。

Python支持命令式程序设计、面向对象程序设计、函数式编程、面向切面编程、泛型编程多种编程范式。与Scheme、Ruby、Perl、Tcl等动态语言一样,Python具备垃圾回收功能,能够自动管理存储器使用。它经常被当作脚本语言用于处理系统管理任务和网络程序编写,然而它也非常适合完成各种高级任务。Python虚拟机本身几乎可以在所有的作业系统中运行。使用一些诸如py2exe、PyPy、PyInstaller之类的工具可以将Python源代码转换成可以脱离Python解释器运行的程序。

Python的主要参考实现是CPython,它是一个由社区驱动的自由软件。目前由Python软件基金会管理。基于这种语言的相关技术正在飞快的发展,用户数量快速扩大,相关的资源非常多。

关注 137879

不加香菜 关注了标签 · 3月24日

程序员

一种近几十年来出现的新物种,是工业革命的产物。英文(Programmer Monkey)是一种非常特殊的、可以从事程序开发、维护的动物。一般分为程序设计猿和程序编码猿,但两者的界限并不非常清楚,都可以进行开发、维护工作,特别是在中国,而且最重要的一点,二者都是一种非常悲剧的存在。

国外的程序员节

国外的程序员节,(英语:Programmer Day,俄语:День программи́ста)是一个俄罗斯官方节日,日期是每年的第 256(0x100) 天,也就是平年的 9 月 13 日和闰年的 9 月 12 日,选择 256 是因为它是 2 的 8 次方,比 365 少的 2 的最大幂。

1024程序员节,中国程序员节

1024是2的十次方,二进制计数的基本计量单位之一。程序员(英文Programmer)是从事程序开发、维护的专业人员。程序员就像是一个个1024,以最低调、踏实、核心的功能模块搭建起这个科技世界。1GB=1024M,而1GB与1级谐音,也有一级棒的意思。

从2012年,SegmentFault 创办开始我们就从网络上引导社区的开发者,发展成中国程序员的节日 :) 计划以后每年10月24日定义为程序员节。以一个节日的形式,向通过Coding 改变世界,也以实际行动在浮躁的世界里,固执地坚持自己对于知识、技术和创新追求的程序员们表示致敬。并于之后的最为临近的周末为程序员们举行了一个盛大的狂欢派对。

2015的10月24日,我们SegmentFault 也在5个城市同时举办黑客马拉松这个特殊的形式,聚集开发者开一个编程大爬梯。

特别推荐:

【SF 黑客马拉松】:http://segmentfault.com/hacka...
【1024程序员闯关秀】小游戏,欢迎来挑战 http://segmentfault.com/game/

  • SF 开发者交流群:206236214
  • 黑客马拉松交流群:280915731
  • 开源硬件交流群:372308136
  • Android 开发者交流群:207895295
  • iOS 开发者交流群:372279630
  • 前端开发者群:174851511

欢迎开发者加入~

交流群信息


程序员相关问题集锦:

  1. 《程序员如何选择自己的第二语言》
  2. 《如何成为一名专业的程序员?》
  3. 《如何用各种编程语言书写hello world》
  4. 《程序员们最常说的谎话是什么?》
  5. 《怎么加入一个开源项目?》
  6. 《是要精于单挑,还是要善于合作?》
  7. 《来秀一下你屎一般的代码...》
  8. 《如何区分 IT 青年的“普通/文艺/二逼”属性?》
  9. 程序员必读书籍有哪些?
  10. 你经常访问的技术社区或者技术博客(IT类)有哪些?
  11. 如何一行代码弄崩你的程序?我先来一发
  12. 编程基础指的是什么?
  13. 后端零起步:学哪一种比较好?
  14. 大家都用什么键盘写代码的?

爱因斯坦

程序猿崛起

关注 155022

不加香菜 关注了标签 · 3月24日

docker

an open source project to pack, ship and run any application as a lightweight container ! By Lock !

关注 44163

不加香菜 关注了标签 · 3月24日

java

Java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的 Java 程序设计语言和 Java 平台(即 JavaSE, JavaEE, JavaME)的总称。Java 技术具有卓越的通用性、高效性、平台移植性和安全性。

Java编程语言的风格十分接近 C++ 语言。继承了 C++ 语言面向对象技术的核心,Java舍弃了 C++ 语言中容易引起错误的指針,改以引用取代,同时卸载原 C++ 与原来运算符重载,也卸载多重继承特性,改用接口取代,增加垃圾回收器功能。在 Java SE 1.5 版本中引入了泛型编程、类型安全的枚举、不定长参数和自动装/拆箱特性。太阳微系统对 Java 语言的解释是:“Java编程语言是个简单、面向对象、分布式、解释性、健壮、安全与系统无关、可移植、高性能、多线程和动态的语言”。

版本历史

重要版本号版本代号发布日期
JDK 1.01996 年 1 月 23 日
JDK 1.11997 年 2 月 19 日
J2SE 1.2Playground1998 年 12 月 8 日
J2SE 1.3Kestrel2000 年 5 月 8 日
J2SE 1.4Merlin2002 年 2 月 6 日
J2SE 5.0 (1.5.0)Tiger2004 年 9 月 30 日
Java SE 6Mustang2006 年 11 月 11 日
Java SE 7Dolphin2011 年 7 月 28 日
Java SE 8JSR 3372014 年 3 月 18 日
最新发布的稳定版本:
Java Standard Edition 8 Update 11 (1.8.0_11) - (July 15, 2014)
Java Standard Edition 7 Update 65 (1.7.0_65) - (July 15, 2014)

更详细的版本更新查看 J2SE Code NamesJava version history 维基页面

新手帮助

不知道如何开始写你的第一个 Java 程序?查看 Oracle 的 Java 上手文档

在你遇到问题提问之前,可以先在站内搜索一下关键词,看是否已经存在你想提问的内容。

命名规范

Java 程序应遵循以下的 命名规则,以增加可读性,同时降低偶然误差的概率。遵循这些命名规范,可以让别人更容易理解你的代码。

  • 类型名(类,接口,枚举等)应以大写字母开始,同时大写化后续每个单词的首字母。例如:StringThreadLocaland NullPointerException。这就是著名的帕斯卡命名法。
  • 方法名 应该是驼峰式,即以小写字母开头,同时大写化后续每个单词的首字母。例如:indexOfprintStackTraceinterrupt
  • 字段名 同样是驼峰式,和方法名一样。
  • 常量表达式的名称static final 不可变对象)应该全大写,同时用下划线分隔每个单词。例如:YELLOWDO_NOTHING_ON_CLOSE。这个规范也适用于一个枚举类的值。然而,static final 引用的非不可变对象应该是驼峰式。

Hello World

public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello, World!");
    }
}

编译并调用:

javac -d . HelloWorld.java
java -cp . HelloWorld

Java 的源代码会被编译成可被 Java 命令执行的中间形式(用于 Java 虚拟机的字节代码指令)。

可用的 IDE

学习资源

常见的问题

下面是一些 SegmentFault 上在 Java 方面经常被人问到的问题:

(待补充)

关注 142248

不加香菜 关注了标签 · 3月24日

关注 92109

不加香菜 关注了用户 · 3月24日

码哥字节 @magebyte

公众号-码哥字节

一个有品位的博主,不跟风不扯淡,助力程序员成长。

一线大厂互联网工作经验,左手微服务、右手中间件、前脚高并发、后脚分布式。

关注 6345

不加香菜 关注了专栏 · 3月24日

疯狂的技术宅

本专栏文章首发于公众号:前端先锋 。

关注 31420

不加香菜 关注了专栏 · 3月24日

SegmentFault 行业快讯

第一时间为开发者提供行业相关的实时热点资讯

关注 63195

认证与成就

  • 获得 11 次点赞
  • 获得 0 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 0 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 3月24日
个人主页被 476 人浏览