C++で学ぶ!ファイル入出力とコンカレントデータ処理の基礎

C++は高性能で柔軟なプログラミング言語として、多くの場面で使用されています。その中でも、ファイル入出力とコンカレントデータ処理は重要な技術です。本記事では、C++を使ったファイル入出力の基礎から、スレッドを活用したコンカレントデータ処理の方法までを詳細に解説します。実例や演習問題を通じて、これらの技術を実践的に学びましょう。

目次

ファイル入出力の基礎

C++でのファイル入出力は、プログラムが外部データを読み書きするために必要な基本的な操作です。標準ライブラリのfstreamを使って、ファイルの読み書きを行います。

fstreamライブラリの導入

C++でファイル操作を行うためには、fstreamライブラリをインクルードする必要があります。

#include <fstream>

ファイルのオープン

ファイルを開くには、std::ifstream(入力ファイルストリーム)またはstd::ofstream(出力ファイルストリーム)を使用します。

std::ifstream inFile("input.txt");
std::ofstream outFile("output.txt");

ファイルの読み書き

ファイルからデータを読み込むには、std::ifstreamオブジェクトを使用し、データを書き込むにはstd::ofstreamオブジェクトを使用します。

std::string data;
inFile >> data; // ファイルからデータを読み込む
outFile << "Hello, World!"; // ファイルにデータを書き込む

ファイルのクローズ

ファイル操作が完了したら、ファイルを閉じる必要があります。

inFile.close();
outFile.close();

このセクションでは、C++における基本的なファイル入出力の手法について学びました。次のセクションでは、具体的なコード例を通じて、ファイル入出力の実際の使い方をさらに詳しく見ていきます。

ファイル入出力の実例

ここでは、C++でのファイル入出力の具体的な例を見ていきます。サンプルコードを使って、実際にファイルを読み書きする方法を詳細に解説します。

テキストファイルの読み込み

次のコードは、テキストファイルから内容を読み込む例です。

#include <iostream>
#include <fstream>
#include <string>

int main() {
    std::ifstream inFile("example.txt");
    if (!inFile) {
        std::cerr << "ファイルを開くことができませんでした。" << std::endl;
        return 1;
    }

    std::string line;
    while (std::getline(inFile, line)) {
        std::cout << line << std::endl;
    }

    inFile.close();
    return 0;
}

このコードは、example.txtというファイルを開き、1行ずつ読み込んでコンソールに出力します。

テキストファイルへの書き込み

次のコードは、テキストファイルにデータを書き込む例です。

#include <iostream>
#include <fstream>

int main() {
    std::ofstream outFile("output.txt");
    if (!outFile) {
        std::cerr << "ファイルを開くことができませんでした。" << std::endl;
        return 1;
    }

    outFile << "これはテストファイルです。" << std::endl;
    outFile << "複数行のデータを書き込むことができます。" << std::endl;

    outFile.close();
    return 0;
}

このコードは、output.txtというファイルにテキストデータを書き込みます。

バイナリファイルの読み込みと書き込み

テキストファイルだけでなく、バイナリファイルも扱うことができます。

#include <iostream>
#include <fstream>

int main() {
    // バイナリファイルへの書き込み
    std::ofstream outFile("binary.dat", std::ios::binary);
    int number = 12345;
    outFile.write(reinterpret_cast<char*>(&number), sizeof(number));
    outFile.close();

    // バイナリファイルからの読み込み
    std::ifstream inFile("binary.dat", std::ios::binary);
    int readNumber;
    inFile.read(reinterpret_cast<char*>(&readNumber), sizeof(readNumber));
    inFile.close();

    std::cout << "読み込んだ数値: " << readNumber << std::endl;

    return 0;
}

このコードは、整数をバイナリファイルに書き込み、再度読み込む例です。

これらの例を通じて、C++でのファイル入出力の基本操作を理解できたと思います。次のセクションでは、ファイル入出力時に発生する可能性のあるエラーとその対処方法について説明します。

エラーハンドリング

ファイル入出力を行う際には、エラーが発生する可能性があるため、それに対処するための方法を知っておくことが重要です。ここでは、一般的なエラーとその対処方法について説明します。

ファイルのオープンに失敗した場合

ファイルを開く際にファイルが存在しない、またはアクセス権がない場合、オープンに失敗することがあります。これを検出するために、ストリームオブジェクトをチェックします。

