Linux多线程编程:从入门到实战

张开发
2026/4/10 2:40:55 15 分钟阅读

分享文章

Linux多线程编程:从入门到实战
轻量级进程让程序并行起来前言在之前的文章中我们学习了进程的概念。进程是系统资源分配的基本单位而线程则是CPU调度的基本单位。线程是轻量级进程它共享进程的地址空间和资源使得多任务并发更加高效。一、线程与进程1.1 概念对比特性进程线程资源分配系统资源分配的基本单位不拥有资源共享所属进程的资源调度执行CPU调度的基本单位CPU调度的基本单位更轻量内存空间独立的用户空间0~3G共享进程的用户空间栈区私有创建开销大复制页表、文件描述符等小只需分配栈区8M通信方式复杂IPC简单直接共享全局变量并发性进程间并发同一进程内多线程并发结论线程适合需要频繁切换、共享数据、资源消耗小的任务进程适合需要隔离、安全、大量资源的任务。1.2 线程的生命周期创建使用pthread_create创建新线程。运行线程被调度执行共享进程资源。结束线程执行完毕或主动退出。资源回收线程退出后栈区不会自动释放需手动回收或设置分离属性。二、线程基本操作2.1 创建线程pthread_create#include pthread.h int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg);thread返回的线程ID。attr线程属性默认用NULL。start_routine线程执行的函数回调函数。arg传递给回调函数的参数。返回值成功返回0失败返回错误码。示例#include stdio.h #include pthread.h ​ void *th_func(void *arg) { int num *(int *)arg; printf(子线程参数 %d\n, num); return NULL; } ​ int main() { pthread_t tid; int val 100; pthread_create(tid, NULL, th_func, val); pthread_join(tid, NULL); // 等待子线程结束 return 0; }2.2 获取自身线程IDpthread_selfpthread_t pthread_self(void);返回当前线程的ID类型为unsigned long可用%lu打印。2.3 结束线程pthread_exitvoid pthread_exit(void *retval);结束当前线程并将retval返回给等待回收的线程通过pthread_join获取。2.4 线程回收pthread_joinint pthread_join(pthread_t thread, void **retval);阻塞等待指定的线程结束并回收其资源。若线程已结束立即返回。retval接收线程的返回值即pthread_exit的参数或return的指针。注意不要返回局部变量的地址应使用静态变量或堆内存。void *th(void *arg) { static char buf[] 线程结束; // 或 char *buf malloc(100); strcpy(buf, 线程结束); return buf; // 注意不要返回局部变量 } ​ int main() { pthread_t tid; pthread_create(tid, NULL, th, NULL); void *ret; pthread_join(tid, ret); printf(子线程返回%s\n, (char *)ret); // free(ret); // 如果堆分配需释放 return 0; }2.5 取消线程pthread_cancelint pthread_cancel(pthread_t thread);向指定线程发送取消请求线程在适当的时候如遇到取消点会退出。2.6 分离线程pthread_detachint pthread_detach(pthread_t thread);设置线程为分离状态当线程退出时系统自动回收其资源无需pthread_join。三、线程同步多线程共享进程资源若不加控制对临界资源的并发访问可能导致数据错乱。需要同步机制保证正确性。3.1 互斥锁Mutex互斥锁实现互斥访问排他性保证同一时间只有一个线程进入临界区。步骤定义互斥锁pthread_mutex_t mutex;初始化pthread_mutex_init(mutex, NULL);加锁pthread_mutex_lock(mutex);解锁pthread_mutex_unlock(mutex);销毁pthread_mutex_destroy(mutex);示例两个线程分别对共享计数器加1和加2。#include stdio.h #include pthread.h int cnt 0; pthread_mutex_t mutex; void *th_add1(void *arg) { for (int i 0; i 10000; i) { pthread_mutex_lock(mutex); cnt; pthread_mutex_unlock(mutex); } return NULL; } void *th_add2(void *arg) { for (int i 0; i 10000; i) { pthread_mutex_lock(mutex); cnt 2; pthread_mutex_unlock(mutex); } return NULL; } int main() { pthread_t t1, t2; pthread_mutex_init(mutex, NULL); pthread_create(t1, NULL, th_add1, NULL); pthread_create(t2, NULL, th_add2, NULL); pthread_join(t1, NULL); pthread_join(t2, NULL); printf(cnt %d\n, cnt); // 应为 30000 pthread_mutex_destroy(mutex); return 0; }3.2 信号量Semaphore信号量可以用于同步有顺序的访问也可以用于互斥多资源控制。它由PV操作实现。相关函数POSIX无名信号量#include semaphore.h int sem_init(sem_t *sem, int pshared, unsigned int value); int sem_wait(sem_t *sem); // P操作资源-1若资源为0则阻塞 int sem_post(sem_t *sem); // V操作资源1唤醒等待线程 int sem_destroy(sem_t *sem);pshared0线程间使用pshared!0进程间使用。value初始资源数二值信号量设为1。二值信号量示例线程A和B交替执行。sem_t sem1, sem2; void *thA(void *arg) { for (int i 0; i 5; i) { sem_wait(sem1); printf(A ); fflush(stdout); sem_post(sem2); } return NULL; } void *thB(void *arg) { for (int i 0; i 5; i) { sem_wait(sem2); printf(B ); fflush(stdout); sem_post(sem1); } return NULL; } int main() { pthread_t t1, t2; sem_init(sem1, 0, 1); // 先A sem_init(sem2, 0, 0); // 后B pthread_create(t1, NULL, thA, NULL); pthread_create(t2, NULL, thB, NULL); pthread_join(t1, NULL); pthread_join(t2, NULL); sem_destroy(sem1); sem_destroy(sem2); return 0; }计数信号量示例模拟三个资源的分配如窗口服务。sem_t sem_win; void *th(void *arg) { sem_wait(sem_win); printf(获取资源\n); sleep(rand() % 3 1); printf(释放资源\n); sem_post(sem_win); return NULL; } int main() { sem_init(sem_win, 0, 3); // 三个资源 pthread_t tid[10]; for (int i 0; i 10; i) pthread_create(tid[i], NULL, th, NULL); for (int i 0; i 10; i) pthread_join(tid[i], NULL); sem_destroy(sem_win); return 0; }3.3 死锁死锁是指两个或多个线程互相等待对方释放资源导致无法继续执行的现象。产生死锁的四个必要条件互斥资源一次只能被一个线程使用。请求与保持线程保持已有资源同时请求其他资源。不剥夺资源不能被强制剥夺。循环等待线程间形成循环等待链。避免死锁按顺序加锁、使用trylock、超时机制等。四、进程间通信IPC除了线程间通信进程间也需要交换数据。Linux提供了多种IPC机制。4.1 管道Pipe无名管道pipe适用于有亲缘关系的进程父子进程。半双工一端读一端写。int pipe(int pipefd[2]); // pipefd[0]读端pipefd[1]写端特性读端存在时写端写入超过64K会阻塞。写端存在时读端读空会阻塞。读端关闭写端写入会收到SIGPIPE信号导致进程终止。写端关闭读端读取到0EOF。有名管道fifo适用于任意进程文件系统中可见。int mkfifo(const char *pathname, mode_t mode);打开方式open时一端只读O_RDONLY另一端只写O_WRONLY。若以O_RDWR打开不会阻塞但需要自己控制读写节奏。4.2 信号Signal信号是一种异步通知机制用于处理突发事件。常用信号SIGINT(2)CtrlC中断SIGKILL(9)强制终止SIGSTOP(19)强制暂停SIGALRM(14)闹钟信号SIGCHLD(17)子进程状态改变通知父进程SIGUSR1/SIGUSR2用户自定义信号信号处理函数#include signal.h typedef void (*sighandler_t)(int); sighandler_t signal(int signum, sighandler_t handler);handler可以是SIG_DFL默认处理SIG_IGN忽略自定义函数发送信号int kill(pid_t pid, int sig);定时器unsigned int alarm(unsigned int seconds); // 定时发送SIGALRM int pause(void); // 等待信号4.3 共享内存Shared Memory共享内存是效率最高的IPC方式多个进程映射同一块物理内存直接读写。步骤生成唯一键值key_t ftok(const char *pathname, int proj_id);创建/获取共享内存int shmget(key_t key, size_t size, int shmflg);映射到进程空间void *shmat(int shmid, const void *shmaddr, int shmflg);读写直接使用映射地址。解除映射int shmdt(const void *shmaddr);删除共享内存int shmctl(int shmid, int cmd, struct shmid_ds *buf);cmd IPC_RMID注意共享内存没有同步机制需结合信号量等实现互斥。五、网络编程基础网络编程是进程间通信的扩展——不同主机之间的通信。5.1 OSI模型与TCP/IP模型OSI七层模型应用层、表示层、会话层、传输层、网络层、数据链路层、物理层。TCP/IP四层模型应用层、传输层、网络层、网络接口层。5.2 常用协议TCP传输控制协议面向连接、可靠、有序、全双工。UDP用户数据报协议无连接、不可靠、高效。IP网际协议负责路由。5.3 Socket编程UDP编程服务端步骤socket()创建套接字SOCK_DGRAMbind()绑定地址和端口recvfrom()/sendto()收发数据close()关闭套接字客户端步骤socket()sendto()/recvfrom()可省略bind由系统自动分配端口close()UDP特点发送次数和接收次数不一定对应可能丢包。无写阻塞发送太快可能导致丢包。有读阻塞无数据时阻塞。TCP编程服务端步骤socket()创建流式套接字SOCK_STREAMbind()绑定地址和端口listen()监听队列accept()接受连接返回新套接字用于通信recv()/send()收发数据close()关闭套接字客户端步骤socket()connect()连接服务器send()/recv()close()TCP特点三次握手建立连接四次挥手断开。可靠传输应答、重传、拥塞控制。流式套接字发送次数和接收次数无对应关系。全双工可同时收发。有读写阻塞。5.4 粘包问题TCP流式传输可能导致多个数据包合并发送接收端无法区分边界。解决方案定长包每个消息固定长度添加结束标志如\n自定义协议包含长度字段六、总结特性线程进程网络通信共享数据全局变量需IPCsocket同步机制互斥锁、信号量信号量、文件锁等协议层面开销小大中等适用场景高性能并发安全隔离分布式系统多线程编程是现代操作系统开发的核心技能。掌握线程的创建、同步和通信你就能编写出高效、可靠的并发程序。结合进程间通信和网络编程更能应对复杂的应用场景。线程让程序灵动同步让数据安全通信让进程互联。掌握这些你将能驾驭任何复杂系统。

更多文章