第八天:PHP开发者快速掌握Go并发编程 | Goroutine与Channel从入门到实战

yvsm5个月前Go语言17820

作为PHP开发者,你知道实现并发需要依赖pcntl扩展创建多进程、pthreads扩展实现多线程,或借助Swoole/Workerman框架——步骤繁琐且资源消耗高。而Go内置原生并发支持:Goroutine(协程)是轻量级线程(占用KB级内存),Channel(通道)实现Goroutine间安全通信,sync.WaitGroup实现并发同步,无需第三方扩展即可高效实现并发。今天我们以PHP多进程/线程为参照,快速掌握Go并发编程的核心逻辑。

一、Goroutine(协程):对标PHP多进程/多线程

Goroutine是Go的轻量级执行单元(由Go运行时管理,非操作系统线程),创建成本极低(初始栈仅2KB),单机可轻松创建数万个Goroutine,对标PHP的多进程/线程,但性能和易用性远超后者。

1. Goroutine基础(创建与运行)

创建Goroutine只需在函数调用前加go关键字,对标PHP的pcntl_fork()(多进程)或new Thread()(多线程)。

特性 Go Goroutine PHP多进程/线程
创建方式 go 函数名(参数) 多进程:pcntl_fork();多线程:new Thread()
资源占用 KB级栈内存,轻量级 MB级内存(进程)/ 几十KB(线程),重量级
调度 Go运行时调度(M:N映射到OS线程) 操作系统调度

基础示例:Go Goroutine vs PHP多进程

// Go:创建Goroutine(并发执行任务)
func task(name string) {
    for i := 0; i < 3; i++ {
        fmt.Printf("Goroutine %s:执行第%d次\n", name, i+1)
        time.Sleep(100 * time.Millisecond) // 模拟耗时操作
    }
}

func main() {
    // 启动2个Goroutine(并发执行)
    go task("A")
    go task("B")

    // 主线程等待(否则主线程退出,Goroutine也会终止)
    time.Sleep(500 * time.Millisecond)
    fmt.Println("主线程执行完毕")
}

PHP对应写法(多进程):

// PHP:创建多进程(pcntl_fork)
function task($name) {
    for ($i = 0; $i < 3; $i++) {
        echo "进程 {$name}:执行第" . ($i+1) . "次\n";
        usleep(100000); // 模拟耗时操作(100ms)
    }
}

// 启动2个进程
$pid1 = pcntl_fork();
if ($pid1 == 0) {
    // 子进程1
    task("A");
    exit(0);
}

$pid2 = pcntl_fork();
if ($pid2 == 0) {
    // 子进程2
    task("B");
    exit(0);
}

// 主进程等待子进程结束
pcntl_waitpid($pid1, $status);
pcntl_waitpid($pid2, $status);
echo "主进程执行完毕\n";

⚠️ 核心差异:
1. Go主线程退出后,所有Goroutine会立即终止(需手动等待);PHP主进程退出后,子进程可继续运行(需手动管理);
2. Goroutine创建无需额外扩展,PHP多进程需开启pcntl扩展、多线程需开启pthreads扩展;
3. Goroutine间共享进程内存(需同步),PHP多进程间内存隔离(需IPC通信)。

2. sync.WaitGroup:Goroutine同步(对标PHP pcntl_waitpid)

手动time.Sleep等待Goroutine不灵活,Go提供sync.WaitGroup实现“等待所有Goroutine完成”,对标PHP的pcntl_waitpid批量等待子进程。

// Go:WaitGroup同步Goroutine
var wg sync.WaitGroup

func taskWithWait(name string) {
    defer wg.Done() // Goroutine完成后,计数-1(必须放在开头)
    for i := 0; i < 3; i++ {
        fmt.Printf("Goroutine %s:执行第%d次\n", name, i+1)
        time.Sleep(100 * time.Millisecond)
    }
}

func main() {
    // 设置等待计数(2个Goroutine)
    wg.Add(2)
    go taskWithWait("A")
    go taskWithWait("B")
    
    // 阻塞等待所有Goroutine完成(计数归0)
    wg.Wait()
    fmt.Println("所有Goroutine执行完毕")
}

PHP对应写法(批量等待子进程):

// PHP:批量等待子进程
function task($name) {
    for ($i = 0; $i < 3; $i++) {
        echo "进程 {$name}:执行第" . ($i+1) . "次\n";
        usleep(100000);
    }
}

$pids = [];
// 启动2个子进程
for ($i = 0; $i < 2; $i++) {
    $pid = pcntl_fork();
    if ($pid == 0) {
        task(chr(65 + $i)); // A/B
        exit(0);
    }
    $pids[] = $pid;
}