std::ifstream inFile("nonexistent.txt");
if (!inFile) {
    std::cerr << "ファイルを開くことができませんでした。" << std::endl;
}

ファイルが正常に開けなかった場合は、エラーメッセージを出力します。

ファイル読み書き中のエラー

ファイルの読み書き中にエラーが発生することもあります。これを検出するために、ストリームの状態をチェックします。

std::ifstream inFile("example.txt");
std::string data;
if (!(inFile >> data)) {
    std::cerr << "データの読み込み中にエラーが発生しました。" << std::endl;
}

読み込み操作が失敗した場合、エラーメッセージを出力します。

エラー後のストリーム状態のリセット

ストリームオブジェクトのエラー状態をリセットする方法も知っておくと便利です。以下のコードは、エラー後にストリームをリセットする方法を示しています。

std::ifstream inFile("example.txt");
std::string data;
inFile >> data;

if (inFile.fail()) {
    inFile.clear(); // ストリームのエラー状態をクリア
    inFile.ignore(std::numeric_limits<std::streamsize>::max(), '\n'); // バッファをクリア
}

この例では、読み込みエラーが発生した場合に、ストリームをクリアして次の操作が正常に行えるようにします。

例外を使ったエラーハンドリング

C++では例外を使ってエラーハンドリングを行うこともできます。std::fstreamは例外をスローするように設定できます。

#include <iostream>
#include <fstream>
#include <stdexcept>

int main() {
    try {
        std::ifstream inFile("example.txt");
        if (!inFile) throw std::runtime_error("ファイルを開くことができませんでした。");

        std::string data;
        inFile >> data;
        if (inFile.fail()) throw std::runtime_error("データの読み込み中にエラーが発生しました。");

        std::cout << data << std::endl;
    } catch (const std::exception& e) {
        std::cerr << "エラー: " << e.what() << std::endl;
    }

    return 0;
}

この例では、std::runtime_errorを使用してエラーをキャッチし、適切なメッセージを出力します。

これで、C++におけるファイル入出力のエラーハンドリングの基本を学びました。次のセクションでは、コンカレントデータ処理の基本概念とその重要性について解説します。

コンカレントデータ処理とは

コンカレントデータ処理は、複数の計算を同時に実行する手法で、現代のマルチコアプロセッサの性能を最大限に引き出すために重要です。このセクションでは、コンカレントデータ処理の基本概念とその重要性について説明します。

コンカレントデータ処理の基本概念

コンカレントデータ処理とは、同時に複数の計算やタスクを並行して実行する技術です。これにより、プログラムの実行速度を向上させることができます。一般的に、コンカレントデータ処理は次のような形で行われます:

  • 並列処理:複数のプロセッサやコアを使って、タスクを同時に実行します。
  • マルチスレッド処理:一つのプログラム内で複数のスレッドを使い、タスクを並行して実行します。

コンカレントデータ処理の重要性

現代のコンピュータシステムでは、マルチコアプロセッサが標準となっており、これらのコアを効率的に利用するためにはコンカレントデータ処理が不可欠です。具体的には以下のような利点があります:

  • 性能向上:複数のタスクを並行して処理することで、全体の処理時間を短縮します。
  • リソースの有効活用:CPUやメモリなどのリソースを効率的に利用できます。
  • スケーラビリティ:プログラムがより多くのタスクを処理できるようになります。

例:ウェブサーバのコンカレント処理

ウェブサーバは、多数のクライアントからのリクエストを同時に処理する必要があります。コンカレントデータ処理を用いることで、複数のリクエストを同時に処理し、サーバの応答速度を向上させることができます。

#include <iostream>
#include <thread>
#include <vector>

void handleRequest(int requestId) {
    std::cout << "Processing request " << requestId << std::endl;
    // リクエスト処理のシミュレーション
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::cout << "Request " << requestId << " completed." << std::endl;
}

int main() {
    std::vector<std::thread> threads;
    for (int i = 0; i < 5; ++i) {
        threads.push_back(std::thread(handleRequest, i));
    }

    for (auto& thread : threads) {
        thread.join();
    }

    return 0;
}

この例では、5つのクライアントリクエストを同時に処理するために、5つのスレッドを作成しています。それぞれのスレッドは、handleRequest関数を実行します。

