Excel におけるマルチスレッド処理とメモリ競合

適用対象: Excel 2013 | Office 2013 | Visual Studio

Excel 2007 より前の バージョンの Microsoft Excel では、すべてのワークシートの計算に 1 つのスレッドを使用します。 ただし、Excel 2007 以降の Excel は、ワークシートの計算に 1 から 1024 の同時実行スレッドを使用するように構成できます。 マルチプロセッサまたはマルチコアのコンピューターでは、既定のスレッド数はプロセッサまたはコアの数と同じです。 したがって、スレッド セーフのセル、またはスレッド セーフの関数のみを含むセルは、同時実行スレッドに割り当てることができ、前回の計算と同様に計算する必要があるという通常の再計算ロジックが適用されます。

スレッド セーフ関数

Excel 2007 以降の組み込みのワークシート関数のほとんどはスレッド セーフです。 XLL 関数をスレッド セーフとして記述して登録することもできます。 Excel は、1 つのスレッド (メイン スレッド) を使用して、すべてのコマンド、スレッド アンセーフ関数、xlAuto 関数 (xlAutoFreexlAutoFree12 を除く)、および COM 関数と Visual Basic for Applications (VBA) 関数を呼び出します。

XLL 関数が xlbitDLLFree が設定された XLOPER または XLOPER12 を返す場合は、Excel はその関数の呼び出しが実行されたのと同じスレッドを使用して xlAutoFree または xlAutoFree12 を呼び出します。 xlAutoFree または xlAutoFree12 の呼び出しは、そのスレッドで次の関数の呼び出しが行われる前に行われます。

XLL 開発者にとって、スレッド セーフ関数を作成することは利点があります。

  • そうすることにより、Excel でマルチプロセッサまたはマルチコア のコンピューターを最大限に活用することができます。

  • 1 つのスレッドを使用して行うよりも効果的にリモート サーバーを使用できる可能性があります。

たとえば、N 個のスレッドを使用するように構成されている単一プロセッサ コンピューターがあるとします。 スプレッドシートが実行されていて、XLL 関数に対して多数の呼び出しを行い、データの要求や計算をリモート サーバーまたはサーバーのクラスターに送信するとします。 依存関係ツリーのトポロジの影響を受けて、Excel は関数 N 回をほぼ同時に呼び出すことができます。 サーバーまたはサーバーが十分に高速または並列である場合、スプレッドシートの再計算時間を 1/N ほど短縮できます。

スレッド セーフ関数を記述する際に重要なことは、リソースの競合を正しく処理することです。 通常、これはメモリの競合を意味し、次の 2 つの点に分けることができます。

  • このスレッドでのみ使用されることが分かっているメモリの作成方法。

  • 共有メモリが複数のスレッドによって必ず安全にアクセスされるようにする方法。

まず注意すべき点は、XLL ですべてのスレッドによってアクセス可能であるメモリと、現在実行中のスレッドによってのみアクセス可能なメモリです。

���ׂẴX���b�h�ɂ���ăA�N�Z�X�\�Ȃ��

  • 関数の本体の外部にある、宣言されている変数、構造体、およびクラス。

  • 関数の本体内で宣言された静的変数。

この 2 つのケースでは、DLL のこのインスタンス用に作成された DLL のメモリ ブロックにメモリが確保されます。 別のアプリケーション インスタンスが DLL を読み込む場合は、DLL のこのインスタンスの外部でこれらのリソースの競合が起きないように、そのメモリの独自のコピーを取得します。

���݂̃X���b�h�ɂ���Ă̂݃A�N�Z�X�\�Ȃ��

  • 関数のコード内の自動変数 (関数の引数を含む)。

この場合、メモリは関数呼び出しのインスタンスごとにスタックに確保されています。

注:

動的に割り当てられたメモリのスコープは、ポインターを指すポインターのスコープによって異なります。ポインターがすべてのスレッドからアクセスできる場合、メモリも同様です。 ポインターが関数内の自動変数である場合、割り当てられたメモリは実質的にそのスレッドに対してプライベートになります。