// 批量等待所有子进程
foreach ($pids as $pid) {
    pcntl_waitpid($pid, $status);
}
echo "所有子进程执行完毕\n";

二、Channel(通道):Goroutine间通信(对标PHP IPC)

Goroutine间共享内存易引发竞态条件,Go推荐用Channel(类型化通道)实现“通信顺序进程(CSP)”——通过通道安全传递数据,对标PHP的进程间通信(IPC)如管道、消息队列、共享内存。

1. Channel基础(声明与使用)

Channel需指定数据类型(如chan int),支持发送(ch<-值)接收(<-ch)关闭(close(ch))操作,分为“无缓冲通道”和“有缓冲通道”。

// Go:无缓冲Channel(同步通信)
func sendData(ch chan string) {
    // 发送数据到通道(阻塞,直到有接收方)
    ch <- "Hello Goroutine"
    close(ch) // 关闭通道(可选,告知接收方无数据)
}

func main() {
    // 声明Channel(字符串类型)
    ch := make(chan string)
    // 启动Goroutine发送数据
    go sendData(ch)
    
    // 从通道接收数据(阻塞,直到有数据)
    data := <-ch
    fmt.Println("接收数据:", data) // 输出:接收数据:Hello Goroutine

    // 检查通道是否关闭
    data, ok := <-ch
    if !ok {
        fmt.Println("通道已关闭") // 输出此内容
    }
}

PHP对应写法(管道通信):

// PHP:管道实现进程间通信
// 创建管道
$pipe = fopen("php://temp", "r+");

// 子进程发送数据
$pid = pcntl_fork();
if ($pid == 0) {
    fwrite($pipe, "Hello Process");
    fclose($pipe);
    exit(0);
}

// 主进程接收数据
pcntl_waitpid($pid, $status);
rewind($pipe);
$data = fread($pipe, 1024);
echo "接收数据:{$data}\n"; // 输出:接收数据:Hello Process
fclose($pipe);

2. 有缓冲Channel(异步通信)

有缓冲Channel指定容量(如make(chan int, 3)),发送数据时仅当缓冲区满才阻塞,对标PHP的带缓冲消息队列。

// Go:有缓冲Channel
func main() {
    // 声明有缓冲Channel(容量2)
    ch := make(chan int, 2)
    
    // 发送数据(缓冲区未满,不阻塞)
    ch <- 10
    ch <- 20
    fmt.Println("缓冲区长度:", len(ch)) // 输出2
    
    // 接收数据
    fmt.Println(<-ch) // 输出10
    fmt.Println(<-ch) // 输出20
    close(ch)
}

3. 实战:Channel实现Goroutine任务分发

// Go:Channel分发任务(并发处理任务)
func worker(id int, tasks <-chan int, results chan<- int) {
    defer wg.Done()
    for task := range tasks { // 遍历通道,直到关闭
        fmt.Printf("工作协程%d:处理任务%d\n", id, task)
        results <- task * 2 // 处理结果发送到结果通道
        time.Sleep(100 * time.Millisecond)
    }
}

var wg sync.WaitGroup

func main() {
    // 任务通道和结果通道
    tasks := make(chan int, 5)
    results := make(chan int, 5)

    // 启动3个工作协程
    wg.Add(3)
    for i := 1; i <= 3; i++ {
        go worker(i, tasks, results)
    }

    // 发送5个任务
    for i := 1; i <= 5; i++ {
        tasks <- i
    }
    close(tasks) // 关闭任务通道(告知协程无新任务)

    // 等待所有协程完成
    wg.Wait()
    close(results) // 关闭结果通道

    // 输出结果
    fmt.Println("处理结果:")
    for res := range results {
        fmt.Println(res)
    }
}

💡 核心知识点:
1. Channel是“类型安全”的,只能传递指定类型数据;
2. 无缓冲Channel是“同步通信”(发送/接收阻塞),有缓冲Channel是“异步通信”;
3. 用for range遍历Channel时,需关闭Channel否则会阻塞;
4. Go的select可监听多个Channel,对标PHP的stream_select

三、实战案例:PHP vs Go 并发处理批量任务

需求:并发处理10个计算任务(计算数字平方),收集所有结果并输出

Go写法(Goroutine+Channel+WaitGroup)

package main

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

var wg sync.WaitGroup

// 计算平方的工作协程
func squareWorker(id int, num int, resChan chan<- int) {
    defer wg.Done()
    time.Sleep(50 * time.Millisecond) // 模拟耗时
    res := num * num
    fmt.Printf("协程%d:计算%d的平方=%d\n", id, num, res)
    resChan <- res
}

