go语言为什么我的程序不会执行子协程

go语言为什么我的程序不会执行子协程,第1张

go的main方法理解为入口函数程序只执行这一个函数。整个项目由这个函数调度使用。

所以你的协程没有被运行。

你将协程函数命名,在main函数中调用 go test()即可

由于python是一种解释性脚本语言,python的多线程在运行过程中始终存在全局线程锁。

简单的来说就是在实际的运行过程中,python只能利用一个线程,因此python的多线程并不达到C语言多线程的性能。

可以使用多进程来代替多线程,但需要注意的是多进程最好不要涉及到例如文件 *** 作的频繁 *** 作IO的功能。

什么协程

协程这个概念在计算机科学里算是一个老概念了,随着现代计算机语言与多核心处理器的普及,似乎也有普及之势。协程是与例程相对而言的。

熟悉C/C++语言的人都知道,一个例程也就是一个函数。当我们调用一个函数时,执行流程进入函数;当函数执行完成后,执行流程返回给上层函数或例程。期间,每个函数执行共享一个线程栈;函数返回后栈顶的内容自动回收。这就是例程的特点,也是现代 *** 作系统都支持这种例程方式。

协程与例程相对,从抽象的角度来说,例程只能进入一次并返回一次,而协程可能进入多次并返回多次。比如说,我们有下面一段程序:

void fun(int val)

{

int a=0; //1

int b=0; //2

int c=a+b; //3

}

如果上面的代码是一个例程,那么它只能把 1、2、3 依次执行后,才返回。如果是协程,它可能在 1 处暂停,然后在某个时刻从 2 处继续执行;接着在 2 处执行完之后暂停,然后在另外一个时刻从 3 处继续执行。

从抽象角度,协程就这么简单。

异步IO的特点与分析

在了解协程的特点(可以多次进入同一个函数,并接着上次运行处继续执行)后,我们再来考虑一下,这一特点如何应用到异步IO程序中。在异步IO程序中,有很大一块代码是处理异步回调的,也就是数据读取或写入由系统执行,当任务完成后,系统会执行用户的回调。如果只是很少使用这种回调,那么程序并不会因为异步而复杂多少,但要是程序中异步回调大量存在,那么此时我们会发现,原本简单的程序可能因为回调而变得支离破碎,原本一个简单的循环,现在需要写入多个函数,并在多个函数里来回调用。下面示例一下:

//下面代码片断是同步代码,它从IO读一段数据,并把这段数据写回

void start()

{

for(;;)

{

Buffer buf;

read (buf);//把书读到buf

write(buf);//把buf的数据写回

}

//注意到没有,同步代码很简单直接,一个循环,几行代码完成全部事务

}

//把上面的同步代码映射为异步,代码量可能要增加很多,并且程序逻辑也变得不清晰

//示例如下

//读回调,在回调里我们发起写 *** 作

void readHandle(buf)

{

writeAsync(buf, writeHandle);

}

//写回调,在回调里我们发起读 *** 作

void writeHandle(buf)

{

readAsync(buf, readHandle);

}

//开始循环

void start()

{

static Buffer buf; //buf变量不能在栈上,为了简单这里写成静态变量

readAsync(buf, readHandle);

}

从上面的代码比较中,我们可以看出异步IO会把代码分隔成许多碎片,同时原本清晰的处理逻辑也因为被放入多个函数里,而变得很不清晰。上面的同步代码,一个了解程序的初级程序员也可以读懂写出,但相同功能的异步代码,一个初级程序员可能就搞不定了,甚至很难搞明白为什么要这么做。

读到这里,对异步不是太了解的人可能会问,既然异步把问题搞复杂了,那我们为什么还要用异步呢?答案简单有力,为了“性能”。只有这一个原因,当程序需要处理大量IO时,异步的效率将高出同步代码许多倍。如何一个程序的性能不其关心部分,那真不应该使用异步IO。

对比我们的异步IO代码与其功能相同的同步代码,我们发现每个异步调用都是要把代码分隔一个小函数——比原本要小的函数,当异步调用返回后,我们又接着下面处理。这一点跟协程很像,在一个协程里,当发起异步IO时,我让它返回,当异步IO完成后,我让这个协程接着执行,处理余下的逻辑。

协程与异步结合——性能与简单的结合

结合上面的分析,如果我们可以写下面功能的代码,将很完美:

void start()

{

for(;;)

{

Buffer buf;

yeild readAsync(buf,start);

//------ 分隔线,协程在这里返回,等待readAsync完成,当readAsync完成后,它再调用start

//此时start将从这里接着运行

yeild writeAsync(buf, start);

//------ 分隔线,协程在这里返回,等待writeAsync完成,当writeAsync完成后,它再调用start

//此时start将从这里接着运行

}

}

