多线程解决什么问题:
- 线程生命周期问题;
- 线程如何划分的问题(性能问题);
- 线程如何通信的问题;
信号的行为是进程级别的;对任何线程发信号,线程组都会响应;
线程与进程关系
多线程通信开销(不涉及大量数据交互),远低于多进程;
多进程比多线程好调试;
关系不亲密,适合用多进程模型;否则用多线程模型;
线程的概念
由于同一进程的多个线程共享同一地址空间,因此Text Segment、Data Segment都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享
以下进程资源和环境:
文件描述符表
每种信号的处理方式(SIG_IGN、SIG_DFL或者自定义的信号处理函数)
当前工作目录
用户id和组id
但有些资源是每个线程各有一份的:
线程id
上下文,包括各种寄存器的值、程序计数器和栈指针
栈空间
errno变量
信号屏蔽字
调度优先级
Linux使用的线程库是由POSIX标准定义的,称为POSIX thread或者pthread。线程函数位于libpthread共享库中,因此在编译时要加上-lpthread选项。
POSIX标准对线程的要求:
由上可见Linux内核通过clone创建线程并不能支持POSIX标准,Linux通过NPTL模型支持POSIX;
leon@pc:~$ getconf GNU_LIBPTHREAD_VERSION
NPTL 2.23
leon@pc:~$
在NPTL模型,同一个进程内的线程提供不同PID, 共用一个TGID;
Top命令看到的PID实际是TGID(等于主线程的PID);
Top –H线程视角看到的是每个线程的PID(不同,内核存在的真正PID);
线程ID
我们知道进程id的类型是pid_t,每个进程的id在整个系统中是唯一的,调用getpid(2)可以获得当前进程的id,是一个正整数值。
线程id的类型是thread_t,它只在当前进程中保证是唯一的,在不同的系统中thread_t这个类型有不同的实现,它可能是一个整数值,也可能是一个结构体,也可能是一个地址,所以不能简单地当成整数用printf打印,调用pthread_self(3)可以获得当前线程的id。
在Linux上,thread_t类型是一个地址值,属于同一进程的多个线程调用getpid(2)可以得到相同的进程号,而调用pthread_self(3)得到的线程号各不相同。
pthread_self是在C库实现的数据结构(TCB),与内核没关系;
多线程生命周期
1 线程的创建
#include <pthread.h>
int pthread_create(pthread_t *restrict thread,
const pthread_attr_t *restrict attr,
void *(*start_routine)(void*),
void *restrict arg);
返回值:成功返回0,失败返回错误号。
在一个线程中调用pthread_create()创建新的线程后,当前线程从pthread_create()返回继续往下执行,而新的线程所执行的代码由我们传给pthread_create的函数指针start_routine决定。
start_routine函数接收一个参数,是通过pthread_create的arg参数传递给它的,该参数的类型为void *,这个指针按什么类型解释由调用者自己定义。
start_routine的返回值类型也是void *,这个指针的含义同样由调用者自己定义。
start_routine返回时,这个线程就退出了,其它线程可以调用pthread_join得到start_routine的返回值,类似于父进程调用wait(2)得到子进程的退出状态,稍后详细介绍pthread_join。
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
pthread_t ntid;
void printids(const char *s)
{
pid_t pid;
pthread_t tid;
pid = getpid();
tid = pthread_self();
printf("%s pid %u tid %u (0x%x)\n", s, (unsigned int)pid, (unsigned int)tid, (unsigned int)tid);
}
void *thr_fn(void *arg)
{
printids(arg);
return NULL;
}
int main(void)
{
int err;
err = pthread_create(&ntid, NULL, thr_fn, "new thread: ");
if (err != 0)
{
fprintf(stderr, "can't create thread: %s\n", strerror(err));
exit(1);
}
printids("main thread:");
sleep(1);
return 0;
}
2 线程终止方法:
(1)从线程函数return; 这个方法对主线程不适用,从main函数return,会调用exit();
(2)一个线程可以调用pthread_cancel终止同一进程中的另一个线程。(会导致资源同步问题,在android中已经弃用);
(3)线程可以调用pthread_exit终止自己;
#include <pthread.h>
void pthread_exit(void *value_ptr);
value_ptr是void *类型,和线程函数返回值的用法一样,其它线程可以调用pthread_join获得这个指针。
整个进程退出
(4)main函数返回;
(5)调用进程级别的API;exit(),_exit();
(6)某一线程做了非常操作,引起segment fault;
需要注意,pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。
如果任意一个线程调用了exit或_exit,会调用exit_group(), 则整个进程的所有线程都终止;
从main函数return会调用exit,相当于调用exit_group;
退出单个线程,用pthread_exit();
exit()退出,调用exit_group(),所有线程退出;
pthread_exit()退出,单个线程退出;
资源的单位是进程;
线程退出,可能引起memory leak等leak;
线程不pthread_join可能引起leak;
3 join线程
#include <pthread.h>
int pthread_join(pthread_t thread, void **value_ptr);
返回值:成功返回0,失败返回错误号。
调用该函数的线程将挂起等待,直到id为thread的线程终止。thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的,总结如下:
(1)如果thread线程通过return返回,value_ptr所指向的单元里存放的是thread线程函数的返回值。
(2)如果thread线程被别的线程调用pthread_cancel异常终止掉,value_ptr所指向的单元里存放的是常数PTHREAD_CANCELED。
(3)如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参数。
如果对thread线程的终止状态不感兴趣,可以传NULL给value_ptr参数。
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
void *thr_fn1(void *arg)
{
printf("thread 1 returning\n");
return (void *)1;
}
void *thr_fn2(void *arg)
{
printf("thread 2 exiting\n");
pthread_exit((void *)2);
}
void *thr_fn3(void *arg)
{
while(1)
{
printf("thread 3 writing\n");
sleep(1);
}
}
int main(void)
{
pthread_t tid;
void *tret;
pthread_create(&tid, NULL, thr_fn1, NULL);
pthread_join(tid, &tret);
printf("thread 1 exit code %d\n", (int)tret);
pthread_create(&tid, NULL, thr_fn2, NULL);
pthread_join(tid, &tret);
printf("thread 2 exit code %d\n", (int)tret);
pthread_create(&tid, NULL, thr_fn3, NULL);
sleep(3);
pthread_cancel(tid);
pthread_join(tid, &tret);
printf("thread 3 exit code %d\n", (int)tret);
return 0;
}
leon@pc:~$ ./a.out
thread 1 returning
thread 1 exit code 1
thread 2 exiting
thread 2 exit code 2
thread 3 writing
thread 3 writing
thread 3 writing
thread 3 exit code -1
可见在Linux的pthread库中常数PTHREAD_CANCELED的值是-1。可以在头文件pthread.h中找到它的定义:
#define PTHREAD_CANCELED ((void *) -1)
pthread_join()退出,释放线程资源;如果线程不做pthread_join,线程栈不会释放,线程在堆上malloc的内存也不会释放(进程没有退出);
4 线程detach
如果线程被设置为detach状态,线程一旦终止就立刻回收它占用所有资源,
而不保留终止状态;
不能对一个已经处于detach状态的线程调用pthread_join,这样的调用将返回EINVAL。对一个尚未detach的线程调用pthread_join或pthread_detach都可以把该线程置为detach状态,也就是说,不能对同一线程调用两次pthread_join,或者如果已经对一个线程调用了pthread_detach就不能再调用pthread_join了。
detach工程上很少用,只有调试线程用;
5多线程的信号处理
信号是进程级的概念,给某个进程发信号,该进程内所有线程都会响应;
对同一个信号,多个线程都设置处理函数,则最后一个设置生效;
POSIX要求可以对特定的线程发送信号,用pthread_kill()实现;
线程组有共用的sispending, 每个线程有私有的sispending;
pthread_kill()对应私有sispending;