如何實現 “defer”:Go vs Java vs C/CPP

hitzhangjie發表於2020-03-04

資源洩漏問題,是不少新手容易忽略的問題。程式中申請的某些資源需要顯示地釋放,特別是系統資源,如開啟的檔案描述符、tcp & udp套接字、動態申請的堆記憶體等等。即便是說很多遍避免資源洩露的各種方式,開發人員仍然不能很好地避免資源洩露問題。避免資源洩露並不是一個技巧性很強的問題,但是當大家寫起程式碼來被各種業務邏輯衝昏了頭的時候,bug就容易趁機而入。
go defer、cpp智慧指標、java try-catch-finally,某種程度上都是解決這類資源釋放問題的利器。

語言層面如果能夠提供某種能力,在資源申請成功、使用之後及時地進行資源釋放,那就再好不過了。在深入對比C、C++、Java、Go中如何實現自動釋放資源之前,我們先考慮下這個過程中存在那些難點:

  • 資源申請成功、正常使用之後,如何判斷資源已經使用完畢?

    以開啟的檔案描述符為例,fd是int型別變數,檔案開啟之後在程式的整個生命週期內都是有效的,其實檔案訪問結束之後就可以認為這個fd可以關閉、釋放了;再以動態申請的堆記憶體為例,堆記憶體空間也是在程式生命週期內有效的,堆記憶體是通過指標進行訪問的,如果沒有任何指標指向這段堆記憶體區域,可以認為分配的堆記憶體可以釋放掉了;再以申請的lock為例,lock成功之後可以訪問臨界區了,從臨界區退出的那一刻開始,可以認為lock可以釋放掉了。

    不同型別的資源,判斷是否使用完畢的方式不一樣,但有一點可以確認,開發人員清楚資源應該何時釋放。

  • 開發人員清楚應該何時釋放資源,但是語言級別如何提供某種機制避免開發人員疏漏?

    C並沒有提供語言層面的機制來避免資源洩露問題,但是利用gcc提供的C擴充套件屬性也可以做到類似的效果;

    C++提供了智慧指標,以malloc動態分配堆記憶體為例,分配成功返回堆記憶體指標,用該指標來初始化一個智慧指標,並繫結對應的回撥方法,當智慧指標作用域結束被銷燬時,其上繫結的回撥方法也會被呼叫。假如我們將釋放堆記憶體的方法free(ptr)註冊為智慧指標上的回撥方法,就可以在分配記憶體所在的作用域銷燬時自動釋放堆記憶體了。

    Java提供了try-catch-finally,在try中申請、使用資源,catch中捕獲可能的異常並處理,在finally中進行資源釋放。以開啟檔案為例,在try中開啟檔案、檔案處理結束之後,finally中呼叫file.close()方法。考慮一種極端情況,我們的檔案處理邏輯比較複雜,中間涉及的程式碼比較多,在編寫了各種邏輯處理、異常處理之後,開發人員是否容易遺忘在finally中關閉檔案呢?這種可能性還是比較大的。開發人員的習慣一般是遵循就近原則,定義變數的時候都是在使用之前,如果try block結尾處沒有明顯的檔案相關的操作,開發人員可能不會聯想到要關閉檔案。

  • 考慮到開發人員遵循就近原則的習慣,能否在資源申請成功後立即註冊一個資源釋放的回撥方法,在資源使用結束的時候回撥這個回撥方法?這個方式是比較容易實現的,聽起來也比較優雅。

    Go defer提供了這樣的能力,在資源申請成功之後立即註冊一個資源釋放的方法,選擇函式退出階段作為申請使用結束的時間點,然後回撥註冊的釋放資源的方法最終完成資源的釋放。既能夠滿足程式設計師“就近使用”的良好作風,也減少了因為遺忘洩露資源的可能,而且程式碼的可維護性也更好。

    Go defer在某些情況下也可能會帶來一定的效能損耗。比如通過lock在保護臨界區,在臨界區退出之後就可以釋放掉lock了,但是呢,defer只有在函式退出階段才會觸發資源的釋放操作。這可能會導致鎖粒度過大,降低併發處理能力。這一點,開發人員要做好權衡,確定自己選擇defer是沒有問題的。

3.1 模擬defer in C

C本身沒有提供defer或者類似defer機制,但是藉助gcc擴充套件也可以實現類似能力。利用gcc提供的擴充套件屬性__cleanup__來修飾變數,當變數離開作用域時可以自動呼叫註冊的回撥函式。

