C++多线程与多进程的创建方法
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
变为 true
。go
函数中,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 si
和PROCESS_INFORMATION pi
分别用于指定新进程的启动信息和获取新进程的相关信息。ZeroMemory
函数初始化si
和pi
结构体。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 系统上,可以使用 msgget
、msgsnd
和 msgrcv
等函数来操作消息队列。
#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 系统上,可以使用 shmget
、shmat
和 shmdt
等函数。
#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++ 中有效地创建和管理多线程与多进程,并处理线程间和进程间的同步与通信问题,从而编写出高效、并发的程序。无论是多线程还是多进程,都有其适用场景,需要根据具体的需求和系统环境进行选择和优化。