スレッドとプロセスの違い

コンカレントデータ処理には、スレッドとプロセスの概念があります。スレッドは同一プロセス内で動作し、メモリを共有しますが、プロセスは独立したメモリ空間を持ちます。スレッドは軽量で作成や切り替えが速いですが、デッドロックや競合状態を避けるための管理が必要です。

次のセクションでは、C++でのスレッドプログラミングの基礎を紹介します。これにより、実際にコンカレントデータ処理を実装するための知識を深めることができます。

C++でのスレッドプログラミング

スレッドプログラミングは、コンカレントデータ処理を実現するための重要な技術です。このセクションでは、C++でスレッドを使ってプログラムを並行して実行する方法について解説します。

スレッドの基本

C++11からは標準ライブラリでスレッドをサポートしています。&lt;thread&gt;ヘッダーをインクルードすることでスレッドを利用できます。

#include <iostream>
#include <thread>

void printMessage() {
    std::cout << "Hello from thread!" << std::endl;
}

int main() {
    std::thread t(printMessage);
    t.join(); // スレッドが終了するまで待機
    return 0;
}

このコードは、新しいスレッドを作成し、printMessage関数を実行します。joinメソッドは、スレッドが終了するまでメインスレッドを待機させます。

パラメータ付きスレッド

スレッドにパラメータを渡すこともできます。

#include <iostream>
#include <thread>

void printNumber(int number) {
    std::cout << "Number: " << number << std::endl;
}

int main() {
    std::thread t(printNumber, 42);
    t.join();
    return 0;
}

この例では、printNumber関数にパラメータ42を渡して実行しています。

複数のスレッドを使用する

複数のスレッドを同時に作成して並行処理を行うことができます。

#include <iostream>
#include <thread>
#include <vector>

void workerFunction(int id) {
    std::cout << "Worker " << id << " is processing" << std::endl;
}

int main() {
    std::vector<std::thread> workers;
    for (int i = 0; i < 5; ++i) {
        workers.emplace_back(workerFunction, i);
    }

    for (auto& worker : workers) {
        worker.join();
    }

    return 0;
}

このコードでは、5つのスレッドを作成し、それぞれworkerFunctionを実行します。

スレッドの同期

複数のスレッドが同時にデータにアクセスする場合、データ競合が発生する可能性があります。これを防ぐために、スレッドの同期が必要です。C++ではstd::mutexを使って同期を実現します。

#include <iostream>
#include <thread>
#include <mutex>

std::mutex mtx;

void printSafeMessage(const std::string& message) {
    std::lock_guard<std::mutex> lock(mtx);
    std::cout << message << std::endl;
}

int main() {
    std::thread t1(printSafeMessage, "Thread 1");
    std::thread t2(printSafeMessage, "Thread 2");

    t1.join();
    t2.join();

    return 0;
}

この例では、std::mutexを使って、printSafeMessage関数内の標準出力操作をスレッドセーフにしています。

次のセクションでは、具体的なコード例を使って、スレッドの使い方をさらに詳しく説明します。これにより、実践的なスレッドプログラミングのスキルを身につけることができます。

スレッドの使い方

ここでは、C++におけるスレッドの具体的な使い方をさらに詳しく見ていきます。実際のプログラムにおいてスレッドをどのように活用するかを理解しましょう。

スレッドのライフサイクル

スレッドのライフサイクルは、以下のステップで構成されます:

  1. スレッドの生成:スレッドオブジェクトを作成し、実行する関数を指定します。
  2. スレッドの実行:スレッドが開始され、指定された関数を実行します。
  3. スレッドの終了:関数の実行が完了すると、スレッドは終了します。
  4. スレッドの結合:メインスレッドがスレッドの終了を待ちます(join)。

スレッドを使った計算の分散

次のコードは、スレッドを使って大規模な計算を分散処理する例です。

#include <iostream>
#include <thread>
#include <vector>

void partialSum(int start, int end, long long &result) {
    result = 0;
    for (int i = start; i <= end; ++i) {
        result += i;
    }
}

