MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

C++多线程与多进程的创建方法

2021-04-191.4k 阅读

C++ 多线程的创建方法

在 C++ 中,多线程编程为开发者提供了利用多核处理器提高程序性能、实现并发任务的能力。C++11 引入了 <thread> 头文件,使得多线程编程更加便捷和标准化。

1. 使用 std::thread 创建简单线程

首先,要创建一个线程,需要包含 <thread> 头文件。下面是一个简单的示例,展示如何创建一个线程并等待它完成:

#include <iostream>
#include <thread>

void hello() {
    std::cout << "Hello from thread " << std::this_thread::get_id() << std::endl;
}

int main() {
    std::thread t(hello);
    std::cout << "Main thread " << std::this_thread::get_id() << " is running" << std::endl;
    t.join();
    return 0;
}

在上述代码中:

  • void hello() 定义了一个函数,这个函数将在新线程中执行。它输出线程的 ID。
  • main 函数中,std::thread t(hello) 创建了一个新线程,该线程开始执行 hello 函数。
  • std::cout << "Main thread " << std::this_thread::get_id() << " is running" << std::endl; 输出主线程的 ID。
  • t.join() 使得主线程等待 t 线程完成。如果不调用 join,主线程可能在新线程完成之前就结束了,导致程序异常。

2. 传递参数给线程函数

线程函数可以接受参数。以下是一个示例,展示如何向线程函数传递参数:

#include <iostream>
#include <thread>

void print_number(int num) {
    std::cout << "Printing number: " << num << " from thread " << std::this_thread::get_id() << std::endl;
}

int main() {
    int number = 42;
    std::thread t(print_number, number);
    std::cout << "Main thread " << std::this_thread::get_id() << " is running" << std::endl;
    t.join();
    return 0;
}

这里,print_number 函数接受一个 int 类型的参数 num。在 main 函数中,通过 std::thread t(print_number, number)number 传递给 print_number 函数。

3. 使用 lambda 表达式创建线程

C++ 中的 lambda 表达式为创建线程提供了一种简洁的方式,特别是当线程函数的逻辑比较简单时。

#include <iostream>
#include <thread>

int main() {
    std::thread t([]() {
        std::cout << "Hello from lambda thread " << std::this_thread::get_id() << std::endl;
    });
    std::cout << "Main thread " << std::this_thread::get_id() << " is running" << std::endl;
    t.join();
    return 0;
}

在这个例子中,使用了一个无参数的 lambda 表达式作为线程函数。这种方式避免了专门定义一个函数的麻烦,使代码更加紧凑。

4. 线程分离 (detach)

除了 join,还可以使用 detach 方法。detach 会让线程在后台运行,主线程不再等待它。一旦线程被分离,就无法再对其调用 join 了。

#include <iostream>
#include <thread>

void background_task() {
    std::cout << "Background thread " << std::this_thread::get_id() << " is running" << std::endl;
}

int main() {
    std::thread t(background_task);
    t.detach();
    std::cout << "Main thread " << std::this_thread::get_id() << " continues without waiting" << std::endl;
    // 模拟一些其他工作
    for (int i = 0; i < 5; ++i) {
        std::cout << "Main thread working: " << i << std::endl;
        std::this_thread::sleep_for(std::chrono::seconds(1));
    }
    return 0;
}

在上述代码中,t.detach() 使 background_task 线程在后台运行,主线程继续执行自己的任务。注意,被分离的线程的资源在其结束时会自动释放,但是如果线程函数中涉及到共享资源,需要特别小心同步问题。

5. 线程局部存储 (Thread - Local Storage, TLS)

线程局部存储允许每个线程拥有自己独立的变量副本。在 C++ 中,可以通过 thread_local 关键字来实现。

#include <iostream>
#include <thread>

thread_local int thread_local_variable = 0;

void increment_thread_local() {
    ++thread_local_variable;
    std::cout << "Thread " << std::this_thread::get_id() << " has incremented thread_local_variable to " << thread_local_variable << std::endl;
}

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

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

    std::cout << "In main thread, thread_local_variable is " << thread_local_variable << std::endl;
    return 0;
}

在这个例子中,thread_local int thread_local_variable = 0; 声明了一个线程局部变量。每个线程在调用 increment_thread_local 函数时,都会独立地增加自己的 thread_local_variable 副本。主线程中的 thread_local_variable 值不受其他线程影响。

6. 线程同步

多线程编程中,线程同步是一个关键问题。当多个线程访问共享资源时,如果没有适当的同步机制,可能会导致数据竞争和未定义行为。常见的同步机制包括互斥锁 (mutex)、条件变量 (condition variable) 和信号量 (semaphore)。