上面的代码也很一清晰明了。如果真的能写这样的代码,那将是所有程序员之福。这样在一些语言里确实可以直接写出来,比如Python、Lua、 golang,这些语言有原生的协程支持——编译器或解释器为我们处理了这些执行流的跳转。那么在C/C++里怎么实现呢?可能肯定的是,C/C++里不能直接写出这样简洁的代码,但却可以写类似的代码,尤其是C++——它提供了更强大的封装能力。

C10k是一个在1999年被提出来的技术挑战,如何在一颗1GHz CPU,2G内存,1gbps网络环境下,让单台服务器同时为1万个客户端提供FTP服务

阻塞式I/O(使用最多)、非阻塞式I/O、I/O复用、信号驱动式I/O(几乎不使用)、异步I/O(POSIX的aio_系列函数)

select、poll、epoll都是IO多路复用的机制。I/O多路复用就是通过一种机制,一个进程可以监听多个描述符,一旦,某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写 *** 作。但select、poll、epoll本质上都是同步I/O,因为他们都需要在读写时间就绪后负责进行读写,也就是说读写过程是阻塞的,而异步I/O无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间

(1)select

select函数监视的文件描述符分3类,分别是writefds、readfds、exceptfds。调用select函数会阻塞,直到有描述符就绪(有数据可读、可写或者有except),或者超时函数返回。当select函数返回后可以通过遍历fdset来找到就绪的描述符。

select目前几乎在所有的平台上支持,其良好的跨平台支持也是它的一个优点。select的一个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,可以通过修改宏定义甚至重新编译内核的方式提升这一限制,但是这样也会降低效率。

(2)poll

不同于select使用三个位图来表示三个fdset的方式,poll使用一个pollfd的指针实现。

pollfd结构包含了要监视的event和发生的event,不再使用select"参数-值"传递的方式。同时pollfd并没有最大数量限制(但是数量过大后性能也会下降)。和select函数一样,poll返回后,需要轮询pollfd来获取就绪的描述符。

从上面看,select和poll都需要在返回后通过遍历文件描述符来获取已经就绪的socket。事实上同时连接的大量客户端在同一时刻可能只有很少的处于就绪的状态,因此随着监视的描述符数量的增长,其效率也会线性下降

(3)epoll

epoll是在26内核中提出的,是之前的select和poll的增强版本。相对于select和poll来说,epoll更加领灵活,没有描述符限制。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。

在并发程序中,由于超时、取消 *** 作或其他一些异常情况,往往需要通知其他goroutine,虽然可以使用channel来处理这些问题,但是会变得非常繁琐,而且不利于多级管理。

go使用Context来做解决方案。

Context接口包含4个方法

emptyCtx其实就是一个int类型的变量,实现了Context接口。

如其名,就是一个没有设置超时时间,不能取消,也不能存储键值对的Context。

emptyCtx用来作为context的根结点。

而我们通常不会直接使用emptyCtx,而是使用emptyCtx实例化的两个变量

valueCtx利用了context类型的变量来表示父节点context,继承了父context的所有信息。

valueCtx携带了一个键值对,实现了Value方法,所以可以在context上获取key对应的值,如果context不存在,会沿着父context向上查找

向context中添加键值对,并不是直接在原context上直接添加,而是创建一个新的valueCtx,将键值对添加在子节点上。

和valueCtx类似,也有父context,

newCancelCtx只是初始化了cancelCtx

propagateCancel建立当前节点与父节点的取消逻辑

1、如果父context已经取消了,就直接返回,因为父节点不可能再被取消了

2、监听信号done,如果接收到了就通知子context取消

3、如果找到父context,就挂在父context上

4、如果没有找到父context,也就是自身是根context,就启动一个goroutine监听信号

而调用的cancel方法,其实就是关闭通道及设置原因

timer声明了一个定时器,用于发送截止时间

而timerCtxcancel有些不一样,是关闭定时器的。

关于timerCtx还有一个方法

与WithDeadline类似,只不过是创建了一个过期时间的context

协同程序(coroutine)与多线程情况下的线程比较类似:有自己的堆栈,自己的局部变量,有自己的指令指针(IP,instruction pointer),但与其它协同程序共享全局变量等很多信息。线程和协同程序的主要不同在于:在多处理器情况下,从概念上来讲多线程程序同时运行多个线程;而协同程序是通过协作来完成,在任一指定时刻只有一个协同程序在运行,并且这个正在运行的协同程序只在必要时才会被挂起。

以上就是关于go语言为什么我的程序不会执行子协程全部的内容,包括:go语言为什么我的程序不会执行子协程、Python怎么多线程中添加协程、协程与函数线程异步的关系等相关内容解答,如果想了解更多相关内容,可以关注我们,你们的支持是我们更新的动力!

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/zz/9841872.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2023-05-02
下一篇 2023-05-02

发表评论

登录后才能评论

评论列表(0条)

保存