int main() {
    const int numThreads = 4;
    std::vector<std::thread> threads;
    std::vector<long long> results(numThreads);

    int range = 1000000;
    int step = range / numThreads;

    for (int i = 0; i < numThreads; ++i) {
        int start = i * step + 1;
        int end = (i + 1) * step;
        threads.emplace_back(partialSum, start, end, std::ref(results[i]));
    }

    long long totalSum = 0;
    for (int i = 0; i < numThreads; ++i) {
        threads[i].join();
        totalSum += results[i];
    }

    std::cout << "Total sum: " << totalSum << std::endl;

    return 0;
}

この例では、4つのスレッドを使って1から1,000,000までの数の合計を計算しています。それぞれのスレッドが部分範囲の合計を計算し、最終的に全体の合計を求めます。

スレッドプールの実装

スレッドプールは、複数のスレッドをプールし、タスクを分散して実行するための技術です。これにより、スレッドの生成と破棄のオーバーヘッドを削減できます。

#include <iostream>
#include <vector>
#include <thread>
#include <queue>
#include <functional>
#include <condition_variable>

class ThreadPool {
public:
    ThreadPool(size_t numThreads);
    ~ThreadPool();
    void enqueueTask(std::function<void()> task);

private:
    std::vector<std::thread> workers;
    std::queue<std::function<void()>> tasks;
    std::mutex queueMutex;
    std::condition_variable condition;
    bool stop;

    void worker();
};

ThreadPool::ThreadPool(size_t numThreads) : stop(false) {
    for (size_t i = 0; i < numThreads; ++i) {
        workers.emplace_back([this] { worker(); });
    }
}

ThreadPool::~ThreadPool() {
    {
        std::unique_lock<std::mutex> lock(queueMutex);
        stop = true;
    }
    condition.notify_all();
    for (std::thread &worker : workers) {
        worker.join();
    }
}

void ThreadPool::enqueueTask(std::function<void()> task) {
    {
        std::unique_lock<std::mutex> lock(queueMutex);
        tasks.push(std::move(task));
    }
    condition.notify_one();
}

void ThreadPool::worker() {
    while (true) {
        std::function<void()> task;
        {
            std::unique_lock<std::mutex> lock(queueMutex);
            condition.wait(lock, [this] { return stop || !tasks.empty(); });
            if (stop && tasks.empty()) {
                return;
            }
            task = std::move(tasks.front());
            tasks.pop();
        }
        task();
    }
}

int main() {
    ThreadPool pool(4);
    for (int i = 0; i < 8; ++i) {
        pool.enqueueTask([i] {
            std::cout << "Task " << i << " is being processed by thread "
                      << std::this_thread::get_id() << std::endl;
            std::this_thread::sleep_for(std::chrono::seconds(1));
        });
    }
    std::this_thread::sleep_for(std::chrono::seconds(5));
    return 0;
}

この例では、スレッドプールを実装し、8つのタスクを4つのスレッドで処理しています。

これらの例を通じて、C++におけるスレッドの具体的な使い方を学びました。次のセクションでは、マルチスレッドによるファイル処理について詳しく見ていきます。

マルチスレッドによるファイル処理

マルチスレッドを活用することで、ファイル処理の効率を大幅に向上させることができます。このセクションでは、C++でマルチスレッドを用いたファイル処理の具体例を紹介します。

マルチスレッドを使ったファイルの読み込み

大きなファイルを複数のスレッドで並行して読み込むことで、読み込み速度を向上させることができます。次のコードは、ファイルを複数のスレッドで並行して読み込む例です。

#include <iostream>
#include <fstream>
#include <thread>
#include <vector>
#include <mutex>

std::mutex mtx;

void readChunk(const std::string& filename, std::streampos start, std::streampos end, std::vector<std::string>& lines) {
    std::ifstream inFile(filename);
    if (!inFile) {
        std::cerr << "ファイルを開くことができませんでした。" << std::endl;
        return;
    }

    inFile.seekg(start);
    std::string line;
    while (inFile.tellg() < end && std::getline(inFile, line)) {
        std::lock_guard<std::mutex> lock(mtx);
        lines.push_back(line);
    }
}

int main() {
    const std::string filename = "largefile.txt";
    const int numThreads = 4;
    std::vector<std::thread> threads;
    std::vector<std::string> lines;
    std::ifstream inFile(filename, std::ios::ate);
    std::streampos fileSize = inFile.tellg();
    std::streampos chunkSize = fileSize / numThreads;

    for (int i = 0; i < numThreads; ++i) {
        std::streampos start = i * chunkSize;
        std::streampos end = (i == numThreads - 1) ? fileSize : start + chunkSize;
        threads.emplace_back(readChunk, filename, start, end, std::ref(lines));
    }

    for (auto& thread : threads) {
        thread.join();
    }

    std::cout << "読み込んだ行数: " << lines.size() << std::endl;

    return 0;
}