互斥锁 (std::mutex) 互斥锁用于保护共享资源,确保在同一时间只有一个线程可以访问它。

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

std::mutex mtx;
int shared_variable = 0;

void increment_shared_variable() {
    mtx.lock();
    ++shared_variable;
    std::cout << "Thread " << std::this_thread::get_id() << " incremented shared_variable to " << shared_variable << std::endl;
    mtx.unlock();
}

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

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

    return 0;
}

在这个例子中,std::mutex mtx; 定义了一个互斥锁。在 increment_shared_variable 函数中,mtx.lock() 锁定互斥锁,防止其他线程同时访问 shared_variable,操作完成后,mtx.unlock() 释放互斥锁。

条件变量 (std::condition_variable) 条件变量通常与互斥锁一起使用,用于线程间的通信。它允许一个线程等待某个条件满足,而其他线程可以通知这个条件已经满足。

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

std::mutex mtx;
std::condition_variable cv;
bool ready = false;

void print_id(int id) {
    std::unique_lock<std::mutex> lock(mtx);
    while (!ready) cv.wait(lock);
    std::cout << "thread " << id << '\n';
}

void go() {
    std::unique_lock<std::mutex> lock(mtx);
    ready = true;
    std::cout << "go\n";
    cv.notify_all();
}

int main() {
    std::thread threads[10];
    for (int i = 0; i < 10; ++i)
        threads[i] = std::thread(print_id, i);

    std::cout << "10 threads ready to race...\n";
    go();

    for (auto& th : threads) th.join();

    return 0;
}

在这个示例中,std::condition_variable cv; 定义了一个条件变量。print_id 函数中的 cv.wait(lock) 使线程等待,直到 ready 变为 truego 函数中,cv.notify_all() 通知所有等待的线程,条件已满足。

信号量 (std::counting_semaphore) C++20 引入了 std::counting_semaphore,它是一个更通用的同步工具。信号量维护一个计数,线程可以获取 (decrement) 和释放 (increment) 信号量。

#include <iostream>
#include <thread>
#include <counting_semaphore>

std::counting_semaphore<10> sem(5);

void worker() {
    sem.acquire();
    std::cout << "Thread " << std::this_thread::get_id() << " acquired semaphore" << std::endl;
    // 模拟工作
    std::this_thread::sleep_for(std::chrono::seconds(1));
    sem.release();
    std::cout << "Thread " << std::this_thread::get_id() << " released semaphore" << std::endl;
}

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

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

    return 0;
}

这里,std::counting_semaphore<10> sem(5); 定义了一个最大计数为 10,初始计数为 5 的信号量。worker 函数中,sem.acquire() 获取信号量,如果信号量计数为 0,则线程等待。sem.release() 释放信号量,增加计数。

C++ 多进程的创建方法

与多线程不同,多进程是指在操作系统中创建多个独立的进程,每个进程有自己独立的地址空间。在 C++ 中,可以通过一些系统相关的库来创建多进程,比如在 Unix - like 系统上使用 fork 函数,在 Windows 系统上使用 CreateProcess 函数。为了实现跨平台的多进程编程,可以使用 boost::process 库。

1. Unix - like 系统上使用 fork 创建进程

在 Unix - like 系统(如 Linux、macOS)上,fork 函数是创建新进程的基础。fork 会创建一个与调用进程几乎完全相同的子进程,子进程从 fork 函数返回处开始执行。

#include <iostream>
#include <unistd.h>
#include <sys/wait.h>

int main() {
    pid_t pid = fork();
    if (pid == 0) {
        // 子进程
        std::cout << "This is the child process. PID: " << getpid() << std::endl;
    } else if (pid > 0) {
        // 父进程
        std::cout << "This is the parent process. PID: " << getpid() << ". Child PID: " << pid << std::endl;
        wait(nullptr);
    } else {
        // fork 失败
        std::cerr << "fork() failed" << std::endl;
    }
    return 0;
}

在上述代码中:

  • pid_t pid = fork(); 调用 fork 函数,它返回两次:在父进程中返回子进程的 PID,在子进程中返回 0。
  • 在子进程分支(if (pid == 0))中,输出子进程的 PID。
  • 在父进程分支(if (pid > 0))中,输出父进程的 PID 和子进程的 PID。wait(nullptr) 使父进程等待子进程结束。
  • 如果 fork 失败(if (pid < 0)),输出错误信息。

2. Windows 系统上使用 CreateProcess 创建进程

在 Windows 系统上,CreateProcess 函数用于创建新进程。下面是一个简单的示例:

#include <iostream>
#include <windows.h>