func main() {
    nums := []int{1,2,3,4,5,6,7,8,9,10}
    resChan := make(chan int, len(nums))

    // 启动协程处理每个任务
    for i, num := range nums {
        wg.Add(1)
        go squareWorker(i+1, num, resChan)
    }

    // 等待所有协程完成,关闭结果通道
    go func() {
        wg.Wait()
        close(resChan)
    }()

    // 收集结果
    total := 0
    for res := range resChan {
        total += res
    }

    fmt.Printf("所有任务完成,结果总和:%d\n", total) // 输出385
}

PHP写法(多进程处理)

// PHP:多进程处理批量任务
function squareWorker($id, $num, $pipe) {
    usleep(50000); // 模拟耗时
    $res = $num * $num;
    echo "进程{$id}:计算{$num}的平方={$res}\n";
    fwrite($pipe, $res . "\n");
    exit(0);
}

// 创建管道收集结果
$pipe = fopen("php://temp", "r+");
$pids = [];
$nums = [1,2,3,4,5,6,7,8,9,10];

// 启动子进程处理任务
foreach ($nums as $i => $num) {
    $pid = pcntl_fork();
    if ($pid == 0) {
        squareWorker($i+1, $num, $pipe);
    }
    $pids[] = $pid;
}

// 等待所有子进程
foreach ($pids as $pid) {
    pcntl_waitpid($pid, $status);
}

// 收集并计算总和
rewind($pipe);
$total = 0;
while (($res = fgets($pipe)) !== false) {
    $total += intval(trim($res));
}
fclose($pipe);

echo "所有任务完成,结果总和:{$total}\n"; // 输出385

五、今日小结

今天我们以PHP多进程/线程为参照,掌握了Go并发编程的核心:

  1. Goroutine是轻量级协程,go关键字快速创建,对标PHP多进程/线程但更轻量、高效;
  2. sync.WaitGroup实现Goroutine同步,对标PHP的pcntl_waitpid批量等待子进程;
  3. Channel是Goroutine间安全通信的核心,分为无缓冲(同步)和有缓冲(异步),对标PHP的IPC通信;
  4. Go并发遵循“不要通过共享内存通信,要通过通信共享内存”,避免竞态条件;
  5. Go原生支持并发,无需第三方扩展,开发效率远高于PHP的多进程/线程。

明天我们将学习Go项目实战(模块化、依赖管理、简单Web服务),对比PHP的Composer、Web开发,详解如何从0搭建一个Go基础Web应用,完成从语法到实战的落地。

相关文章

第六天:PHP开发者快速掌握Go结构体与接口 | 从PHP类/接口到Go面向对象

作为PHP开发者,你熟悉用class定义类、interface定义接口,通过实例化对象实现面向对象编程。Go没有“类(class)”的概念,但通过结构体(struct)实现类的属性封装,通过方法(me...

第七天:PHP开发者快速掌握Go错误处理与文件操作 | 从PHP异常/文件函数到Go实战

作为PHP开发者,你熟悉用try/catch处理异常、file_get_contents/file_put_contents读写文件、mkdir创建目录。Go没有“异常(Exception)”的概念,...

第九天:PHP开发者快速掌握Go项目实战 | 模块化、依赖管理与Web服务开发

作为PHP开发者,你熟悉用命名空间组织代码、Composer管理依赖、PHP-FPM+原生/框架(如Laravel/ThinkPHP)开发Web服务。Go从1.11版本开始引入Go Modules实现...

第五天:PHP开发者快速掌握Go数组、切片与Map | 从PHP数组到Go复合类型的转换

作为PHP开发者,你早已习惯了“万能数组”——既可以当索引数组用,也可以当关联数组用,长度动态变化、元素类型不限。而Go没有“万能数组”,而是将复合数据类型拆分为数组(array)、切...

第二天:PHP 开发者快速掌握 Go 变量与数据类型

作为PHP开发者,你早已习惯了“无需声明类型、变量随用随定义”的弱类型特性,而Go作为强类型语言,变量必须显式声明类型(或通过推导确定)。今天我们就以PHP的变量/数据类型为参照,快速掌握Go的变量声...

第三天:PHP 开发者快速掌握 Go 运算符与流程控制

作为PHP开发者,你早已熟悉if/else、for、foreach等流程控制语法,以及+/-/*/、==/===等运算符。Go的运算符和流程控制语法与PHP高度相似,但也有不少“细节差异”——比如Go...

发表评论    

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。