天天看點

協程(使用者态線程)協程(使用者态線程)

協程(使用者态線程)

文章目錄

  • 協程(使用者态線程)
    • 協程
    • 對比線程
    • 多線程一定快嗎?
    • 并發和上下文切換
    • 協程的藝術
    • 示範
    • 總結

協程

首先什麼是協程?有人認為程序下有線程,線程管理着協程.其實這并不對

協程是一種使用者态線程.它比線程更加輕量并且協程對于作業系統是并不可見的.

也就是說作業系統看不見協程

同一時刻一個CPU隻會執行一個協程.

比如交給協程去執行的代碼你可以了解為一個個送出的任務

對比線程

那什麼是線程?

線程是程序的一個實體,是CPU排程和分派的基本機關.線程是程式中一個單一的順序控制流程。程序内有一個相對獨立的、可排程的執行單元,是系統獨立排程和分派CPU的基本機關指令運作時的程式的排程機關。

建立一個線程每個線程都有自己的TCB都有自己的堆棧.建立一個線程是有很大的花銷的

也就是說一台伺服器的資源總是有限的,你不可能無休止的去開線程(總有一天你會把記憶體占完)

預設建立一個線程的大小是1MB(取決于實作)也就是說4G的記憶體 最多隻能建立4096個

但是很多時候我們建立的一個線程用不了這麼多記憶體空間

線程的平均記憶體空間使用率是很低的

那可能有很多人寫過這樣的一個程式. 是一個基于Socket聊天的程式.大部分人是這樣做的來一個Socket以後為其開一個線程工作.這樣小程式的時候貌似沒有什麼問題

但是隻要程式一大可能計算機資源就被配置設定光了. 更好的做法應該是像Reactor模式一樣(有興趣的可以去看一下).使用IO多路複用.反應器模式的效率會高很多

多線程一定快嗎?

有的人一說到多線程就覺得多線程開越多程式用行越好.其實并不是的

多線程的建立和銷毀都有一定的開銷(更好的方式是線程池和任務隊列)

有時候多線程并不如串行化快(一會說).

确實有些場景下多線程會比單線程快 比如:傳回一個頁面,這個頁面可能是圖文并茂.你經常看到的一個場景是文字先全部出來然後是圖檔加載出來.因為文字是很快的,而有時候圖檔是很大的.如果按順序加載可能會造成一些不太友好的界面

再比如你使用歸并排序或者Dijkstra的求最短路徑算法.歸并排序和Dijkstra是可以分開成一個個子任務的,每一個線程去執行一個個子任務彼此都是分開的不會産生共享不會産生并發的問題.這樣的問題 如果你有很多的資料的話那麼使用多線程的效率可能會高一些,但是資料比較小往往來說得到的效果就會很差

而且你有時候可能想嘗試使用多線程去優化你的程式,你會發現還不如不使用呢

并發和上下文切換

現代OS可以并發和上下文切換密不可分

在作業系統中,CPU切換到另一個程序需要儲存目前程序的狀态并恢複另一個程序的狀态:目前運作任務轉為就緒(或者挂起、删除)狀态,另一個被標明的就緒任務成為目前任務。上下文切換包括儲存目前任務的運作環境,恢複将要運作任務的運作環境。

也就是說當你cpu配置設定給每個線程一些時間,當時間片使用完了就會切換到其他線程這一點也是會使得你多線程效率差的原因

當你程序特别多的時候頻繁的上下文切換會浪費大量的時間.

協程的藝術

上文說到了協程是一種更輕量級的線程.為什麼是更輕量級的? 因為協程建立的代價很小 線程比協程的建立需要的資源大了成百上千倍. 那這是怎麼做到的呢? 協程隻需要很小很小的空間. 其他的東西都是多個協程共享的.

這個時候可能會有人問了 共享的不就會出現并發問題了嗎?不是的協程對cpu并不可見. 也就是cpu是看不見協程的

同一時刻一個cpu隻會運作一個協程任務那麼這樣是不是就保證了安全性?

那為了更好的明白同一時刻cpu隻執行一個協程 cpu對協程不可見的會用代碼示範一下

示範

那麼這裡采用Golang語言示範.看不懂也沒關系 你隻需要明白輸出,為什麼這樣輸出就可以

那麼我在這裡說一下Golang語言的好處, Golang語言是最契合現代作業系統的語言.它真的非常棒.簡單 易上手 高效 工具強大背景強大絕對是它的優勢. 而且它那極具藝術的并發程式設計和接口才是最吸引我的. 可能就是因為Golang的并發使得它真的是一門非常适合網絡程式設計的語言

下面這段代碼很簡單

主函數在循環列印0-1000 那麼 worker函數在循環列印0-1000 後面還會帶個abc

協程(使用者态線程)協程(使用者态線程)
package main

import (
	"fmt"
)

func main() {
	str := "abc"
	//建立一個管道可以使worker函數比main函數晚結束
	//在java中main函數會等待其他線程結束才會結束 在Golang中不會
	ch := make(chan int)
	//go 是go語言的一個關鍵字,是使用一個協程執行worker函數
	go worker(ch, str)
	for i := 0; i < 1000; i++ {
		fmt.Println(i)
	}
	//這裡會阻塞等待有人往管道裡寫資料
	<- ch
}

func worker(channel  chan int, str string)  {
	for i := 0; i < 1000; i++ {
		fmt.Println(i,str)
	}
	channel <- 1
}
           

下面是我的一個執行結果片段

協程(使用者态線程)協程(使用者态線程)

可以看到這是并發執行的

協程(使用者态線程)協程(使用者态線程)

那麼有人可能問這和多線程有什麼差別

你别急因為這裡有多核CPU的存在 可能4核CPU4個協程就可以并發的工作

接下來加一行代碼 把CPU的核數改成1

協程(使用者态線程)協程(使用者态線程)

接下來是結果的一個執行片段

協程(使用者态線程)協程(使用者态線程)
協程(使用者态線程)協程(使用者态線程)

我來解釋一下 這個時候隻會有一個任務在執行 可以先到main這個任務先執行到 <-ch 這個管道阻塞等待别人寫資料

這個時候就可以切換到另外一個協程執行完畢後程式才退出

如果下面這個程式 更改worker函數那麼程式永遠卡死

協程(使用者态線程)協程(使用者态線程)

程式永遠死循環

協程(使用者态線程)協程(使用者态線程)

下面程式将CPU的核數改成2

協程(使用者态線程)協程(使用者态線程)

那麼下面部分輸出結果

協程(使用者态線程)協程(使用者态線程)
協程(使用者态線程)協程(使用者态線程)

并且程式正常退出了

如果将main函數的最後一行注釋掉 并且CPU的核數改為1 那麼worker函數并沒有進入

協程(使用者态線程)協程(使用者态線程)
協程(使用者态線程)協程(使用者态線程)

這就是cpu同一時刻隻會執行一個協程

總結

那麼為什麼會有協程?

協程更加輕量(所需要的記憶體空間更小)

協程是使用者态的 這也就是它為什麼叫使用者态的線程.核心态并不知道它的存在,也就是說沒有上下文切換

使得協程變得效率更高