线程基础
1、线程与进程比较
进程是资源分配的最小单位,线程是CPU调度的最小单位。
多进程:编程方便,简化逻辑。
多线程:简化逻辑;减少开销(创建、切换和调度);通信方便。
1)Linux下线程是轻量级进程
linux实现Posix线程有两种方式:LinuxThreads(旧的,glibc2.4起就不再支持了)和NPTL(Native POSIX Threads Library, glibc 2.3.2, kernel 2.6)。
NPTL创建线程的同时,创建manager线程,负责回收分离状态线程。
安装manpages-posix-dev可man thread相关函数,ubuntu下安装方法:
sudo apt-get install manpages-posix-dev
2)多线程共享资源
同一地址空间(包括代码段,数据段,自然包括全局变量)。
进程id和父进程id,进程组id和session ID
用户id和组id
文件描述符表
每种信号处理方式(SIG_IGN, SIG_DFL或自定义信号处理函数)
当前工作目录
3)线程独有
线程id(递减)
上下文,包括各种寄存器的值,程序计数器和栈指针。
栈空间
errno变量
信号屏蔽字(新线程将继承进程(主线程)的信号屏蔽字,但新线程的未决信号集被清空,以防同一信号被多个线程处理)
调度优先级
注:线程安全函数不一定为可重入函数(线程安全函数可只读全局变量而不写,可重入函数不可访问全局变量)。
2、线程函数
man pthreads了解线程相关内容。
线程函数成功返回0,失败返回错误码,不设置errno。POSIX.1-2001指出线程函数绝不会(never)返回EINTR(不会因EINTR而失败)。
1)线程创建
#include<pthead.h> int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void * (*start_routine)(void *), void * restrict arg);
attr一般为NULL,采用系统默认属性。
int pthread_attr_init(pthread_attr_t *attr); int pthread_attr_destroy(pthread_attr_t *attr);
>>默认创建的线程是joinable的,可通过函数pthread_attr_setdetachstate()设置attr从而创建detached的线程。
>>可通过pthread_attr_setstacksize()设置attr从而创建指定栈大小的线程。pthread_attr_getstacksize()获取当线程栈大小。
成功返回0,失败返回错误号。
pthread_t无符号整型 typedef unsigned long int pthread_t
pthread库的函数都是通过返回值返回错误号,虽然每个线程都有一个errno,但并不使用它。因此不能调用perror打印错误信息,可先用strerror把返回值(错误码)转化为错误信息再打印。
注:线程函数参数和返回值都是void*,且函数返回的指针所指向的内存单元必须是全局的或者malloc分配的。
void * (*start_routine)(void *)
2)线程终止
终止线程有三种方法:
- 从线程函数return。
- 调用pthread_cancel()终止同一进程中的另一个线程。
- 线程可调用pthread_exit()终止自己。
void pthread_exit(void *value_ptr);
无返回值,总是成功。
注:pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者malloc分配的,不能在线程函数的栈上分配。
int pthread_cancel(pthread_t thread);
成功返回0,失败返回非零错误号。
被终止的线程的响应取决于可终止状态和类型(cancelability state and type)。
int pthread_setcancelstate(int state, int *oldstate); int pthread_setcanceltype(int type, int *oldtype); // redis bio /* Make the thread killable at any time, so that bioKillThreads() can work reliably. */ pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL); pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, NULL);
注:系统并不会马上关闭被取消线程,只有在被取消线程下次系统调用时(Cancellation points),才会真正结束线程。或调用pthread_testcancel(如没有系统调用),让内核去检测是否需要取消当前线程。被取消的线程,退出值,定义在Linux的pthread库中常数PTHREAD_CANCELED的值是-1。参考:https://blog.csdn.net/huangshanchun/article/details/47420961
注:如果任意一个线程调用exit或_exit,则整个进程的所有线程都终止。
3)获取终止状态
int pthread_join(pthread_t thread, void **retval);
等待线程终止,并获取线程退出状态。该线程必须是joinable。调用该函数的线程将挂起等待,直到id为thread的线程终止。阻塞函数
函数调用时注意参数:定义为void *res; 调用pthread_join(&res);最终调用参数(char *)res。
retval:
1)如果thread线程通过return返回,thread线程函数返回值。
2)pthread_cancel()异常终止,则retval所指向的单元存放常量PTHREAD_CANCELED(-1)。
3)自己调用pthread_exit()终止,retval存放pthread_exit参数。
4)分离线程
#include<pthread.h>
int pthread_detach(pthread_t thread);
标记线程thread为分离状态。当一个分离状态的线程终止时,它的资源自动释放给系统,不需要其他线程join。
成功返回0,失败返回错误号。
注:让线程自己pthread_detach(线程函数内调用)不好,库函数不是原子的。
注:不能对一个已经处于detach状态的线程调用pthread_join.==>EINVAL.
注:不能对同一线程调用两次pthread_join或pthread_detach,或者一个线程已经调用pthread_detach就不能再调用pthread_join了。
注:线程创建时,都应该调用pthread_join()或pthread_detach(),以使系统资源释放。
示例:pthread_detach(pthread_self());
5)获取线程id
pthread_t pthread_self(void);
函数总是成功,返回id。
pthread_t是不适合作为线程的标识的。pthread_t是由POSIX pthread库内部提供的,只在进程内部有意义,无法关联操作系统的任务调度之类的信息。比方说在/proc查找不到关于pthread_t得到的task。
glibc的Pthreads实现实际上把pthread_t作为一个结构体指针,指向一块动态分配的内存,但是这块内存是可以反复使用的,也就是说很容易造成pthread_t的重复。也就是说pthreads只能保证同一进程内,同一时刻的各个线程不同;不能保证同一个进程全程时段每个线程具有不同的id,不能保证线程id的唯一性。
在LINUX系统中,建议使用gettid系统调用的返回值作为线程id,这么做的原因:返回值是一个pid_t,其值是一个很小的整数,方便输出。
在linux系统中,它直接标识内核任务调度id,可通过/proc文件系统中找到对应项:/proc/tid 或者 /proc/pid/task/tid,方便定位到具体线程。
任何时刻都是唯一的,并且由于linux分配新的pid采用递增轮回办法,短时间内启动多个线程也会具有不同的id。
#define gettid() syscall(__NR_gettid)
6)信号函数
线程信号用pthread_sigmask, pthread_kill。
int pthread_kill(pthread_t thread, int sig); int pthread_sigmask(int how, const sigset_t *set, sigset_t *oldset);
pthread_sigmask()参数与sigprocmask()等同。
pthread_kill() 成功:0,线程不存在:ESRCH,信号不合法:EINVAL
给线程发送一个0就可以判断线程是否存在pthread_kill(tid, 0)。
int kill_ret1 = pthread_kill(tid1,0); if(kill_ret1 ==ESRCH) { printf("线程1不存在 id = %x \n",thread_id1); }else if(kill_ret1 == 0){ printf("线程1存活 id = %x \n",thread_id1); }
7)读写文件
ssize_t pread(int fd, void *buf, size_t count, off_t offset); ssize_t pwrite(int fd, const void *buf, size_t count, off_t offset);
线程读写用pread/pwrite(文件偏移不改变)。
The pread() and pwrite() system calls are especially useful in multithreaded applications. They allow multiple threads to perform I/O on the same file
descriptor without being affected by changes to the file offset by other threads.
3、线程信号
1)根据APUE 12.8,进程的处理函数与处理方式是进程中所有线程共享的。
2)根据APUE 12.8,如果进程接收到信号,该信号只会被递送到某一个单独线程。一般情况下由那个线程引起信号则递送到那个线程。如果没有线程引发信号,信号被发送到任意线程。
线程信号处理函数编程时要注意死锁问题(同一线程如果重复申请同一个互斥锁那么必然会死锁)。
如下代码会死锁:
#include <stdio.h> #include <unistd.h> #include <pthread.h> #include <signal.h> pthread_mutex_t mmutex = PTHREAD_MUTEX_INITIALIZER; void *thread_run(void *p) { printf("thread...\n"); pthread_mutex_lock(&mmutex); int i; for(i = 0; i < 10; i++){ sleep(1); printf("thread run [%d]!\n", i); } pthread_mutex_unlock(&mmutex); return NULL; } void signal_handler(int signo) { printf("signal...\n"); pthread_mutex_lock(&mmutex); int i; for(i = 0; i < 5; i++){ sleep(1); printf("signal run [%d]!\n", i); } pthread_mutex_unlock(&mmutex); } int main() { signal(SIGUSR1, signal_handler); pthread_t p; // pthread_create(&p, NULL, thread_run, NULL); sleep(2); //raise(SIGUSR1); pthread_create(&p, NULL, thread_run, NULL); pthread_kill(p, SIGUSR1); while(1){ printf("main...\n"); sleep(1); } pthread_join(p, NULL); pthread_mutex_destroy(&mmutex); return 0; }
sighandler和main函数默认使用相同的堆栈空间,所有函数或变量均可使用。多线程时每个线程与该线程的信号处理函数共享栈空间,虽然各个线程处理函数相同,堆空间所有线程和信号处理函数共享,但每个线程执行信号处理函数时是在线程的栈空间。但为了程序稳定性,在信号处理函数中应使用可重入函数(如sleep)。
上述代码中同一线程中调用了pthread_mutex_lock()后再在该线程信号处理函数中调用,必然死锁。
如果是两个线程空间,则在主线程触发信号后主线程会阻塞到信号处理函数,等待其他线程释放互斥锁。
4、示例
manpage示例
#include <ctype.h> #include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <string.h> #include <pthread.h> #include <errno.h> #define handle_error_en(en, msg) \ do{ errno = en; perror(msg); exit(EXIT_FAILURE); } while(0) #define handle_error(msg) \ do { perror(msg); exit(EXIT_FAILURE); } while(0) struct thread_info{ pthread_t thread_id; int thread_num; char *argv_string; }; static void * t1(void *arg){ struct thread_info *tinfo = arg; char *uargv, *p; printf("Thread %d: top of stack near %p; argv_string=%s\n", tinfo->thread_num, &p, tinfo->argv_string); uargv = strdup(tinfo->argv_string); if(uargv == NULL) printf("strdup error.\n"); for(p = uargv; *p != '\0'; p++) *p = toupper(*p); return uargv; } int main(int argc, char ** argv) { int s, tnum, opt, num_threads; struct thread_info *tinfo; pthread_attr_t attr; int stack_size; void *res; /* the "-s" option specifies a stack size for our threads */ stack_size = -1; while((opt = getopt(argc, argv, "s:")) != -1){ switch(opt){ case 's': stack_size = strtoul(optarg, NULL, 0); break; default: fprintf(stderr, "Usage: %s [-s stack-size] arg ...\n", argv[0]); exit(EXIT_FAILURE); } } num_threads = argc - optind; s = pthread_attr_init(&attr); if(s != 0) handle_error_en(s, "pthread_attr_init"); if(stack_size > 0){ s = pthread_attr_setstacksize(&attr, stack_size); if(s != 0) handle_error_en(s, "pthread_attr_setstacksize"); } tinfo = calloc(num_threads, sizeof(struct thread_info)); if(tinfo == NULL) handle_error("calloc"); for(tnum = 0; tnum < num_threads; tnum++){ tinfo[tnum].thread_num = tnum +1; tinfo[tnum].argv_string = argv[optind + tnum]; s = pthread_create(&tinfo[tnum].thread_id, &attr, &t1, &tinfo[tnum]); if(s != 0) handle_error_en(s, "pthread_create"); } s = pthread_attr_destroy(&attr); if(s != 0) handle_error_en(s, "pthread_attr_destroy"); for(tnum = 0; tnum < num_threads; tnum++){ s = pthread_join(tinfo[tnum].thread_id, &res); if(s != 0) handle_error_en(s, "pthread_join"); printf("Joined with thread %d; returned value was %s\n", tinfo[tnum].thread_num, (char*)res); free(res); } free(tinfo); exit(EXIT_SUCCESS); }
执行:
yuxi@ubuntu:~/test/pthread$ ./a.out yuxi@ubuntu:~/test/pthread$ ./a.out -s 0x100000 abc def CHA Thread 3: top of stack near 0x7f7be5184f20; argv_string=CHA Thread 2: top of stack near 0x7f7be5285f20; argv_string=def Thread 1: top of stack near 0x7f7be5a74f20; argv_string=abc Joined with thread 1; returned value was ABC Joined with thread 2; returned value was DEF Joined with thread 3; returned value was CHA
参考:
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具