int main() {
    STARTUPINFO si;
    PROCESS_INFORMATION pi;

    ZeroMemory(&si, sizeof(si));
    si.cb = sizeof(si);
    ZeroMemory(&pi, sizeof(pi));

    if (!CreateProcess(
        NULL,
        TEXT("notepad.exe"),
        NULL,
        NULL,
        FALSE,
        0,
        NULL,
        NULL,
        &si,
        &pi
    )) {
        std::cerr << "CreateProcess failed" << std::endl;
        return 1;
    }

    std::cout << "Child process created. Process ID: " << pi.dwProcessId << std::endl;

    WaitForSingleObject(pi.hProcess, INFINITE);

    CloseHandle(pi.hProcess);
    CloseHandle(pi.hThread);

    return 0;
}

在这个示例中:

  • STARTUPINFO siPROCESS_INFORMATION pi 分别用于指定新进程的启动信息和获取新进程的相关信息。
  • ZeroMemory 函数初始化 sipi 结构体。
  • CreateProcess 函数创建一个新进程,这里启动了 notepad.exe。如果创建失败,输出错误信息。
  • WaitForSingleObject(pi.hProcess, INFINITE) 使当前进程等待新创建的进程结束。
  • 最后,通过 CloseHandle 关闭进程和线程的句柄。

3. 使用 boost::process 实现跨平台多进程编程

boost::process 库提供了跨平台的多进程编程接口,使得代码可以在不同操作系统上使用相似的方式创建和管理进程。

#include <iostream>
#include <boost/process.hpp>

namespace bp = boost::process;

int main() {
    try {
        bp::child c("ls", "-l");
        std::cout << "Child process started. PID: " << c.id() << std::endl;
        c.wait();
    } catch (const std::exception& e) {
        std::cerr << "Error: " << e.what() << std::endl;
    }
    return 0;
}

在上述代码中:

  • bp::child c("ls", "-l"); 使用 boost::process 创建一个子进程,执行 ls -l 命令。
  • std::cout << "Child process started. PID: " << c.id() << std::endl; 输出子进程的 PID。
  • c.wait(); 使当前进程等待子进程结束。如果在创建或等待过程中出现异常,捕获并输出错误信息。

4. 进程间通信 (IPC)

创建多进程后,进程间通信是一个重要的问题。常见的进程间通信方式包括管道 (pipe)、消息队列 (message queue)、共享内存 (shared memory) 和信号 (signal)。

管道 (Pipe) 管道是一种半双工的通信方式,数据只能单向流动。在 Unix - like 系统上,可以使用 pipe 函数创建管道。

#include <iostream>
#include <unistd.h>
#include <sys/wait.h>
#include <cstring>

#define BUFFER_SIZE 1024

int main() {
    int pipe_fds[2];
    if (pipe(pipe_fds) == -1) {
        std::cerr << "pipe() failed" << std::endl;
        return 1;
    }

    pid_t pid = fork();
    if (pid == 0) {
        // 子进程
        close(pipe_fds[0]); // 关闭读端
        const char* message = "Hello from child";
        write(pipe_fds[1], message, strlen(message) + 1);
        close(pipe_fds[1]);
    } else if (pid > 0) {
        // 父进程
        close(pipe_fds[1]); // 关闭写端
        char buffer[BUFFER_SIZE];
        ssize_t bytes_read = read(pipe_fds[0], buffer, BUFFER_SIZE);
        if (bytes_read > 0) {
            buffer[bytes_read] = '\0';
            std::cout << "Parent received: " << buffer << std::endl;
        }
        close(pipe_fds[0]);
        wait(nullptr);
    } else {
        // fork 失败
        std::cerr << "fork() failed" << std::endl;
        return 1;
    }
    return 0;
}

在这个例子中:

  • pipe(pipe_fds) 创建一个管道,pipe_fds[0] 是读端,pipe_fds[1] 是写端。
  • 子进程关闭读端,向管道写端写入消息。
  • 父进程关闭写端,从管道读端读取消息并输出。

消息队列 (Message Queue) 消息队列允许进程以消息的形式进行通信。在 Unix - like 系统上,可以使用 msggetmsgsndmsgrcv 等函数来操作消息队列。

#include <iostream>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <cstring>
#include <unistd.h>
#include <sys/wait.h>

#define MSG_SIZE 128

struct msgbuf {
    long mtype;
    char mtext[MSG_SIZE];
};

