一、netcore跨平台之 Linux上部署netcore和webapi

這幾天閑着的時候在linux上部署了一下netcore webapi,下面就紀要一下這個過程。

中間遇到不少的坑,心裏都是淚啊。

 話不多說,開始幹活。

————————————————————————

第一步,你得先創建一個netcore的接口,這個我就簡單創建一個接口。

關於開發工具,我用的是vs2017,當然最新的vs2019也出來了,你可以用新的,都沒關係。

開始選擇創建項目,如圖所示,這個入門的程序員都應該懂

 

 

 選擇API

 

 

 點擊確定按鈕就創建成功。

如圖打開 Program.cs 

 

 

 在這裏添加一段代碼

 

代碼添加后

 

 

 這樣代碼就寫好了。

接下來就是發布。

 

選擇文件夾,選擇你要發布的項目的位置。

 

 

 點擊高級配置如下,注意下,這裏的目標框架是2.2版本,所以我們在linux上安裝的也是2.2。

 這裏我就遇到過坑,我vs發布的是2.0的版本,結果我linux是2.2,就各種運行報錯,後來改成2.2就好了。

 

 最後保存併發布就好了。

netcore項目的創建和發布就這樣結束了。

第二步,你得準備一個linux服務器,然後安裝環境

如果你條件允許,可以直接在阿里雲或者騰訊雲、華為雲、百度雲上買一個服務器。

新用戶是白菜價哦,(這裏真不是打廣告)當然你可以在你電腦上安裝一個VMware虛擬機。

具體安裝步驟百度一下一大把,這裏就不演示了。

我就在在百度雲買了一個linux服務器,嗯,價格還算便宜,畢竟新用戶,為什麼用百度雲??

當然不是因為他好,而是我阿里雲已經不是新用戶了

好了,我們繼續。

用xshell登錄到你的linux服務器上。(如果不懂linux,沒關係,你總會百度吧)

登錄成功后,你可以在直接輸入如下命令

sudo yum install dotnet-sdk-2.2

 

 點擊確認,你需要等一段時間,如果你服務器網速很差,那麼你可以會等很久。

 如下圖示,遇到這裏你需要點擊敲一下你的鍵盤上的 y 回車即可

 這個時候系統開始慢慢的下載了,請耐心等待即可。

 

 

 終於下載完成了

 

你可以輸入下面的命令看看是否成功

dotnet --version

显示如下,表示按照成功

 

 

 

然後我們把發布包上傳到服務器上來

我這裏用的是xftp工具,當然也有其他工具可,下圖所示是我安裝的兩個工具,大家可以去下載安裝。

 

 

這裏給大家提供一些我在網盤保存的一些工具

Xshell+Xftp真正破解版    https://pan.baidu.com/s/1Ew1XPg11sakpc8mvK6QsHg 

 打開xftp並連接到服務器,如下所示

 

 

 

我這裏用的root權限,這裏進來就直接就是root根目錄了

然後右鍵點擊創建一個目錄用來保存你上傳的netcore文件,嗯,就取名netcore吧

 

 

 

 

 

 

 然後在左邊找到你剛剛發布的那個包的位置,並且點擊右邊的netcore進入到對應的目錄中

 

 

 

然後全選左邊的所有文件,並右鍵然後點擊傳輸,如圖所示

 

 

然後文件就開始傳輸了

 

 

 

 

 等下面的傳輸沒有記錄了,那麼恭喜你,你傳遞完成了。

 

 有人可能會問為啥這麼多文件,我vs2017本來沒有2.2版本,後來我在本機安裝了2.2,結果發布后就這麼多……

然後我們再次回到xshell看看

輸入命令 ll 可以看到我們新加了一個文件 netcore

 

 然後輸入命令cd  進入到我們上傳的這個發布包中。

然後在輸入命令 ll

cd netcore
ll

我們可以找到WebApiTest.dll文件

 

 然後輸入命令

dotnet WebApiTest.dll

如圖所示,就恭喜你你的項目運行正常。

 

 然後你Ctrl+C結束掉這個程序,這裏只能在服務器內部訪問,外面是不能訪問的。

 然後輸入如下命令

dotnet WebApiTest.dll   --server.urls "http://*:6666"

如圖所示

 

 然後用postman或者一些在線工具訪問這個接口,如圖所示,那麼恭喜你成功了

這個測試工具的網站:https://www.sojson.com/httpRequest/

 

 

 好了,到這裏netcore在linux上配置就基本完成了,接下來下一篇我們開始講nginx的配置,以及讓netcore運行在nginx下。

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※專營大陸空運台灣貨物推薦

台灣空運大陸一條龍服務

※評比彰化搬家公司費用,南投搬家公司費用收費行情懶人包大公開

彰化搬家費用,南投搬家費用,距離,噸數怎麼算?達人教你簡易估價知識!

用這個庫 3 分鐘實現讓你滿意的表格功能:Bootstrap-Table

本文作者:HelloGitHub-kalifun

這是 HelloGitHub 推出的系列,今天給大家推薦一個基於 Bootstrap 和 jQuery 的表格插件:Bootstrap-Table

一、介紹

從項目名稱就可以知道,這是一款 Bootstrap 的表格插件。表格的展示的形式所有的前端幾乎在工作中都有涉及過,Bootstrap Table 提供了快速的建表、查詢、分頁、排序等一系列功能。

項目地址:https://github.com/wenzhixin/bootstrap-table

可能 Bootstrap 和 jQuery 技術有些過時了,但如果因為歷史的技術選型或者舊的項目還在用這兩個庫的話,那這個項目一定會讓你的嘴角慢慢上揚,拿下錶格展示方面的需求易如反掌!

二、模式

Boostatrp Table 分為兩種模式:客戶端(client)模式、服務端(server)模式。

  • 客戶端:通過數據接口將服務器需要加載的數據一次性展現出來,然後裝換成 json 然後生成 table。我們可以自己定義显示行數,分頁等,此時就不再會向服務器發送請求了。

  • 服務器:根據設定的每頁記錄數和當前显示頁,發送數據到服務器進行查詢。

三、實戰操作

Tips: 解釋說明均在代碼中以註釋方式展示,請大家注意閱讀。

我們採用的是最簡單的 CDN 引入方式,代碼可直接運行。複製代碼並將配置好 json 文件的路徑即可看到效果。

3.1 快速上手

註釋中的星號表示該參數必寫,話不多說上代碼。示例代碼:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Hello, Bootstrap Table!</title>
    // 引入 css
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.2.1/css/bootstrap.min.css" integrity="sha384-GJzZqFGwb1QTTN6wy59ffF1BuGJpLSa9DkKMp0DgiMDm4iYMj70gZWKYbI706tWS" crossorigin="anonymous">
    <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.6.3/css/all.css" integrity="sha384-UHRtZLI+pbxtHCWp1t77Bi1L4ZtiqrqD80Kn4Z8NTSRyMA2Fd33n5dQ8lWUE00s/" crossorigin="anonymous">
    <link rel="stylesheet" href="https://unpkg.com/bootstrap-table@1.15.3/dist/bootstrap-table.min.css">
</head>
<body>
    // 需要填充的表格
    <table id="tb_departments" data-filter-control="true" data-show-columns="true"></table>