1 �̃X���b�h�ɂ���Ă̂݃A�N�Z�X�\�ȃ�����:�X���b�h ���[�J�� ������

関数の本文内の静的変数がすべてのスレッドによってアクセス可能である場合、それらを使用する関数は明らかにスレッド セーフではありません。 1 つのスレッド上にある関数の 1 つのインスタンスが値を変更している可能性があり、別のスレッド上にある別のインスタンスはそれをまったく異なるものであると仮定しています。

関数内で静的変数を宣言することには 2 つの理由があります。

  1. 静的データは、1 つの呼び出しから次の呼び出しまで維持されます。

  2. 静的データへのポインターは、関数によって安全に返されます。

1 つ目の理由の場合、関数のすべての呼び出しで維持され、なおかつ有効なデータ (おそらく、関数がスレッドに呼び出されるたびに 1 ずつ増加する単純なカウンター、またはすべての呼び出しで使用率とパフォーマンスデータを収集する構造) を保持することができます。 問題は、共有データまたはデータ構造を保護する方法です。 これを行うには、次のセクションで説明するクリティカル セクションを使用することが最善の方法です。

データがこのスレッドによる使用のみを目的としており、理由 1 の場合があり、常に理由 2 の場合である場合、問題は、保持されるが、このスレッドからのみアクセスできるメモリを作成する方法です。 1 つの解決策は、スレッド ローカル ストレージ (TLS) API を使用することです。

���Ƃ��A XLOPER �ւ̃|�C���^�[��Ԃ�����l���Ă݂܂��B

LPXLOPER12 WINAPI mtr_unsafe_example(LPXLOPER12 pxArg)
{
    static XLOPER12 xRetVal; // memory shared by all threads!!!
// code sets xRetVal to a function of pxArg ...
    return &xRetVal;
}

1 つのスレッドは静的 XLOPER12 を返すことができ、別のスレッドがそのスレッドを上書きしているため、この関数はスレッド セーフではありません。 XLOPER12xlAutoFree12 に渡される必要がある場合、このような事態が発生する可能性が大きくなります。 1 つの解決策は、XLOPER12 を割り当てて、それに対するポインターを返し、XLOPER12 メモリ自体が解放されるように xlAutoFree12 を実装することです。 この方法は、 Excel のメモリ管理に示されている関数の例の多くで使用されます。

LPXLOPER12 WINAPI mtr_safe_example_1(LPXLOPER12 pxArg)
{
// pxRetVal must be freed later by xlAutoFree12
    LPXLOPER12 pxRetVal = new XLOPER12;
// code sets pxRetVal to a function of pxArg ...
    pxRetVal->xltype |= xlbitDLLFree; // Needed for all types
    return pxRetVal; // xlAutoFree12 must free this
}

このアプローチは、次のセクションで説明するアプローチよりも簡単に実装できます。次のセクションで説明するアプローチは TLS API に依存する一方でいくつかのデメリットがあります。 まず、どの種類の XLOPER/ XLOPER12 が返される場合でも、Excel は xlAutoFree/ xlAutoFree12 を呼び出す必要があります。 次に、C API コールバック関数の呼び出しの戻り値である XLOPER/ XLOPER12 を返す際に問題があります。 XLOPER/ XLOPER12 は、Excel によって解放される必要のあるメモリを指す場合がありますが、XLOPER/ XLOPER12 そのものが割り当てられたのと同じ方法で解放される必要があります。 このような XLOPER/ XLOPER12 を XLL ワークシート関数の戻り値として使用する場合は、両方のポインターを適切な方法で開放する必要があることを xlAutoFree/ xlAutoFree12 に通知する簡単な方法がありません。 (xlbitXLFreexlbitDLLFree の両方を設定しても問題は解決されません。両方が設定された Excel での XLOPER/XLOPER12s の処理は定義されておらず、バージョンによって異なる可能性があるためです。) この問題は、XLL がワークシートに返す、Excel によって割り当てられたすべての XLOPER/XLOPER12s のディープ コピーを作成することで回避できます。

これらの制限を回避する解決策として、スレッド ローカル XLOPER/XLOPER12 を設定して返すアプローチがあります。このアプローチでは、 xlAutoFree/xlAutoFree12XLOPER/XLOPER12 ポインターそのものを開放しないことが要件となります。

LPXLOPER12 get_thread_local_xloper12(void);
LPXLOPER12 WINAPI mtr_safe_example_2(LPXLOPER12 pxArg)
{
    LPXLOPER12 pxRetVal = get_thread_local_xloper12();
// Code sets pxRetVal to a function of pxArg setting xlbitDLLFree or
// xlbitXLFree as required.
    return pxRetVal; // xlAutoFree12 must not free this pointer!
}

次の質問は、スレッド ローカル メモリを設定して取得する方法、つまり、前の例で get_thread_local_xloper12 関数を実装する方法です。 これは、スレッド ローカル ストレージ (TLS) API を使用して行われます。 最初の手順は、 TlsAlloc を使用して TLS インデックスを取得することです。最終的には TlsFree を使用して解放する必要があります。 どちらも DllMain から行うのが最適です。

// This implementation just calls a function to set up
// thread-local storage.
BOOL TLS_Action(DWORD Reason); // Could be in another module
BOOL WINAPI DllMain(HINSTANCE hDll, DWORD Reason, void *Reserved)
{
    return TLS_Action(Reason);
}
DWORD TlsIndex; // Module scope only if all TLS access in this module
BOOL TLS_Action(DWORD DllMainCallReason)
{
    switch (DllMainCallReason)
    {
    case DLL_PROCESS_ATTACH: // The DLL is being loaded.
        if((TlsIndex = TlsAlloc()) == TLS_OUT_OF_INDEXES)
            return FALSE;
        break;
    case DLL_PROCESS_DETACH: // The DLL is being unloaded.
        TlsFree(TlsIndex); // Release the TLS index.
        break;
    }
    return TRUE;
}

インデックスを取得したら、次に行う手順はスレッドごとにメモリ ブロックを割り当てることです。 Windows 開発ドキュメントでは、DLL_THREAD_ATTACH イベントで DllMain コールバック関数が呼び出されるたびにこれを実行し、DLL_THREAD_DETACHごとにメモリを解放することをお勧めします。 ただし、このアドバイスに従うと、DLL は再計算に使用されないスレッドに対して不要な作業を行うことになる可能性があります。

この方法よりも、最初に使用するときに割り当てる方法を使用することをお勧めします。 まず、スレッドごとに割り当てる構造を定義する必要があります。 XLOPERs または XLOPER12s を返す前述の例の場合は、次のとおりで十分ですが、何であれニーズを満たす構造を作成することができます。

struct TLS_data
{
    XLOPER xloper_shared_ret_val;
    XLOPER12 xloper12_shared_ret_val;
// Add other required thread-local data here...
};

次の関数は、スレッド ローカル インスタンスへのポインターを取得するか、またはこれが最初の呼び出しである場合は1 つを割り当てます。

TLS_data *get_TLS_data(void)
{
// Get a pointer to this thread's static memory.
    void *pTLS = TlsGetValue(TlsIndex);
    if(!pTLS) // No TLS memory for this thread yet
    {
        if((pTLS = calloc(1, sizeof(TLS_data))) == NULL)
        // Display some error message (omitted).
            return NULL;
        TlsSetValue(TlsIndex, pTLS); // Associate with this thread
    }
    return (TLS_data *)pTLS;
}

����ŁA�X���b�h ���[�J�� XLOPER/XLOPER12 ��������擾������@���킩��͂��ł��B�܂��A�X���b�h�� TLS_data �̃C���X�^���X�ւ̃|�C���^�[��擾���Ă���A���̂悤�ɂ��̓���Ɋ܂܂�Ă��� XLOPER/XLOPER12 �ւ̃|�C���^�[��Ԃ��܂��B

LPXLOPER get_thread_local_xloper(void)
{
    TLS_data *pTLS = get_TLS_data();
    if(pTLS)
        return &(pTLS->xloper_shared_ret_val);
    return NULL;
}
LPXLOPER12 get_thread_local_xloper12(void)
{
    TLS_data *pTLS = get_TLS_data();
    if(pTLS)
        return &(pTLS->xloper12_shared_ret_val);
    return NULL;
}

The mtr_safe_example_1 and mtr_safe_example_2 functions can be registered as thread-safe worksheet functions when you are running Excel. However, you cannot mix the two approaches in one XLL. Your XLL can only export one implementation of xlAutoFree and xlAutoFree12, and each memory strategy requires a different approach. With mtr_safe_example_1, the pointer passed to xlAutoFree/xlAutoFree12 must be freed along with any data it points to. With mtr_safe_example_2, only the pointed-to data should be freed.

Windows には、現在のスレッドの一意のシステム全体の ID を返す GetCurrentThreadId 関数も用意されています。 これも、コードをスレッド セーフにしたりコードの動作をスレッド固有にしたりするための手段になります。

�����̃X���b�h�ɂ���ăA�N�Z�X�\�ȃ�����:�N���e�B�J�� �Z�N�V����

クリティカル セクションを使用して複数のスレッドがアクセスできる読み取り/書き込みメモリを保護する必要があります。 保護するメモリのブロックごとに指定されたクリティカル セクションが必要となります。 これらは xlAutoOpen 関数の呼び出し中に初期化でき、また xlAutoClose 関数の呼び出し中に、解放したり、null に設定したりすることができます。 EnterCriticalSectionLeaveCriticalSection の呼び出しのペア内に保護されたブロックへの各アクセスを含める必要があります。 クリティカル セクションへのアクセスを許可されるのは、常に 1 つのスレッドのみです。 以下に、g_csSharedTable と呼ばれるセクションの初期化、初期化解除、および使用の例を示します。

CRITICAL_SECTION g_csSharedTable; // global scope (if required)
bool xll_initialised = false; // Only module scope needed
int WINAPI xlAutoOpen(void)
{
    if(xll_initialised)
        return 1;
// Other initialisation omitted
    InitializeCriticalSection(&g_csSharedTable);
    xll_initialised = true;
    return 1;
}
int WINAPI xlAutoClose(void)
{
    if(!xll_initialised)
        return 1;
// Other cleaning up omitted.
    DeleteCriticalSection(&g_csSharedTable);
    xll_initialised = false;
    return 1;
}
#define SHARED_TABLE_SIZE 1000 /* Some value consistent with the table */
bool read_shared_table_element(unsigned int index, double &d)
{
    if(index >= SHARED_TABLE_SIZE) return false;
    EnterCriticalSection(&g_csSharedTable);
    d = shared_table[index];
    LeaveCriticalSection(&g_csSharedTable);
    return true;
}
bool set_shared_table_element(unsigned int index, double d)
{
    if(index >= SHARED_TABLE_SIZE) return false;
    EnterCriticalSection(&g_csSharedTable);
    shared_table[index] = d;
    LeaveCriticalSection(&g_csSharedTable);
    return true;
}

メモリ ブロックを保護するもう 1 つの安全な方法は、独自の CRITICAL_SECTION を含み、コンストラクター、デストラクター、アクセサー メソッドがその使用を処理するクラスを作成することです。 この方法では、 xlAutoOpen が実行される前に初期化される可能性があるオブジェクトを保護する、または xlAutoClose が呼び出された後も存続する可能性があるオブジェクトを保護するという利点が追加されていますが、重要なセクションと、これが作成するオペレーティング システムオーバーヘッドが多すぎることに注意する必要があります。

保護されたメモリの複数のブロックに同時にアクセスする必要があるコードがある場合は、クリティカル セクションの開始と終了の順序を非常に慎重に検討する必要があります。 たとえば、次の 2 つの関数はデッドロックを作成する可能性があります。

// WARNING: Do not copy this code. These two functions
// can produce a deadlock and are provided for
// example and illustration only.
bool copy_shared_table_element_A_to_B(unsigned int index)
{
    if(index >= SHARED_TABLE_SIZE) return false;
    EnterCriticalSection(&g_csSharedTableA);
    EnterCriticalSection(&g_csSharedTableB);
    shared_table_B[index] = shared_table_A[index];
// Critical sections should be exited in the order
// they were entered, NOT as shown here in this
// deliberately wrong illustration.
    LeaveCriticalSection(&g_csSharedTableA);
    LeaveCriticalSection(&g_csSharedTableB);
    return true;
}
bool copy_shared_table_element_B_to_A(unsigned int index)
{
    if(index >= SHARED_TABLE_SIZE) return false;
    EnterCriticalSection(&g_csSharedTableB);
    EnterCriticalSection(&g_csSharedTableA);
    shared_table_A[index] = shared_table_B[index];
    LeaveCriticalSection(&g_csSharedTableA);
    LeaveCriticalSection(&g_csSharedTableB);
    return true;
}

1 つのスレッドの最初の関数で g_csSharedTableA を開始し、別のスレッドの2 番目の関数で g_csSharedTableB を開始すると、両方のスレッドがハングします。 次のように、一貫した順序で開始して、その逆の順序で終了するのが正しいアプローチです。

    EnterCriticalSection(&g_csSharedTableA);
    EnterCriticalSection(&g_csSharedTableB);
    // code that accesses both blocks
    LeaveCriticalSection(&g_csSharedTableB);
    LeaveCriticalSection(&g_csSharedTableA);

可能であれば、スレッド連携の観点から、次に示すように、個別のブロックにアクセスを分離することをお勧めします。

bool copy_shared_table_element_A_to_B(unsigned int index)
{
    if(index >= SHARED_TABLE_SIZE) return false;
    EnterCriticalSection(&g_csSharedTableA);
    double d = shared_table_A[index];
    LeaveCriticalSection(&g_csSharedTableA);
    EnterCriticalSection(&g_csSharedTableB);
    shared_table_B[index] = d;
    LeaveCriticalSection(&g_csSharedTableB);
    return true;
}

頻繁な短時間のアクセス要求など、共有リソースの競合が多い場合は、クリティカル セクションのスピン機能の使用を検討する必要があります。 これは、リソースの待機によるプロセッサへの負荷をなるべく小さくするための手法です。 これを行うためには、セクションの初期化時に InitializeCriticalSectionAndSpinCount、もしくは初期化されてからは SetCriticalSectionSpinCount を使用して、リソースが使用可能になるのを待機する前にスレッドがループする回数を設定できます。 待機操作にはコストがかかるため、その間にリソースが解放されると、スピンによって待機操作は回避されます。 シングル プロセッサ システムでは、スピン数は効果的に無視されますが、スピン数を指定していても問題になることはありません。 メモリ ヒープ マネージャーで使用するスピン数は 4000 です。 クリティカル セクションの使用に関する詳細については、Windows SDK のドキュメントをご参照ください。

関連項目

Excel のメモリ管理

Excel でのマルチスレッド再計算

アドイン マネージャーと XLL インターフェイス関数