int main() {
    key_t key = ftok(".", 'a');
    if (key == -1) {
        std::cerr << "ftok() failed" << std::endl;
        return 1;
    }

    int msgid = msgget(key, IPC_CREAT | 0666);
    if (msgid == -1) {
        std::cerr << "msgget() failed" << std::endl;
        return 1;
    }

    pid_t pid = fork();
    if (pid == 0) {
        // 子进程
        msgbuf sendbuf;
        sendbuf.mtype = 1;
        std::strcpy(sendbuf.mtext, "Hello from child");
        if (msgsnd(msgid, &sendbuf, std::strlen(sendbuf.mtext) + 1, 0) == -1) {
            std::cerr << "msgsnd() failed" << std::endl;
        }
    } else if (pid > 0) {
        // 父进程
        msgbuf recvbuf;
        if (msgrcv(msgid, &recvbuf, MSG_SIZE, 1, 0) == -1) {
            std::cerr << "msgrcv() failed" << std::endl;
        } else {
            std::cout << "Parent received: " << recvbuf.mtext << std::endl;
        }
        wait(nullptr);
        if (msgctl(msgid, IPC_RMID, nullptr) == -1) {
            std::cerr << "msgctl() failed" << std::endl;
        }
    } else {
        // fork 失败
        std::cerr << "fork() failed" << std::endl;
        return 1;
    }
    return 0;
}

在这个示例中:

  • ftok 函数生成一个唯一的键值。
  • msgget 创建一个消息队列。
  • 子进程使用 msgsnd 向消息队列发送消息。
  • 父进程使用 msgrcv 从消息队列接收消息,并在最后通过 msgctl 删除消息队列。

共享内存 (Shared Memory) 共享内存允许不同进程共享同一块物理内存,从而实现高效的数据共享。在 Unix - like 系统上,可以使用 shmgetshmatshmdt 等函数。

#include <iostream>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <cstring>
#include <unistd.h>
#include <sys/wait.h>

#define SHM_SIZE 1024

int main() {
    key_t key = ftok(".", 'a');
    if (key == -1) {
        std::cerr << "ftok() failed" << std::endl;
        return 1;
    }

    int shmid = shmget(key, SHM_SIZE, IPC_CREAT | 0666);
    if (shmid == -1) {
        std::cerr << "shmget() failed" << std::endl;
        return 1;
    }

    void* shmaddr = shmat(shmid, nullptr, 0);
    if (shmaddr == reinterpret_cast<void*>(-1)) {
        std::cerr << "shmat() failed" << std::endl;
        return 1;
    }

    pid_t pid = fork();
    if (pid == 0) {
        // 子进程
        const char* message = "Hello from child";
        std::strcpy(static_cast<char*>(shmaddr), message);
        if (shmdt(shmaddr) == -1) {
            std::cerr << "shmdt() failed" << std::endl;
        }
    } else if (pid > 0) {
        // 父进程
        wait(nullptr);
        std::cout << "Parent received: " << static_cast<char*>(shmaddr) << std::endl;
        if (shmdt(shmaddr) == -1) {
            std::cerr << "shmdt() failed" << std::endl;
        }
        if (shmctl(shmid, IPC_RMID, nullptr) == -1) {
            std::cerr << "shmctl() failed" << std::endl;
        }
    } else {
        // fork 失败
        std::cerr << "fork() failed" << std::endl;
        return 1;
    }
    return 0;
}

在这个例子中:

  • ftok 生成键值,shmget 创建共享内存段。
  • shmat 将共享内存段附加到进程地址空间。
  • 子进程向共享内存写入消息,父进程从共享内存读取消息,并在最后通过 shmdt 分离共享内存,shmctl 删除共享内存段。

信号 (Signal) 信号是一种异步通知机制,用于在进程间传递事件。在 Unix - like 系统上,可以使用 signal 函数注册信号处理函数。

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>

void signal_handler(int signum) {
    std::cout << "Received signal " << signum << std::endl;
}

int main() {
    signal(SIGUSR1, signal_handler);

    pid_t pid = fork();
    if (pid == 0) {
        // 子进程
        std::cout << "Child process sending signal to parent" << std::endl;
        kill(getppid(), SIGUSR1);
    } else if (pid > 0) {
        // 父进程
        std::cout << "Parent process waiting for signal" << std::endl;
        wait(nullptr);
    } else {
        // fork 失败
        std::cerr << "fork() failed" << std::endl;
        return 1;
    }
    return 0;
}

在这个示例中:

  • signal(SIGUSR1, signal_handler) 注册了 SIGUSR1 信号的处理函数 signal_handler
  • 子进程使用 kill 函数向父进程发送 SIGUSR1 信号,父进程捕获并处理该信号。

通过上述方法,开发者可以在 C++ 中有效地创建和管理多线程与多进程,并处理线程间和进程间的同步与通信问题,从而编写出高效、并发的程序。无论是多线程还是多进程,都有其适用场景,需要根据具体的需求和系统环境进行选择和优化。