// 引入js
<script src="https://code.jquery.com/jquery-3.3.1.min.js" integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.6/umd/popper.min.js" integrity="sha384-wHAiFfRlMFy6i5SRaxvfOCifBUQy1xHdJ/yoi7FRNXMRBu5WHdZYu1hA6ZOblgut" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.2.1/js/bootstrap.min.js" integrity="sha384-B0UglyR+jN6CkvvICOB2joaf5I4l3gm9GU6Hc1og6Ls7i6U/mkkaduKaBhlAXv9k" crossorigin="anonymous"></script>
<script src="https://unpkg.com/bootstrap-table@1.15.3/dist/bootstrap-table.min.js"></script>
<script>
        window.operateEvents = {
            // 當點擊 class=delete 時觸發
            'click .delete': function (e,value,row,index) {
                // 在 console 打印出整行數據
                console.log(row);
            }
        };

        $('#tb_departments').bootstrapTable({
            url: '/frontend/bootstrap-table/user.json',         //請求後台的 URL(*)
            method: 'get',                      //請求方式(*)
            // data: data,                      //當不使用上面的後台請求時,使用data來接收數據
            toolbar: '#toolbar',                //工具按鈕用哪個容器
            striped: true,                      //是否显示行間隔色
            cache: false,                       //是否使用緩存,默認為 true,所以一般情況下需要設置一下這個屬性(*)
            pagination: true,                   //是否显示分頁(*)
            sortable: false,                    //是否啟用排序
            sortOrder: "asc",                   //排序方式
            sidePagination: "client",           //分頁方式:client 客戶端分頁,server 服務端分頁(*)
            pageNumber:1,                       //初始化加載第一頁,默認第一頁
            pageSize: 6,                        //每頁的記錄行數(*)
            pageList: [10, 25, 50, 100],        //可供選擇的每頁的行數(*)
            search: true,                       //是否顯示錶格搜索,此搜索是客戶端搜索,不會進服務端,所以個人感覺意義不大
            strictSearch: true,                 //啟用嚴格搜索。禁用比較檢查。
            showColumns: true,                  //是否显示所有的列
            showRefresh: true,                  //是否显示刷新按鈕
            minimumCountColumns: 2,             //最少允許的列數
            clickToSelect: true,                //是否啟用點擊選中行
            height: 500,                        //行高,如果沒有設置 height 屬性,表格自動根據記錄條數覺得表格高度
            uniqueId: "ID",                     //每一行的唯一標識,一般為主鍵列
            showToggle:true,                    //是否显示詳細視圖和列表視圖的切換按鈕
            cardView: false,                    //是否显示詳細視圖
            detailView: false,                  //是否显示父子表
            showExport: true,                   //是否显示導出
            exportDataType: "basic",            //basic', 'all', 'selected'.
            columns: [{
                checkbox: true     //複選框標題,就是我們看到可以通過複選框選擇整行。
            }, {
                field: 'id', title: 'ID'       //我們取json中id的值,並將表頭title設置為ID
            }, {
                field: 'username', title: '用戶名'         //我們取 json 中 username 的值,並將表頭 title 設置為用戶名
            },{
                field: 'sex', title: '性別'                //我們取 json 中 sex 的值,並將表頭 title 設置為性別
            },{
                field: 'city', title: '城市'               //我們取 json 中 city 的值,並將表頭 title 設置為城市
            },{
                field: 'sign', title: '簽名'               //我們取 json 中 sign 的值,並將表頭 title 設置為簽名
            },{
                field: 'classify', title: '分類'           //我們取 json 中 classify 的值,並將表頭 title 設置為分類
            },{
                //ormatter:function(value,row,index) 對後台傳入數據 進行操作 對數據重新賦值 返回 return 到前台
                // events 觸發事件
                field: 'Button',title:"操作",align: 'center',events:operateEvents,formatter:function(value,row,index){
                    var del = '<button type="button" class="btn btn-danger delete">刪除</button>'
                    return del;
                }
            }
            ],
            responseHandler: function (res) {
                return res.data      //在加載遠程數據之前,處理響應數據格式.
                // 我們取的值在data字段中,所以需要先進行處理,這樣才能獲取我們想要的結果
            }
        });
</script>
</body>
</html>

上面的代碼展示通過基本 API 實現基礎的功能,示例代碼並沒有羅列所有的 API。該庫還有很多好玩的功能等着大家去發現,正所謂師父領進門修行靠個人~

3.2 拆解講解

下面對關鍵點進行闡述,為了更方便使用的小夥伴清楚插件的用法。

3.2.1 初始化部分

選擇需要初始化表格。
$('#tb_departments').bootstrapTable({})
這個就像table的入口一樣。
<table id="tb_departments" data-filter-control="true" data-show-columns="true"></table>

3.2.2 閱讀數據部分

columns:[{field: 'Key', title: '文件路徑',formatter: function(value,row,index){} }]
  • field json 中鍵值對中的 Key
  • title 是表格頭显示的內容
  • formatter 是一個函數類型,當我們對數據內容需要修改時會用它。例:編碼轉換

3.2.3 事件觸發器

events:operateEvents
 window.operateEvents = {
        'click .download': function (e,value,row,index) {
            console.log(row);
        }
   }

因為很多時候我們需要針對錶格進行處理,所以事件觸發器是一個不錯的選擇。比如:它可以記錄我們的行數據,可以利用觸發器進行定製函數的執行等。

四、擴展

介紹幾個擴展可以讓我們便捷的實現更多的表格功能,而不需要自己造輪子讓我們的工作更加高效(也可以進入官網查看擴展的具體使用方法,官方已經收集了大量的擴展)。老規矩直接上代碼:

4.1 表格導出

<script src="js/bootstrap-table-export.js"></script> 
showExport: true,                                           //是否显示導出
exportDataType: basic,                                      //導出數據類型,支持:'基本','全部','選中'
exportTypes:['json', 'xml', 'csv', 'txt', 'sql', 'excel']   //導出類型

4.2 自動刷新

<script src="extensions/auto-refresh/bootstrap-table-auto-refresh.js"></script>
autoRefresh: true,                              //設置 true 為啟用自動刷新插件。這並不意味着啟用自動刷新
autoRefreshStatus: true,                        //設置 true 為啟用自動刷新。這是表加載時狀態自動刷新
autoRefreshInterval: 60,                        //每次發生自動刷新的時間(以秒為單位)
autoRefreshSilent: true                         //設置為靜默自動刷新

4.3 複製行

<script src="extensions/copy-rows/bootstrap-table-copy-rows.js"></script>
showCopyRows: true,                                 //設置 true 為显示複製按鈕。此按鈕將所選行的內容複製到剪貼板
copyWithHidden: true,                               //設置 true 為使用隱藏列進行複製
copyDelimiter: ', ',                                //複製時,此分隔符將插入列值之間
copyNewline: '\n'                                   //複製時,此換行符將插入行值之間

五、總結

本篇文章只是簡單的闡述 Bootstrap-Table 如何使用,正在對錶格功能實現而憂愁的小夥伴,可以使用 HelloGitHub 推薦的這款插件。你會發現網頁製作表格還可以如此快捷,期待小夥伴挖掘出更加有意思的功能哦。

注:上面 js 部分並沒有採用函數形式,建議在使用熟悉之後還是採用函數形式,這樣也方便復用及讓代碼看起來更加規範。

六、參考資料

『講解開源項目系列』——讓對開源項目感興趣的人不再畏懼、讓開源項目的發起者不再孤單。跟着我們的文章,你會發現編程的樂趣、使用和發現參与開源項目如此簡單。歡迎留言聯繫我們、加入我們,讓更多人愛上開源、貢獻開源~

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】

※想知道網站建置網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計後台網頁設計

※不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※帶您來看台北網站建置台北網頁設計,各種案例分享

小三通物流營運型態?

※快速運回,大陸空運推薦?

Vue躬行記(9)——Vuex

  Vuex是一個專為Vue.js設計的狀態管理庫,適用於多組件共享狀態的場景。Vuex能集中式的存儲和維護所有組件的狀態,並提供相關規則保證狀態的獨立性、正確性和可預測性,這不僅讓調試變得可追蹤,還讓代碼變得更結構化且易維護。本文所使用的Vuex,其版本是3.1.1。