このコードでは、大きなファイルを4つのチャンクに分割し、それぞれのチャンクを異なるスレッドで読み込みます。各スレッドはファイルの指定された範囲を読み込み、その結果を共通のベクターに保存します。

マルチスレッドを使ったファイルの書き込み

次に、複数のスレッドを使ってデータを並行してファイルに書き込む例を見てみましょう。

#include <iostream>
#include <fstream>
#include <thread>
#include <vector>
#include <mutex>

std::mutex writeMtx;

void writeChunk(const std::string& filename, const std::vector<std::string>& data, int start, int end) {
    std::ofstream outFile(filename, std::ios::app);
    if (!outFile) {
        std::cerr << "ファイルを開くことができませんでした。" << std::endl;
        return;
    }

    for (int i = start; i < end; ++i) {
        std::lock_guard<std::mutex> lock(writeMtx);
        outFile << data[i] << std::endl;
    }
}

int main() {
    const std::string filename = "outputfile.txt";
    const int numThreads = 4;
    std::vector<std::thread> threads;
    std::vector<std::string> data = {"行1", "行2", "行3", "行4", "行5", "行6", "行7", "行8"};
    int chunkSize = data.size() / numThreads;

    for (int i = 0; i < numThreads; ++i) {
        int start = i * chunkSize;
        int end = (i == numThreads - 1) ? data.size() : start + chunkSize;
        threads.emplace_back(writeChunk, filename, std::ref(data), start, end);
    }

    for (auto& thread : threads) {
        thread.join();
    }

    std::cout << "書き込みが完了しました。" << std::endl;

    return 0;
}

この例では、データベクターを4つのチャンクに分割し、複数のスレッドでファイルに並行して書き込みます。各スレッドはデータの指定された範囲をファイルに書き込みます。

注意点

マルチスレッドによるファイル処理を行う際には、データの一貫性を保つために適切な同期が重要です。上記の例では、std::mutexを使用してファイルへの同時アクセスを防ぎ、安全にデータを読み書きしています。

これで、マルチスレッドによる効率的なファイル処理の基本が理解できたと思います。次のセクションでは、デッドロックや競合状態を防ぐための対策とその実装方法について説明します。

デッドロックと競合状態の対策

マルチスレッドプログラミングにおいて、デッドロックや競合状態を防ぐことは非常に重要です。このセクションでは、これらの問題を防ぐための対策とその実装方法について説明します。

デッドロックとは

デッドロックは、複数のスレッドが互いにリソースの解放を待つ状態のことです。これにより、スレッドが無限に待ち続け、プログラムが停止してしまいます。

デッドロックの例

以下のコードは、デッドロックの例を示しています。

#include <iostream>
#include <thread>
#include <mutex>

std::mutex mtx1;
std::mutex mtx2;

void threadFunction1() {
    std::lock_guard<std::mutex> lock1(mtx1);
    std::this_thread::sleep_for(std::chrono::milliseconds(100));
    std::lock_guard<std::mutex> lock2(mtx2);
    std::cout << "Thread 1 finished" << std::endl;
}

void threadFunction2() {
    std::lock_guard<std::mutex> lock2(mtx2);
    std::this_thread::sleep_for(std::chrono::milliseconds(100));
    std::lock_guard<std::mutex> lock1(mtx1);
    std::cout << "Thread 2 finished" << std::endl;
}

int main() {
    std::thread t1(threadFunction1);
    std::thread t2(threadFunction2);

    t1.join();
    t2.join();

    return 0;
}

このコードでは、threadFunction1threadFunction2が互いに異なる順序でミューテックスを取得しようとするため、デッドロックが発生します。

デッドロックを防ぐ方法

デッドロックを防ぐためには、次のような方法があります:

1. ミューテックスの順序を統一する

常に同じ順序でミューテックスを取得することで、デッドロックを防ぐことができます。