下面是 gcc擴充套件屬性``的描述,感興趣的可以瞭解下。其實gcc提供的這種擴充套件屬性比go defer控制力度更細,因為它可以控制的粒度可以細到“作用域級別”,而go defer只能將有效範圍細到“函式級別”。

示例一

cleanup_attribute_demo.c

# include <stdio.h>

/* Demo code showing the usage of the cleanup variable
   attribute. See:http://gcc.gnu.org/onlinedocs/gcc/Variable-Attributes.html
*/

/* cleanup function
   the argument is a int * to accept the address
   to the final value
*/

void clean_up(int *final_value)
{
  printf("Cleaning up\n");
  printf("Final value: %d\n",*final_value);
}

int main(int argc, char **argv)
{
  /* declare cleanup attribute along with initiliazation
     Without the cleanup attribute, this is equivalent 
     to:
     int avar = 1;
  */

  int avar __attribute__ ((__cleanup__(clean_up))) = 1;
  avar = 5;

  return 0;
}

編譯執行:

$ gcc -Wall cleanup_attribute_demo.c
$ ./a.out
Cleaning up
Final value: 5
示例二
/* Demo code showing the usage of the cleanup variable
   attribute. See:http://gcc.gnu.org/onlinedocs/gcc/Variable-Attributes.html
*/

/* Defines two cleanup functions to close and delete a temporary file
   and free a buffer
*/

# include <stdlib.h>
# include <stdio.h>

# define TMP_FILE "/tmp/tmp.file"

void free_buffer(char **buffer)
{
  printf("Freeing buffer\n");
  free(*buffer);
}

void cleanup_file(FILE **fp)
{
  printf("Closing file\n");
  fclose(*fp);

  printf("Deleting the file\n");
  remove(TMP_FILE);
}

int main(int argc, char **argv)
{
  char *buffer __attribute__ ((__cleanup__(free_buffer))) = malloc(20);
  FILE *fp __attribute__ ((__cleanup__(cleanup_file)));

  fp = fopen(TMP_FILE, "w+");

  if (fp != NULL)
    fprintf(fp, "%s", "Alinewithnospaces");

  fflush(fp);
  fseek(fp, 0L, SEEK_SET);
  fscanf(fp, "%s", buffer);
  printf("%s\n", buffer);

  return 0;
}

編譯執行:

Alinewithnospaces
Closing file
Deleting the file
Freeing buffer

3.2 模擬defer in C++

C++中通過智慧指標可以用來對defer進行簡單模擬,並且其粒度可以控制到作用域級別,而非go defer函式級別。C++模擬實現的defer也是比較優雅地。

#include <iostream>
#include <memory>
#include <funtional>

using namespace std;
using defer = shared_ptr<void>;

int main() {
    defer _(nullptr, bind([]{ cout << ", world"; }));
    cout << "hello"
}

也可以做去掉bind,直接寫lambda表示式。

#include <iostream>
#include <memory>

using namespace std;
using defer = shared_ptr<void>;

int main() {
    defer _(nullptr, [](...){ cout << ", world"; });
    cout << "hello"
}

上述程式碼執行時輸出:hello, world。智慧指標變數為在main函式退出時作用域結束,智慧指標銷燬時會自動呼叫b繫結的lambda表示式,程式先輸出hello,然後再輸出world。這裡的示例程式碼近似模擬了go defer。

3.3 模擬defer in Java

Java中try-catch-finally示例程式碼,這裡就簡單以虛擬碼的形式提供吧:

InputStream fin = null;

try {
    // open file and read
fin = new FileInputStream(...);
    String line = fin.readLine();
    System.out.println(line);
} catch (Exception e) {
    // handle exception
} finally {
    fin.close();
}

Java中的這種try-catch-finally的方式,只能算是一種資源釋放的方式,不能算作是模擬defer。Java中好像沒有提供什麼感知到作用域結束或者函式結束並觸發回撥函式的能力。我沒有想出Java中如何優雅地模擬defer。

3.4 defer in Go

我認為defer in Go是目前各種程式語言裡面實現的最為優雅的,簡單易用,符合大家使用習慣,程式碼可讀性好。defer in Go使用地如此之廣,以致於連舉個例子都是多餘,這裡就省掉示例程式碼了 :).

資源釋放是需要慎重考慮的,資源洩漏是新手程式設計師常犯的錯誤,本文從go defer的思想觸發,對比了下不同語言c、cpp、java實現defer語義的方式。

您的支援,是我繼續創作、分享知識的動力。如果您認為本文不錯,請點贊、轉發、讚賞 :)

本作品採用《CC 協議》,轉載必須註明作者和本文連結

hitzhangjie

相關文章