一、基本用法

  首先需要引入Vue和Vuex兩個庫,如果像下面這樣在Vue之後引入Vuex,那麼Vuex會自動調用Vue.use()方法註冊其自身;但如果以模塊的方式引用,那麼就得顯式地調用Vue.use()。注意,因為Vuex依賴Promise,所以對於那些不支持Promise的瀏覽器,要使用Vuex的話,得引入相關的polyfill庫,例如es6-promise。

<script src="js/vue.js"></script>
<script src="js/vuex.js"></script>

  然後創建Vuex應用的核心:Store(倉庫)。它是一個容器,保存着大量的響應式狀態(State),並且這些狀態不能直接修改,需要顯式地將修改請求提交到Mutation(變更)中才能實現更新,因為這樣便於追蹤每個狀態的變化。在下面的示例中,初始化了一個digit狀態,並在mutations選項中添加了兩個可將其修改的方法。

const store = new Vuex.Store({
  state: {
    digit: 0
  },
  mutations: {
    add: state => state.digit++,
    minus: state => state.digit--
  }
});

  接着創建根實例,並將store實例注入,從而讓整個應用都能讀寫其中的狀態,在組件中可通過$store屬性訪問到它,如下所示,以計算屬性的方式讀取digit狀態,並通過調用commit()方法來修改該狀態。

var vm = new Vue({
  el: "#container",
  store: store,
  computed: {
    digit() {
      return this.$store.state.digit;
    }
  },
  methods: {
    add() {
      this.$store.commit("add");
    },
    minus() {
      this.$store.commit("minus");
    }
  }
});

  最後將根實例中的方法分別註冊到兩個按鈕的點擊事件中,如下所示,每當點擊這兩個按鈕時,狀態就會更新,並在頁面中显示。

<div id="container">
  <p>{{digit}}</p>
  <button @click="add">增加</button>
  <button @click="minus">減少</button>
</div>

二、主要組成

  Vuex的主要組成除了上一節提到的Store、State和Mutation之外,還包括Getter和Action,本節會對其中的四個做重點講解,它們之間的關係如圖2所示。

圖2  四者的關係

1)State

  State是一個可存儲狀態的對象,在應用的任何位置都能被訪問到,並且作為單一數據源(Single Source Of Truth)而存在。

  當組件需要讀取大量狀態時,一個個的聲明成計算屬性會顯得過於繁瑣而冗餘,於是Vuex提供了一個名為mapState()的輔助函數,用來將狀態自動映射成計算屬性,它的參數既可以是數組,也可以是對象。

  當計算屬性的名稱與狀態名稱相同,並且不需要做額外處理時,可將名稱組成一個字符串數組傳遞給mapState()函數,在組件中可按原名調用,如下所示。

var vm = new Vue({
  computed: Vuex.mapState([ "digit" ])
});

  當計算屬性的名稱與狀態名稱不同,或者計算屬性讀取的是需要處理的狀態時,可將一個對象傳遞給mapState()函數,其鍵就是計算屬性的名稱,而其值既可以是函數,也可以是字符串,如下代碼所示。如果是函數,那麼它的第一個參數是state,即狀態對象;如果是字符串,那麼就是從state中指定一個狀態作為計算屬性。

var vm = new Vue({
  computed: Vuex.mapState({
    digit: state => state.digit,
    alias: "digit"        //相當於state => state.digit
  })
});

  因為mapState()函數返回的是一個對象,所以當組件內已經包含計算屬性時,可以對其應用擴展運算符(…)來進行合併,如下所示,這是一種極為簡潔的寫法。

var vm = new Vue({
  computed: {
    name() {},
    ...Vuex.mapState([ "digit" ])
  }
});

2)Getter

  Getter是從State中派生出的狀態,當多個組件要對同一個狀態進行相同的處理時,就需要將狀態轉移到Getter中,以免產生重複的冗餘代碼。

  Getter相當於Store的計算屬性,它能接收兩個參數,第一個是state對象,第二個是可選的getters對象,該參數能讓不同的Getter之間相互訪問。Getter的返回值會被緩存,並且只有當依賴值發生變化時才會被重新計算。不過當返回值是函數時,其結果就不會被緩存,如下所示,其中caculate返回的是個数字,而sum返回的是個函數。

const store = new Vuex.Store({
  state: {
    digit: 0
  },
  getters: {
    caculate: state => {
      return state.digit + 2;
    },
    sum: state => right => {
      return state.digit + right;
    }
  }
});

  在組件內可通過this.$store.getters訪問到Getter中的數據,如下所示,讀取了上一個示例中的兩個Getter。

var vm = new Vue({
  methods: {
    add() {
      this.$store.getters.caculate;
      this.$store.getters.sum(1);
    }
  }
});

  Getter也有一個輔助函數,用來將Getter自動映射為組件的計算屬性,名字叫mapGetters(),其參數也是數組或對象。但與之前的mapState()不同,當參數是對象時,其值不能是函數,只能是字符串,如下所示,為了對比兩種寫法,聲明了兩個computed選項。

var vm = new Vue({
  computed: Vuex.mapGetters([ "caculate" ]),
  computed: Vuex.mapGetters({
    alias: "caculate"
  })
});

3)Mutation

  更改狀態的唯一途徑是提交Mutation,Vuex中的Mutation類似於事件,也包含一個類型和回調函數,在函數體中可進行狀態更改的邏輯,並且它能接收兩個參數,第一個是state對象,第二個是可選的附加數據,叫載荷(Payload)。下面這個Mutation的類型是“interval”,接收了兩個參數。

const store = new Vuex.Store({
  state: {
    digit: 0
  },
  mutations: {
    interval: (state, payload) => state.digit += payload.number
  }
});

  在組件中不能直接調用Mutation的回調函數,得通過this.$store.commit()方法觸發更新,如下所示,採用了兩種提交方式,第一種是傳遞type參數,第二種是傳遞包含type屬性的對象。

var vm = new Vue({
  methods: {
    interval() {
      this.$store.commit("interval", { number: 2 });             //第一種
      this.$store.commit({ type: "interval", number: 2 });       //第二種
    }
  }
});

  當多人協作時,Mutation的類型適合寫成常量,這樣更容易維護,也能減少衝突。

const INTERVAL = "interval";

  Mutation有一個名為mapMutations()的輔助函數,其寫法和mapState()相同,它能將Mutation自動映射為組件的方法,如下所示。

var vm = new Vue({
  methods: Vuex.mapMutations(["interval"])
  //相當於
  methods: {
    interval(payload) {
      this.$store.commit(INTERVAL, payload);
    }
  }
});

  注意,為了能追蹤狀態的變更,Mutation只支持同步的更新,如果要異步,那麼得使用Action。

4)Action

  Action類似於Mutation,但不同的是它可以包含異步操作,並且只能用來通知Mutation,不會直接更新狀態。Action的回調函數能接收兩個參數,第一個是與Store實例具有相同屬性和方法的context對象(注意,不是Store實例本身),第二個是可選的附加數據,如下所示,調用commit()方法提交了一個Mutation。

const store = new Vuex.Store({
  actions: {
    interval(context, payload) {
      context.commit("interval", payload);
    }
  }
});

  在組件中能通過this.$store.dispatch()方法分發Action,如下所示,與commit()方法一樣,它也有兩種提交方式。

var vm = new Vue({
  methods: {
    interval() {
      this.$store.dispatch("interval", { number: 2 });            //第一種
      this.$store.dispatch({type: "interval", number: 2});        //第二種
    }
  }
});

  注意,由於dispatch()方法返回的是一個Promise對象,因此它能以一種更優雅的方式來處理異步操作,如下所示。