void threadFunction1() {
    std::lock(mtx1, mtx2);
    std::lock_guard<std::mutex> lock1(mtx1, std::adopt_lock);
    std::lock_guard<std::mutex> lock2(mtx2, std::adopt_lock);
    std::cout << "Thread 1 finished" << std::endl;
}

void threadFunction2() {
    std::lock(mtx1, mtx2);
    std::lock_guard<std::mutex> lock1(mtx1, std::adopt_lock);
    std::lock_guard<std::mutex> lock2(mtx2, std::adopt_lock);
    std::cout << "Thread 2 finished" << std::endl;
}

このコードでは、std::lockを使って同時にミューテックスを取得し、std::adopt_lockを使ってロックを引き継ぎます。

2. `std::unique_lock`を使用する

std::unique_lockは、デッドロックを防ぐために便利な機能を提供します。

void threadFunction1() {
    std::unique_lock<std::mutex> lock1(mtx1, std::defer_lock);
    std::unique_lock<std::mutex> lock2(mtx2, std::defer_lock);
    std::lock(lock1, lock2);
    std::cout << "Thread 1 finished" << std::endl;
}

void threadFunction2() {
    std::unique_lock<std::mutex> lock1(mtx1, std::defer_lock);
    std::unique_lock<std::mutex> lock2(mtx2, std::defer_lock);
    std::lock(lock1, lock2);
    std::cout << "Thread 2 finished" << std::endl;
}

このコードでは、std::defer_lockを使ってロックの取得を遅延させ、std::lockで同時にロックを取得します。

競合状態とは

競合状態は、複数のスレッドが同じデータに同時にアクセスすることでデータの整合性が失われる状態のことです。

競合状態の例

以下のコードは、競合状態の例を示しています。

#include <iostream>
#include <thread>

int counter = 0;

void incrementCounter() {
    for (int i = 0; i < 1000000; ++i) {
        ++counter;
    }
}

int main() {
    std::thread t1(incrementCounter);
    std::thread t2(incrementCounter);

    t1.join();
    t2.join();

    std::cout << "Counter: " << counter << std::endl;

    return 0;
}

このコードでは、複数のスレッドが同時にcounterをインクリメントするため、予期しない結果になります。

競合状態を防ぐ方法

競合状態を防ぐためには、共有データへのアクセスをミューテックスで保護する必要があります。

#include <iostream>
#include <thread>
#include <mutex>

int counter = 0;
std::mutex counterMtx;

void incrementCounter() {
    for (int i = 0; i < 1000000; ++i) {
        std::lock_guard<std::mutex> lock(counterMtx);
        ++counter;
    }
}

int main() {
    std::thread t1(incrementCounter);
    std::thread t2(incrementCounter);

    t1.join();
    t2.join();

    std::cout << "Counter: " << counter << std::endl;

    return 0;
}

このコードでは、std::lock_guardを使ってcounterへのアクセスを保護しています。

これで、デッドロックと競合状態を防ぐための基本的な対策を学びました。次のセクションでは、学んだ内容を応用した例や演習問題を紹介します。

応用例と演習問題

ここでは、これまで学んだファイル入出力とコンカレントデータ処理の知識を応用した例や、理解を深めるための演習問題を紹介します。

応用例:マルチスレッドによるログファイルの解析

大量のログファイルを解析する際、マルチスレッドを使用して効率的に処理することができます。次のコードは、複数のスレッドを使ってログファイルの解析を行う例です。

#include <iostream>
#include <fstream>
#include <thread>
#include <vector>
#include <mutex>
#include <string>

std::mutex mtx;

void parseLogFile(const std::string& filename, std::streampos start, std::streampos end, int& errorCount) {
    std::ifstream logFile(filename);
    if (!logFile) {
        std::cerr << "ファイルを開くことができませんでした。" << std::endl;
        return;
    }

    logFile.seekg(start);
    std::string line;
    int localErrorCount = 0;
    while (logFile.tellg() < end && std::getline(logFile, line)) {
        if (line.find("ERROR") != std::string::npos) {
            ++localErrorCount;
        }
    }

    std::lock_guard<std::mutex> lock(mtx);
    errorCount += localErrorCount;
}

