GetTempFileName() をご利用いただく場合の注意点

Platform SDK (Windows SDK) サポートの平田です。11 月になり、ちらちら紅葉も見られるようになってきました。私の家は高尾に比較的近いところにあるため、この時期の高尾山を見に行くのを楽しみにしております。

本日は、GetTempFileName() をご紹介させてください。こちらの API は、一時ファイルを作成するときに便利な関数です。私も仕事柄サンプルを作ったりすることが多いのですが、大体作成するログファイルの名前は
“log.tmp” です。当然、サンプルを動作させる場合に、同じファイルに上書きさせてしまったことがあります。その結果、出力した結果を後日確認できなくなったりして困ったりすることがありました。皆様も私と同じ失敗をしないように、この API を利用してみてください。

GetTempFileName
https://msdn.microsoft.com/ja-jp/library/cc429354.aspx

UINTGetTempFileName(  LPCTSTR lpPathName, // ディレクトリ名  LPCTSTR lpPrefixString, // ファイル名の接頭辞  UINT uUnique, // 整数  LPTSTR lpTempFileName // ファイル名を格納するバッファ);

一時ファイル名を作成するための規則をそれぞれの引数で指定することができ、lpPathName には、出力を行うフォルダのパスを設定します。ちなみに、”.” と設定するとカレントディレクトリに出力されます。lpPrefixString には、ファイルの接頭辞をつけます。たとえば、hir と設定した場合、出力されるファイル名は、”hir1111.tmp”
といった形で先頭3 文字を設定できます。uUnique に設定する値をもとに16 進文字列が生成されますが、0 を設定するのがこの API をご利用いただく一番のメリットとなります。0 を設定した場合には、一意なファイルが作成されます。0 を設定された場合は、結果は、hir1111.tmp だったり、hir1112.tmp だったりします。この結果の文字列が lpTempFileName
が指すバッファに書き込みが行われます。

こんな動作をする
GetTempFileName() の動作メカニズムについて、以下にまとめてみました。

 

GetTempFileName() の動作メカニズム

一番利用されるシナリオと考えられる uUnique パラメーターにて 0 が設定されている場合に限定しますと、このようなメカニズムで一意なファイル名を生成します

a) ミリ秒単位で取得できるシステム起動からの経過時間に、0 から 1 ずつ加算されるstatic ローカル変数を加算する。

b) a) の static ローカル変数に対して、1 加算する。

c) 加算結果の下位16ビットを使って、lpPrefixString で設定された接頭辞+16 進文字列を作成する

d) c) で生成された文字列をファイル名としてCreateFile() でファイル ハンドルを取得し、同じファイルが存在していないか確認する。

e) d) でファイルハンドルが取得できない場合、a) からやり直しを行う。

* 65535 回リトライを実施して取得できない場合は一時ファイル名の作成に失敗したとして 0 を返します。これにより、無限ループを防いでいます。

 

GetTempFileName() の注意点

メカニズムを紐解いた結果による注意点について、まとめました。ご利用いただく際にご留意ください。

 

出力ファイルの16 進数部が、 0 ~ 0xFFFF までのファイルが既に存在する場合

65535 回のリトライを行いますが、すでにすべてのパターンが埋まっている場合には、この関数は失敗します。自動で削除はしてくれませんので、0 が返ってきてしまった場合は、lpPathName に違うパスを設定するなどの対処が必要です。おすすめとしては、日にちを設定したディレクトリを作成して、そこを設定するのがよいかもしれません。そうすれば、一日あたり、65535 までという制限になりますが、毎日少しずつ情報が蓄積できます。

 

GetTempFileName() を呼び出した直後に DeleteFile() を呼び出した場合

同一のシステム時刻内に複数のスレッドまたはプロセスから GetTempFileName() を呼び出した直後に DeleteFile() を呼び出し、作成した一時ファイルをすぐに削除した場合、ファイル名が一意とならない場合があります。一時ファイルの作成および削除を連続して行った場合、その名前が再利用される可能性があります。この現象は処理性能の高い環境ほど発生しやすい傾向にあります。

 

マルチスレッドで動作させた場合

マルチスレッドで動作するアプリケーションでこのAPIを使う場合、以下の 3 つのすべての条件を満たした場合はファイル名が一意となりません。これら3つの条件すべてを満たす頻度は低いと思いますが、回避するためにはクリティカルセクションによる排他処理を入れてみるまたは、Interlocked 系の関数を使うことで、呼び出し時に排他するとよいでしょう。

 

<条件 1>

システム起動からの経過時間をミリ秒単位で取得しようとしても、システムタイマの分解能以上の精度を出すことが出来ません。そのため、複数のスレッドから同時に呼ばれた場合は同じ経過時間となる可能性があります。

 

<条件 2>

static ローカル変数に対する演算に対しては排他制御をおこなっていません。そのため、同タイミングにて、参照・更新が行われた場合、同じ値を参照する可能性があります。

 

<条件 3>

既に存在するファイル名に対して CreateFile() を呼んでGetLastError() の戻り値が ERROR_FILE_EXISTS となればリトライの対象なりますが、CreateFile() の処理のタイミングが重複した場合に、ハンドルが取れてしまった場合、関数の戻り値として同値となる可能性があります。

 

回避サンプル

このサンプルでは、クリティカル セクションを使ってスレッド間の排他制御を行っています。

 #include "stdafx.h"
 
 CRITICAL_SECTION cs;
 unsigned __stdcall SecondThreadFunc(void* pArguments)
 {
 
 ::EnterCriticalSection(&cs);
 
 TCHAR szFileName[MAX_PATH];
 UINT ret = GetTempFileName(L"c:\\temp", L"tmp", 0, szFileName);
 if (0 == ret)
 {
 wprintf(L"Error %d ...\n", GetLastError());
 }
 
 ::LeaveCriticalSection(&cs);
 
 _endthreadex(0);
 return 0;
 }
 
 #define MAX_COUNT 1000
 
 int _tmain(int argc, _TCHAR* argv[])
 {
 HANDLE hThread[MAX_COUNT];
 unsigned threadID;
 
 ::InitializeCriticalSection(&cs);
 
 
 // Create the second thread.
 for (int i = 0; i < MAX_COUNT; i++)
 {
 hThread[i] = (HANDLE)_beginthreadex(NULL, 0, &SecondThreadFunc, NULL, 0, &threadID);
 if (0 == hThread[i])
 {
 printf("_beginthreadex error\n");
 }
 }
 
 for (int i = 0; i < MAX_COUNT; i++)
 {
 WaitForSingleObject(hThread[i], INFINITE);
 }
 
 return 0;
 }
 

また、以下のヘッダファイルを stdafx.h として保存してプロジェクトに入れてください。

 #pragma once
 
 #include "targetver.h"
 
 #include <stdio.h>
 #include <tchar.h>
 #include <Windows.h>
 #include <WinBase.h>
 #include <process.h>