var vm = new Vue({
  methods: {
    interval() {
      this.$store.dispatch("interval", { number: 2 }).then(() => {
        console.log("success");
      });
    }
  }
});

  Action有一個名為mapActions()的輔助函數,其寫法和mapState()相同,它能將Action自動映射為組件的方法,如下所示。

var vm = new Vue({
  methods: Vuex.mapActions([ "interval" ])
});

三、模塊

  當應用越來越大時,為了避免Store變得過於臃腫,有必要將其拆分到一個個的模塊(Module)中。每個模塊就是一個對象,包含屬於自己的State、Getter、Mutation和Action,甚至還能嵌套其它模塊。

1)局部狀態

  對於模塊內部的Getter和Mutation,它們接收的第一個參數是模塊的局部狀態,而Getter的第三個參數rootState和Action的context.rootState屬性可訪問根節點狀態(即全局狀態),如下所示。

const moduleA = {
  state: { digit: 0 },
  mutations: {
    add: state => state.digit++
  },
  getters: {
    caculate: (state, getter, rootState) => {
      return state.digit + 2;
    }
  },
  actions: {
    interval(context, payload) {
      context.commit("add", payload);
    }
  }
};

2)命名空間

  默認情況下,只有在訪問State時需要帶命名空間,而Getter、Mutation和Action的調用方式不變。將之前的moduleA模塊註冊到Store實例中,如下所示,modules選項的值是一個子模塊對象,其鍵是模塊名稱。

const store = new Vuex.Store({
  modules: {
    a: moduleA
  }
});

  如果要訪問模塊中的digit狀態,那麼可以像下面這樣寫。

store.state.a.digit;

  當模塊的namespaced屬性為true時,它的Getter、Mutation和Action也會帶命名空間,在使用時,需要添加命名空間前綴,如下代碼所示,此舉大大提升了模塊的封裝性和復用性。

const moduleA = {
  namespaced: true
};
var vm = new Vue({
  el: "#container",
  store: store,
  methods: {
    add() {
      this.$store.commit("a/add");
    },
    caculate() {
      this.$store.getters["a/caculate"];
    }
  }
});

  如果要在帶命名空間的模塊中提交全局的Mutation或分發全局的Action,那麼只要將{root: true}作為第三個參數傳給commit()或dispatch()就可實現,如下所示。

var vm = new Vue({
  methods: {
    add() {
      this.$store.dispatch("add", null, { root: true });
      this.$store.commit("add", null, { root: true });
    }
  }
});

  如果要在帶命名空間的模塊中註冊全局的Action,那麼需要將其修改成對象的形式,然後添加root屬性並設為true,再將Action原先的定義轉移到handler()函數中,如下所示。

const moduleA = {
  actions: {
    interval: {
      root: true,
      handler(context, payload) {}
    }
  }
};

3)輔助函數

  當使用mapState()、mapGetters()、mapMutations()和mapActions()四個輔助函數對帶命名空間的模塊做映射時,需要顯式的包含命名空間,如下所示。

var vm = new Vue({
  computed: Vuex.mapState({
    digit: state => state.a.digit
  }),
  methods: Vuex.mapMutations({
    add: "a/add"
  })
});

  這四個輔助函數的第一個參數都是可選的,用於綁定命名空間,可簡化映射過程,如下所示。

var vm = new Vue({
  computed: Vuex.mapState("a", {
    digit: state => state.digit
  }),
  methods: Vuex.mapMutations("a", {
    add: "add"
  })
});

  Vuex還提供了另一個輔助函數createNamespacedHelpers(),可創建綁定命名空間的輔助函數,如下所示。

const { mapState, mapMutations } = Vuex.createNamespacedHelpers("a");

四、動態註冊

  在創建Store實例后,可通過registerModule()方法動態註冊模塊,如下代碼所示,調用了兩次registerModule()方法,第一次註冊了模塊“a”,第二次註冊了嵌套模塊“a/b”。

const store = new Vuex.Store();
store.registerModule("a", moduleA);    
store.registerModule(["a", "b"], moduleAB);

  通過store.state.a和store.state.a.b可訪問模塊的局部狀態。如果要卸載動態註冊的模塊,那麼可以通過unregisterModule()方法實現。

  registerModule()方法的第三個參數是可選的配置對象,當preserveState屬性的值為true時(如下所示),在註冊模塊時會忽略模塊中的狀態,即無法在store中讀取模塊中的狀態。

store.registerModule("a", moduleA, { preserveState: true });

五、表單處理

  表單默認能直接修改組件的狀態,但是在Vuex中,狀態只能由Mutation觸發更新。為了能更好的追蹤狀態的變化,也為了能更符合Vuex的思維,需要讓表單控件與狀態綁定在一起,並通過input或change事件監聽狀態更新的行為,如下所示。

<div id="container">
  <input :value="digit" @input="add" />
</div>

  然後在Store實例中初始化digit狀態,並添加更新狀態的Mutation,如下所示。

const store = new Vuex.Store({
  state: {
    digit: 0
  },
  mutations: {
    add: (state, value) => {
      state.digit = value;
    }
  }
});

  最後在創建根實例時,將digit狀態映射成它的計算屬性,在事件處理程序add()中調用commit()方法,並將控件的值作為第二個參數傳入,如下所示。

var vm = new Vue({
  el: "#container",
  store: store,
  computed: Vuex.mapState(["digit"]),
  methods: {
    add(e) {
      this.$store.commit("add", e.target.value);
    }
  }
});

  還有一個方法也能實現相同的功能,那就是在控件上使用v-model指令,但需要與帶setter的計算屬性配合,如下所示(只給出了關鍵部分的代碼)。

<div id="container">
  <input v-model="digit" />
</div>
<script>
  var vm = new Vue({
    computed: {
      digit: {
        get() {
          return this.$store.state.digit;
        },
        set(value) {
          this.$store.commit("add", value);
        }
      }
    }
  });
</script>

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

※廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

※自行創業 缺乏曝光? 下一步"網站設計"幫您第一時間規劃公司的門面形象

台灣寄大陸海運貨物規則及重量限制?

大陸寄台灣海運費用試算一覽表

台中搬家,彰化搬家,南投搬家前需注意的眉眉角角,別等搬了再說!

[ASP.NET Core 3框架揭秘] 文件系統[2]:總體設計

在《》中,我們通過幾個簡單的實例演示從編程的角度對文件系統做了初步的體驗,接下來我們繼續從設計的角度來進一步認識它。這個抽象的文件系統以目錄的形式來組織文件,我們可以利用它讀取某個文件的內容,還可以對目錄或者文件實施監控並及時得到變化的通知。由於IFileProvider對象提供了針對文件系統變換的監控功能,在.NET Core下里類似的功能大都利用一個IChangeToken對象來實現,所以我們在對IFileProvider進行深入介紹之前有必要先來了解一下IChangeToken。

一、IChangeToken

從字面上理解的IChangeToken對象就是一個與某組監控數據關聯的“令牌(Token)”,它能夠在檢測到數據改變的時候及時地對外發出一個通知。如果IChangeToken關聯的數據發生改變,它的HasChanged屬性將變成True。我們可以調用其RegisterChangeCallback方法註冊一個在數據發生改變時可以自動執行的回調,該方法會返回一個IDisposable對象,我們通過用其Dispose方法解除註冊的回調。至於IChangeToken接口的另一個屬性ActiveChangeCallbacks,它表示當數據發生變化時是否需要主動執行註冊的回調操作。

public interface IChangeToken
{
    bool HasChanged { get; }
    bool ActiveChangeCallbacks { get; }
    IDisposable RegisterChangeCallback(Action<object> callback, object state);
}

.NET Core提供了若干原生的IChangeToken實現類型,我們最常使用的是一個名為CancellationChangeToken的實現。CancellationChangeToken的實現原理很簡單,它基本上就是按照如下的形式藉助我們熟悉的CancellationToken對象來發送通知。