int main() {
    const std::string filename = "logfile.txt";
    const int numThreads = 4;
    std::vector<std::thread> threads;
    int errorCount = 0;
    std::ifstream logFile(filename, std::ios::ate);
    std::streampos fileSize = logFile.tellg();
    std::streampos chunkSize = fileSize / numThreads;

    for (int i = 0; i < numThreads; ++i) {
        std::streampos start = i * chunkSize;
        std::streampos end = (i == numThreads - 1) ? fileSize : start + chunkSize;
        threads.emplace_back(parseLogFile, filename, start, end, std::ref(errorCount));
    }

    for (auto& thread : threads) {
        thread.join();
    }

    std::cout << "エラーの総数: " << errorCount << std::endl;

    return 0;
}

このコードでは、ログファイルを4つのチャンクに分割し、各スレッドがそれぞれのチャンクを解析してエラーの数をカウントします。最終的に、全体のエラー数を合計して出力します。

演習問題

以下の演習問題に取り組み、学んだ内容を実践的に活用してください。

演習問題1:マルチスレッドによるファイルコピー

大きなファイルを複数のスレッドで並行してコピーするプログラムを作成してください。それぞれのスレッドがファイルの一部をコピーし、最終的に全体のファイルがコピーされるようにします。

演習問題2:並列計算による数値積分

並列計算を使用して、数値積分の結果を求めるプログラムを作成してください。具体的には、関数f(x)を指定された範囲で積分し、その結果を複数のスレッドで並行して計算します。

演習問題3:スレッドプールの改良

以前のスレッドプールの例を改良し、スレッドがタスクを終了した後に新しいタスクを受け取れるようにしてください。これにより、スレッドプールが動的にタスクを処理できるようになります。

回答例

演習問題1の回答例として、以下のようなコードが考えられます。

#include <iostream>
#include <fstream>
#include <thread>
#include <vector>
#include <mutex>

std::mutex mtx;

void copyFileChunk(const std::string& source, const std::string& dest, std::streampos start, std::streampos end) {
    std::ifstream inFile(source, std::ios::binary);
    std::ofstream outFile(dest, std::ios::binary | std::ios::app);

    if (!inFile || !outFile) {
        std::cerr << "ファイルを開くことができませんでした。" << std::endl;
        return;
    }

    inFile.seekg(start);
    std::vector<char> buffer(end - start);
    inFile.read(buffer.data(), end - start);

    std::lock_guard<std::mutex> lock(mtx);
    outFile.seekp(start);
    outFile.write(buffer.data(), end - start);
}

int main() {
    const std::string sourceFile = "largefile.bin";
    const std::string destFile = "copy_largefile.bin";
    const int numThreads = 4;
    std::vector<std::thread> threads;
    std::ifstream inFile(sourceFile, std::ios::binary | std::ios::ate);
    std::streampos fileSize = inFile.tellg();
    std::streampos chunkSize = fileSize / numThreads;

    for (int i = 0; i < numThreads; ++i) {
        std::streampos start = i * chunkSize;
        std::streampos end = (i == numThreads - 1) ? fileSize : start + chunkSize;
        threads.emplace_back(copyFileChunk, sourceFile, destFile, start, end);
    }

    for (auto& thread : threads) {
        thread.join();
    }

    std::cout << "ファイルコピーが完了しました。" << std::endl;

    return 0;
}

この回答例では、大きなバイナリファイルを4つのスレッドで並行してコピーしています。それぞれのスレッドがファイルの一部を読み込み、出力ファイルに書き込みます。

このようにして、C++でのファイル入出力とコンカレントデータ処理を実践的に学びましょう。次のセクションでは、この記事のまとめを行います。

まとめ

この記事では、C++を使ったファイル入出力とコンカレントデータ処理の基礎から応用までを学びました。まず、ファイルの読み書きの基本操作を理解し、具体的なコード例を通じて実践的な知識を身につけました。次に、コンカレントデータ処理の重要性を学び、スレッドプログラミングの基本概念と具体的な実装方法を確認しました。

さらに、マルチスレッドを用いたファイル処理の具体例を紹介し、デッドロックや競合状態の対策についても詳しく解説しました。最後に、応用例と演習問題を通じて、これらの知識を実践的に応用する方法を学びました。

これらの内容を通じて、C++におけるファイル入出力とコンカレントデータ処理のスキルを深め、効率的なプログラムの設計と実装ができるようになったことと思います。今後も継続的に学習と実践を重ね、さらに高度な技術を身につけていってください。

コメント

コメントする

目次