マルチスレッドで「libcurl」使用時の注意点

まだ夏に入ってもいないのに、最近どんどん暑くなってきてますね。
自分は暑いのが本当に苦手なので、家ではクーラーと扇風機を併用して
なんとか過ごしています。本格的な夏になったらと思うと、今からげんなりです…

それはさておき、表題の話。
少し前に、既存アプリの改修を行った際、「libcurl」使用箇所でで少し詰まった事があったので
それについてお話しします。

まず「libcurl」とは何かですが、簡単に言うと、C言語、C++で、HTTPのGETやPOST通信などを
サポートするクライアント用ライブラリです。
(HTTPの他、HTTPS、FTP、POP3、IMAPなどもサポートしているようです。詳しくはこちら(英語))

この「libcurl」、使用方法もそれほど難しくなく、普通に使う分には良い感じなのですが、
マルチスレッドで使用する際に少し注意が必要です。

以下、簡単な使用例。

// 初期化
CURL *curl = curl_easy_init();

// 各種パラメータ設定
curl_easy_setopt(curl, CURLOPT_URL, "https://xxxxx.com/");
curl_easy_setopt(curl, …, …);
      :

// 実行
curl_easy_perform(curl);

// 終了
curl_easy_cleanup(curl);

実際は、通信後のデータ受け取りなどの処理が入るためもう少し複雑になりますが、
今回そこは重要ではないので割愛します。
今回重要なのは、「初期化」「終了」処理です。

実は、上記のコードは公式で推奨されていない書き方で、本来は以下が推奨されています。

// 初期化
curl_global_init(CURL_GLOBAL_ALL);

CURL *curl = curl_easy_init();

// 各種パラメータ設定
curl_easy_setopt(curl, CURLOPT_URL, "https://xxxxx.com/");
curl_easy_setopt(curl, …, …);
      :

// 実行
curl_easy_perform(curl);

curl_easy_cleanup(curl);

// 終了
curl_global_cleanup();

最初の例では記載されていなかった「curl_global_init()」と「curl_global_cleanup()」ですが、
これが本来の初期化処理であり、「curl_easy_init()」「curl_easy_cleanup()」は
CURL オブジェクトの作成と解放をしているに過ぎません。

ただ、初期化処理を明示的に記載していない場合、
「curl_easy_init()」内部で「curl_global_init()」が、
「curl_easy_cleanup()」内部で「curl_global_cleanup()」が
呼ばれることになります。

勝手に呼んでくれるのは、まあ便利と言えば便利ですし、
実際シングルタスクであればそれもありです。

しかし「curl_global_init()」と「curl_global_cleanup()」は
「スレッドセーフ」ではない。これが問題となります。

具体的には、上記処理の初期化から終了までの間に、別スレッドから初期化処理が実行されると
「segmentation fault」が発生してアプリが落ちます。

それを回避するには、
「curl_global_init()」をアプリ開始時に1回、
「curl_global_cleanup()」をアプリ終了時に1回
実行されるような作りにすれば良いです。

最初に初期化処理を明示的に実行しておけば、
以降、勝手に実行されることは無くなり、マルチスレッドでの動作も問題が無くなります。

とまあ、分かってしまえば単純な話なのですが、
テスト中に突然「segmentation fault」が発生したときはかなり焦りました。
想定外のエラーというのはあまり遭遇したくないものです。

以上