public class CancellationChangeToken : IChangeToken
{
    private readonly CancellationToken _token;
    public CancellationChangeToken(CancellationToken token)  => _token = token;
    public bool HasChanged  => _token.IsCancellationRequested; 
    public bool ActiveChangeCallbacks  => true;    
    public IDisposable RegisterChangeCallback(Action<object> callback, object state)  => _token.Register(callback, state);
}

除了CancellationChangeToken,有時也我們也會使用到一個名為CompositeChangeToken的實現。顧名思義,CompositeChangeToken代表由多個IChangeToken組合而成的複合型IChangeToken對象。如下面的代碼片段所示,我們在調用構造函數創建一個CompositeChangeToken對象的時候,需要提供這些IChangeToken對象。對於一個CompositeChangeToken對象來說,只要組成它的任何一個IChangeToken發生改變,其HasChanged屬性將會變成True,而註冊的回調自然會被執行。至於ActiveChangeCallbacks屬性,只要任何一個IChangeToken的同名屬性返回True,該屬性就會返回True。

public class CompositeChangeToken : IChangeToken
{
    public bool  ActiveChangeCallbacks { get; }
    public IReadOnlyList<IChangeToken>  ChangeTokens { get; }
    public bool  HasChanged { get; }
   
    public CompositeChangeToken(IReadOnlyList<IChangeToken> changeTokens);   
    public IDisposable RegisterChangeCallback(Action<object> callback, object state);   
}

我們可以直接調用IChangeToken提供的RegisterChangeCallback方法來註冊在接收到數據變化通知后的回調操作,但是更常用的方式則是直接調用靜態類型ChangeToken提供的如下兩個OnChange方法重載來進行回調註冊,這兩個方法的第一個參數需要被指定為一個用來提供IChangeToken對象的Func<IChangeToken>委託。

public static class ChangeToken
{
    public static IDisposable OnChange(Func<IChangeToken> changeTokenProducer,  Action changeTokenConsumer) ;
    public static IDisposable OnChange<TState>(Func<IChangeToken> changeTokenProducer,  Action<TState> changeTokenConsumer, TState state) ;
}

二、IFileProvider

在了解了IChangeToken是怎樣一個對象之後,我們將關注轉移到文件系統的核心接口IFileProvider上,該接口定義在NuGet包“Microsoft.Extensions.FileProviders.Abstractions”中。我們在《》做了幾個簡單的實例演示,它們實際上體現了文件系統承載的三個基本功能,而這三個基本功能分別體現在IFileProvider接口如下所示的三個方法中。

public interface IFileProvider
{    
    IFileInfo GetFileInfo(string subpath);
    IDirectoryContents GetDirectoryContents(string subpath);
    IChangeToken Watch(string filter);
}

三、IFileInfo

雖然文件系統採用目錄來組織文件,但是不論是目錄還是文件都通過一個IFileInfo對象來表示,至於具體是目錄還是文件則通過IFileInfo的IsDirectory屬性來確定。對於一個IFileInfo對象,我們可以通過只讀屬性Exists判斷指定的目錄或者文件是否真實存在。至於另外兩個屬性NamePhysicalPath,它們分別表示文件或者目錄的名稱和物理路徑。屬性LastModified返回一個時間戳,表示目錄或者文件最終一次被修改的時間。對於一個表示具體文件的IFileInfo對象來說,我們可以利用屬性Length得到文件內容的字節長度。如果我們希望讀取文件的內容,可以藉助於CreateReadStream方法返回的Stream對象來完成。

public interface IFileInfo
{
    bool Exists { get; }
    bool  IsDirectory { get; }
    string Name { get; }
    string PhysicalPath { get; }
    DateTimeOffset LastModified { get; }
    long Length { get; }

    Stream CreateReadStream();
}

IFileProvider接口的GetFileInfo方法會根據指定的路徑得到表示所在文件的IFileInfo對象。換句話說,雖然一個IFileInfo對象可以用於描述目錄和文件,但是GetFileInfo方法的目的在於得到指定路徑返回的文件而不是目錄(我個人不太認同這種令人產生歧義的API設計)。一般來說,不論指定的文件是否存在,該方法總會返回一個具體的IFileInfo對象,因為目標文件的存在與否是由該對象的Exists屬性來確定的。

四、IDirectoryContents

如果希望得到某個目錄的內容,比如需要查看多少文件或者子目錄包含在這個目錄下,我們可以調用IFileProvider對象的GetDirectoryContents方法並將所在目錄的路徑作為參數。目錄內容通過該方法返回的IDirectoryContents對象來表示。如下面的代碼片段所示,一個IDirectoryContents對象實際上是一組IFileInfo對象的集合,組成這個集合的所有IFileInfo自然就是對包含在這個目錄下的所有文件和子目錄的描述。和GetFileInfo方法一樣,不論指定的目錄是否存在,GetDirectoryContents方法總是會返回一個具體的IDirectoryContents對象,它的Exists屬性會幫助我們確定指定目錄是否存在。

public interface IDirectoryContents : IEnumerable<IFileInfo>
{
    bool Exists { get; }
}

五、監控目錄或者文件更新

如果我們希望監控IFileProvider所在目錄或者文件的變化,我們可以調用它的Watch方法,當然前提是對應的IFileProvider對象提供了這樣的監控功能。這個方法接受一個字符串類型的參數filter,我們可以利用這個參數指定一個針對“文件匹配模式(File Globing Pattern)”表達式(以下簡稱Globing Pattern表達式)來篩選需要監控的目標目錄或文件。

Globing Pattern表達式比正則表達式簡單多了,它只包含“*”一種“通配符”,如果硬說它包含兩種通配符的話,那麼另一個通配符是“**”。Globing Pattern表達式體現為一個文件路徑,其中“*”代表所有不包括路徑分隔符(“/”或者“\”)的所有字符,而“**”則代表包含路徑分隔符在內的所有字符。下錶給出了幾個典型的Globing Pattern表達式和它們代碼的文件匹配語義。






















Globing Pattern表達式

匹配的文件

src/foobar/foo/settings.*

 

子目錄“src/foobar/foo/”(不含其子目錄)下名為“settings”的所有文件,比如settings.jsonsettings.xmlsettings.ini等。

src/foobar/foo/*.cs

 

子目錄“src/foobar/foo/”(不含其子目錄)下的所有.cs文件。

src/foobar/foo/*.*

子目錄“src/foobar/foo/”(不含其子目錄)下所有文件。

src/**/*.cs

子目錄“src”(含其子目錄)下的所有.cs文件。

一般來說,不論是調用IFileProvider對象的GetFileInfo或GetDirectoryContents方法所指定的目標文件或目錄的路徑,還是調用Watch方法指定的篩選表達式,都是一個針對當前IFileProvider對象映射根目錄的相對路徑。指定的這個路徑可以採用“/”字符作為前綴,但是這個前綴是不必要的。換句話說,如下所示的這兩組程序是完全等效的。

路徑不包含前綴“/”

var dirContents = fileProvider.GetDirectoryContents("foobar");
var fileInfo = fileProvider.GetFileInfo("foobar/foobar.txt");
var changeToken = fileProvider.Watch("foobar/*.txt");

路徑包含前綴“/”

var dirContents = fileProvider.GetDirectoryContents("/foobar");
var fileInfo = fileProvider.GetFileInfo("/foobar/foobar.txt");
var changeToken = fileProvider.Watch("/foobar/*.txt");

總的來說,以IFileProvider對象為核心的文件系統在設計上看是非常簡單的。除了IFileProvider接口之外,文件系統還涉及到其他一些對象,比如IDirectoryContents、IFileInfo和IChangeToken等,下圖所示的UML展示了這些接口以及它們之間的關係。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

大陸寄台灣空運注意事項

大陸海運台灣交貨時間多久?

※避免吃悶虧無故遭抬價!台中搬家公司免費估價,有契約讓您安心有保障!

go中的數據結構-通道channel

1. channel的使用

  很多文章介紹channel的時候都和併發揉在一起,這裏我想把它當做一種數據結構來單獨介紹它的實現原理。

  channel,通道。golang中用於數據傳遞的一種數據結構。是golang中一種傳遞數據的方式,也可用作事件通知。

1.1 聲明、傳值、關閉

  使用chan關鍵字聲明一個通道,在使用前必須先創建,操作符 <- 用於指定通道的方向,發送或接收。如果未指定方向,則為雙向通道。

 1 //聲明和創建
 2 var ch chan int      // 聲明一個傳遞int類型的channel
 3 ch := make(chan int) // 使用內置函數make()定義一個channel
 4 ch2 := make(chan interface{})         // 創建一個空接口類型的通道, 可以存放任意格式
 5 
 6 type Equip struct{ /* 一些字段 */ }
 7 ch2 := make(chan *Equip)             // 創建Equip指針類型的通道, 可以存放*Equip
 8 
 9 //傳值
10 ch <- value          // 將一個數據value寫入至channel,這會導致阻塞,直到有其他goroutine從這個channel中讀取數據
11 value := <-ch        // 從channel中讀取數據,如果channel之前沒有寫入數據,也會導致阻塞,直到channel中被寫入數據為止
12 
13 ch := make(chan interface{})  // 創建一個空接口通道
14 ch <- 0 // 將0放入通道中
15 ch <- "hello"  // 將hello字符串放入通道中
16 
17 //關閉
18 close(ch)            // 關閉channel

  把數據往通道中發送時,如果接收方一直都沒有接收,那麼發送操作將持續阻塞。Go 程序運行時能智能地發現一些永遠無法發送成功的語句並報錯:

fatal error: all goroutines are asleep - deadlock! 
//運行時發現所有的 goroutine(包括main)都處於等待 goroutine。

1.2 四種重要的通道使用方式

無緩衝通道

  通道默認是無緩衝的,無緩衝通道上的發送操作將會被阻塞,直到有其他goroutine從對應的通道上執行接收操作,數據傳送完成,通道繼續工作。

package main

import (
    "fmt"
    "time"
)
var done chan bool
func HelloWorld() {
    fmt.Println("Hello world goroutine")
    time.Sleep(1*time.Second)
    done <- true
}
func main() {
    done = make(chan bool)  // 創建一個channel
    go HelloWorld()
    <-done
}
1 //輸出
2 //Hello world goroutine

  由於main不會等goroutine執行結束才返回,前文專門加了sleep輸出為了可以看到goroutine的輸出內容,那麼在這裏由於是阻塞的,所以無需sleep。

  將代碼中”done <- true”和”<-done”,去掉再執行,沒有上面的輸出內容。

管道

  通道可以用來連接goroutine,這樣一個的輸出是另一個輸入。這就叫做管道:

 

 1 package main
 2 
 3 import (
 4     "fmt"
 5     "time"
 6 )
 7 var echo chan string
 8 var receive chan string
 9 
10 // 定義goroutine 1 
11 func Echo() {
12     time.Sleep(1*time.Second)
13     echo <- "這是一次測試"
14 }
15 
16 // 定義goroutine 2
17 func Receive() {
18     temp := <- echo // 阻塞等待echo的通道的返回
19     receive <- temp
20 }
21 
22 
23 func main() {
24     echo = make(chan string)
25     receive = make(chan string)
26 
27     go Echo()
28     go Receive()
29 
30     getStr := <-receive   // 接收goroutine 2的返回
31 
32     fmt.Println(getStr)
33 }

  輸出字符串:”這是一次測試”。

  在這裏不一定要去關閉channel,因為底層的垃圾回收機制會根據它是否可以訪問來決定是否自動回收它。(這裏不是根據channel是否關閉來決定的)

單向通道類型
 1 package main
 2 
 3 import (
 4     "fmt"
 5     "time"
 6 )
 7 
 8 // 定義goroutine 1
 9 func Echo(out chan<- string) {   // 定義輸出通道類型
10     time.Sleep(1*time.Second)
11     out <- "這又是一次測試"
12     close(out)
13 }
14 
15 // 定義goroutine 2
16 func Receive(out chan<- string, in <-chan string) { // 定義輸出通道類型和輸入類型
17     temp := <-in // 阻塞等待echo的通道的返回
18     out <- temp
19     close(out)
20 }
21 
22 
23 func main() {
24     echo := make(chan string)
25     receive := make(chan string)
26 
27     go Echo(echo)
28     go Receive(receive, echo)
29 
30     getStr := <-receive   // 接收goroutine 2的返回
31 
32     fmt.Println(getStr)
33 }

  輸出:這又是一次測試。

緩衝管道

  goroutine的通道默認是是阻塞的,那麼有什麼辦法可以緩解阻塞? 答案是:加一個緩衝區。

  創建一個緩衝通道:

1 ch := make(chan string, 3) // 創建了緩衝區為3的通道
2 
3 //==
4 len(ch)   // 長度計算
5 cap(ch)   // 容量計算

  緩衝通道傳遞數據示意圖:

 

2. 內部結構 

  Go語言channel是first-class的,意味着它可以被存儲到變量中,可以作為參數傳遞給函數,也可以作為函數的返回值返回。作為Go語言的核心特徵之一,雖然channel看上去很高端,但是其實channel僅僅就是一個數據結構而已,具體定義在 $GOROOT/src/runtime/chan.go里。如下:

 1 type hchan struct {
 2   qcount uint   // 隊列中的總數據
 3   dataqsiz uint   // 循環隊列的大小
 4   buf unsafe.Pointer // 指向dataqsiz元素數組
 5   elemsize uint16  // 
 6   closed uint32 
 7   elemtype *_type // 元素類型
 8   sendx uint // 發送索引
 9   recvx uint // 接收索引
10   recvq waitq // 接待員名單, 因recv而阻塞的等待隊列。
11   sendq waitq // 發送服務員列表, 因send而阻塞的等待隊列。
12   //鎖定保護hchan中的所有字段,以及幾個在此通道上阻止的sudogs中的字段。
13   //按住此鎖定時不要更改另一個G的狀態(尤其是不要準備G),因為這可能會導致死鎖堆棧縮小。
14   lock mutex 
15 }

   其中一個核心的部分是存放channel數據的環形隊列,由qcount和elemsize分別指定了隊列的容量和當前使用量。dataqsize是隊列的大小。elemalg是元素操作的一個Alg結構體,記錄下元素的操作,如copy函數,equal函數,hash函數等。

  如果是帶緩衝區的chan,則緩衝區數據實際上是緊接着Hchan結構體中分配的。不帶緩衝的 channel ,環形隊列 size 則為 0。

1 c = (Hchan*)runtime.mal(n + hint*elem->size);

  另一重要部分就是recvq和sendq兩個鏈表,一個是因讀這個通道而導致阻塞的goroutine,另一個是因為寫這個通道而阻塞的goroutine。如果一個goroutine阻塞於channel了,那麼它就被掛在recvq或sendq中。WaitQ是鏈表的定義,包含一個頭結點和一個尾結點:

1 struct    WaitQ
2 {
3     SudoG*    first;
4     SudoG*    last;
5 };

  隊列中的每個成員是一個SudoG結構體變量:

1 struct    SudoG
2 {
3     G*    g;        // g和selgen構成
4     uint32    selgen;        // 指向g的弱指針
5     SudoG*    link;
6     int64    releasetime;
7     byte*    elem;        // 數據元素
8 };

  該結構中主要的就是一個g和一個elem。elem用於存儲goroutine的數據。讀通道時,數據會從Hchan的隊列中拷貝到SudoG的elem域。寫通道時,數據則是由SudoG的elem域拷貝到Hchan的隊列中。

 

  基本的寫channel操作,在底層運行時庫中對應的是一個runtime.chansend函數。

1 c <- v
  在運行時庫中會執行:
1 void runtime·chansend(ChanType *t, Hchan *c, byte *ep, bool *pres, void *pc)

  其中c就是channel,ep是取變量v的地址。這裏的傳值約定是調用者負責分配好ep的空間,僅需要簡單的取變量地址就夠了。pres參數是在select中的通道操作使用的。

  這個函數首先會區分是同步還是異步。同步是指chan是不帶緩衝區的,因此可能寫阻塞,而異步是指chan帶緩衝區,只有緩衝區滿才阻塞。在同步的情況下,由於channel本身是不帶數據緩存的,這時首先會查看Hchan結構體中的recvq鏈表時否為空,即是否有因為讀該管道而阻塞的goroutine。如果有則可以正常寫channel,否則操作會阻塞。

  recvq不為空的情況下,將一個SudoG結構體出隊列,將傳給通道的數據(函數參數ep)拷貝到SudoG結構體中的elem域,並將SudoG中的g放到就緒隊列中,狀態置為ready,然後函數返回。如果recvq為空,否則要將當前goroutine阻塞。此時將一個SudoG結構體,掛到通道的sendq鏈表中,這個SudoG中的elem域是參數eq,SudoG中的g是當前的goroutine。當前goroutine會被設置為waiting狀態並掛到等待隊列中。

  在異步的情況,如果緩衝區滿了,也是要將當前goroutine和數據一起作為SudoG結構體掛在sendq隊列中,表示因寫channel而阻塞。否則也是先看有沒有recvq鏈表是否為空,有就喚醒。跟同步不同的是在channel緩衝區不滿的情況,這裏不會阻塞寫者,而是將數據放到channel的緩衝區中,調用者返回。

  讀channel的操作也是類似的,對應的函數是runtime.chansend。一個是收一個是發,基本的過程都是差不多的。

  當協程嘗試從未關閉的 channel 中讀取數據時,內部的操作如下:

  • 當 buf 非空時,此時 recvq 必為空,buf 彈出一個元素給讀協程,讀協程獲得數據後繼續執行,此時若 sendq 非空,則從 sendq 中彈出一個寫協程轉入 running 狀態,待寫數據入隊列 buf ,此時讀取操作 <- ch 未阻塞;
  • 當 buf 為空但 sendq 非空時(不帶緩衝的 channel),則從 sendq 中彈出一個寫協程轉入 running 狀態,待寫數據直接傳遞給讀協程,讀協程繼續執行,此時讀取操作 <- ch 未阻塞;
  • 當 buf 為空並且 sendq 也為空時,讀協程入隊列 recvq 並轉入 blocking 狀態,當後續有其他協程往 channel 寫數據時,讀協程才會重新轉入 running 狀態,此時讀取操作 <- ch 阻塞。

  類似的,當協程嘗試往未關閉的 channel 中寫入數據時,內部的操作如下:

  • 當隊列 recvq 非空時,此時隊列 buf 必為空,從 recvq 彈出一個讀協程接收待寫數據,此讀協程此時結束阻塞並轉入 running 狀態,寫協程繼續執行,此時寫入操作 ch <- 未阻塞;
  • 當隊列 recvq 為空但 buf 未滿時,此時 sendq 必為空,寫協程的待寫數據入 buf 然後繼續執行,此時寫入操作 ch <- 未阻塞;
  • 當隊列 recvq 為空並且 buf 為滿時,此時寫協程入隊列 sendq 並轉入 blokcing 狀態,當後續有其他協程從 channel 中讀數據時,寫協程才會重新轉入 running 狀態,此時寫入操作 ch <- 阻塞。

  當關閉 non-nil channel 時,內部的操作如下:

  • 當隊列 recvq 非空時,此時 buf 必為空,recvq 中的所有協程都將收到對應類型的零值然後結束阻塞狀態;
  • 當隊列 sendq 非空時,此時 buf 必為滿,sendq 中的所有協程都會產生 panic ,在 buf 中數據仍然會保留直到被其他協程讀取。

  空通道是指將一個channel賦值為nil,或者定義后不調用make進行初始化。按照Go語言的語言規範,讀寫空通道是永遠阻塞的。其實在函數runtime.chansend和runtime.chanrecv開頭就有判斷這類情況,如果發現參數c是空的,則直接將當前的goroutine放到等待隊列,狀態設置為waiting。

  讀一個關閉的通道,永遠不會阻塞,會返回一個通道數據類型的零值。這個實現也很簡單,將零值複製到調用函數的參數ep中。寫一個關閉的通道,則會panic。關閉一個空通道,也會導致panic。

3. channel的高級用法

3.1 條件變量(condition variable)

  類型於 POSIX 接口中線程通知其他線程某個事件發生的條件變量,channel 的特性也可以用來當成協程之間同步的條件變量。因為 channel 只是用來通知,所以 channel 中具體的數據類型和值並不重要,這種場景一般用 strct {} 作為 channel 的類型。

一對一通知

  類似 pthread_cond_signal() 的功能,用來在一個協程中通知另個某一個協程事件發生:

 1 package main
 2 
 3 import (
 4     "fmt"
 5     "time"
 6 )
 7 
 8 func main() {
 9     ch := make(chan struct{})
10     nums := make([]int, 100)
11 
12     go func() {
13         time.Sleep(time.Second)
14         for i := 0; i < len(nums); i++ {
15             nums[i] = i
16         }
17         // send a finish signal
18         ch <- struct{}{}
19     }()
20 
21     // wait for finish signal
22     <-ch
23     fmt.Println(nums)
24 }
廣播通知

  類似 pthread_cond_broadcast() 的功能。利用從已關閉的 channel 讀取數據時總是非阻塞的特性,可以實現在一個協程中向其他多個協程廣播某個事件發生的通知:

 1 package main
 2 
 3 import (
 4     "fmt"
 5     "time"
 6 )
 7 
 8 func main() {
 9     N := 10
10     exit := make(chan struct{})
11     done := make(chan struct{}, N)
12 
13     // start N worker goroutines
14     for i := 0; i < N; i++ {
15         go func(n int) {
16             for {
17                 select {
18                 // wait for exit signal
19                 case <-exit:
20                     fmt.Printf("worker goroutine #%d exit\n", n)
21                     done <- struct{}{}
22                     return
23                 case <-time.After(time.Second):
24                     fmt.Printf("worker goroutine #%d is working...\n", n)
25                 }
26             }
27         }(i)
28     }
29 
30     time.Sleep(3 * time.Second)
31     // broadcast exit signal
32     close(exit)
33     // wait for all worker goroutines exit
34     for i := 0; i < N; i++ {
35         <-done
36     }
37     fmt.Println("main goroutine exit")
38 }

3.2 信號量

  channel 的讀/寫相當於信號量的 P / V 操作,下面的示例程序中 channel 相當於信號量:

 1 package main
 2 
 3 import (
 4     "log"
 5     "math/rand"
 6     "time"
 7 )
 8 
 9 type Seat int
10 type Bar chan Seat
11 
12 func (bar Bar) ServeConsumer(customerId int) {
13     log.Print("-> consumer#", customerId, " enters the bar")
14     seat := <-bar // need a seat to drink
15     log.Print("consumer#", customerId, " drinks at seat#", seat)
16     time.Sleep(time.Second * time.Duration(2+rand.Intn(6)))
17     log.Print("<- consumer#", customerId, " frees seat#", seat)
18     bar <- seat // free the seat and leave the bar
19 }
20 
21 func main() {
22     rand.Seed(time.Now().UnixNano())
23 
24     bar24x7 := make(Bar, 10) // the bar has 10 seats
25     // Place seats in an bar.
26     for seatId := 0; seatId < cap(bar24x7); seatId++ {
27         bar24x7 <- Seat(seatId) // none of the sends will block
28     }
29 
30     // a new consumer try to enter the bar for each second
31     for customerId := 0; ; customerId++ {
32         time.Sleep(time.Second)
33         go bar24x7.ServeConsumer(customerId)
34     }
35 }

3.3 互斥量

  互斥量相當於二元信號里,所以 cap 為 1 的 channel 可以當成互斥量使用:

 1 package main
 2 
 3 import "fmt"
 4 
 5 func main() {
 6     mutex := make(chan struct{}, 1) // the capacity must be one
 7 
 8     counter := 0
 9     increase := func() {
10         mutex <- struct{}{} // lock
11         counter++
12         <-mutex // unlock
13     }
14 
15     increase1000 := func(done chan<- struct{}) {
16         for i := 0; i < 1000; i++ {
17             increase()
18         }
19         done <- struct{}{}
20     }
21 
22     done := make(chan struct{})
23     go increase1000(done)
24     go increase1000(done)
25     <-done; <-done
26     fmt.Println(counter) // 2000
27 }

4. 關閉 channel

  關閉不再需要使用的 channel 並不是必須的。跟其他資源比如打開的文件、socket 連接不一樣,這類資源使用完后不關閉後會造成句柄泄露,channel 使用完后不關閉也沒有關係,channel 沒有被任何協程用到后最終會被 GC 回收。關閉 channel 一般是用來通知其他協程某個任務已經完成了。golang 也沒有直接提供判斷 channel 是否已經關閉的接口,雖然可以用其他不太優雅的方式自己實現一個:

1 func isClosed(ch chan int) bool {
2     select {
3     case <-ch:
4         return true
5     default:
6     }
7     return false
8 }

  不過實現一個這樣的接口也沒什麼必要。因為就算通過 isClosed() 得到當前 channel 當前還未關閉,如果試圖往 channel 里寫數據,仍然可能會發生 panic ,因為在調用 isClosed() 后,其他協程可能已經把 channel 關閉了。
關閉 channel 時應該注意以下準則:

  • 不要在讀取端關閉 channel ,因為寫入端無法知道 channel 是否已經關閉,往已關閉的 channel 寫數據會 panic ;
  • 有多個寫入端時,不要再寫入端關閉 channle ,因為其他寫入端無法知道 channel 是否已經關閉,關閉已經關閉的 channel 會發生 panic ;
  • 如果只有一個寫入端,可以在這個寫入端放心關閉 channel 。

  關閉 channel 粗暴一點的做法是隨意關閉,如果產生了 panic 就用 recover 避免進程掛掉。稍好一點的方案是使用標準庫的 sync 包來做關閉 channel 時的協程同步,不過使用起來也稍微複雜些。下面介紹一種優雅些的做法。

4.1 一寫多讀

  這種場景下這個唯一的寫入端可以關閉 channel 用來通知讀取端所有數據都已經寫入完成了。讀取端只需要用 for range 把 channel 中數據遍歷完就可以了,當 channel 關閉時,for range 仍然會將 channel 緩衝中的數據全部遍歷完然後再退出循環:

 1 package main
 2 
 3 import (
 4     "fmt"
 5     "sync"
 6 )
 7 
 8 func main() {
 9     wg := &sync.WaitGroup{}
10     ch := make(chan int, 100)
11 
12     send := func() {
13         for i := 0; i < 100; i++ {
14             ch <- i
15         }
16         // signal sending finish
17         close(ch)
18     }
19 
20     recv := func(id int) {
21         defer wg.Done()
22         for i := range ch {
23             fmt.Printf("receiver #%d get %d\n", id, i)
24         }
25         fmt.Printf("receiver #%d exit\n", id)
26     }
27 
28     wg.Add(3)
29     go recv(0)
30     go recv(1)
31     go recv(2)
32     send()
33 
34     wg.Wait()
35 }

4.2 多寫一讀

  這種場景下雖然可以用 sync.Once 來解決多個寫入端重複關閉 channel 的問題,但更優雅的辦法設置一個額外的 channel ,由讀取端通過關閉來通知寫入端任務完成不要再繼續再寫入數據了:

 1 package main
 2 
 3 import (
 4     "fmt"
 5     "sync"
 6 )
 7 
 8 func main() {
 9     wg := &sync.WaitGroup{}
10     ch := make(chan int, 100)
11     done := make(chan struct{})
12 
13     send := func(id int) {
14         defer wg.Done()
15         for i := 0; ; i++ {
16             select {
17             case <-done:
18                 // get exit signal
19                 fmt.Printf("sender #%d exit\n", id)
20                 return
21             case ch <- id*1000 + i:
22             }
23         }
24     }
25 
26     recv := func() {
27         count := 0
28         for i := range ch {
29             fmt.Printf("receiver get %d\n", i)
30             count++
31             if count >= 1000 {
32                 // signal recving finish
33                 close(done)
34                 return
35             }
36         }
37     }
38 
39     wg.Add(3)
40     go send(0)
41     go send(1)
42     go send(2)
43     recv()
44 
45     wg.Wait()
46 }

4.2 多寫多讀

  這種場景稍微複雜,和上面的例子一樣,也需要設置一個額外 channel 用來通知多個寫入端和讀取端。另外需要起一個額外的協程來通過關閉這個 channel 來廣播通知:

 1 package main
 2 
 3 import (
 4     "fmt"
 5     "sync"
 6     "time"
 7 )
 8 
 9 func main() {
10     wg := &sync.WaitGroup{}
11     ch := make(chan int, 100)
12     done := make(chan struct{})
13 
14     send := func(id int) {
15         defer wg.Done()
16         for i := 0; ; i++ {
17             select {
18             case <-done:
19                 // get exit signal
20                 fmt.Printf("sender #%d exit\n", id)
21                 return
22             case ch <- id*1000 + i:
23             }
24         }
25     }
26 
27     recv := func(id int) {
28         defer wg.Done()
29         for {
30             select {
31             case <-done:
32                 // get exit signal
33                 fmt.Printf("receiver #%d exit\n", id)
34                 return
35             case i := <-ch:
36                 fmt.Printf("receiver #%d get %d\n", id, i)
37                 time.Sleep(time.Millisecond)
38             }
39         }
40     }
41 
42     wg.Add(6)
43     go send(0)
44     go send(1)
45     go send(2)
46     go recv(0)
47     go recv(1)
48     go recv(2)
49 
50     time.Sleep(time.Second)
51     // signal finish
52     close(done)
53     // wait all sender and receiver exit
54     wg.Wait()
55 }

  channle 作為 golang 最重要的特性,用起來還是比較方便的。傳統的 C 里要實現類似的功能的話,一般需要用到 socket 或者 FIFO 來實現,另外還要考慮數據包的完整性與併發衝突的問題,channel 則屏蔽了這些底層細節,使用者只需要考慮讀寫就可以了。 channel 是引用類型,了解一下 channel 底層的機制對更好的使用 channel 還是很用必要的。雖然操作原語簡單,但涉及到阻塞的問題,使用不當可能會造成死鎖或者無限制的協程創建最終導致進程掛掉。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※專營大陸空運台灣貨物推薦

台灣空運大陸一條龍服務

※評比彰化搬家公司費用,南投搬家公司費用收費行情懶人包大公開

彰化搬家費用,南投搬家費用,距離,噸數怎麼算?達人教你簡易估價知識!