多线程解决什么问题:
- 线程生命周期问题;
- 线程如何划分的问题 (性能问题);
- 线程如何通信的问题;
信号的行为是进程级别的;对任何线程发信号,线程组都会响应;
线程与进程关系
多线程通信开销 (不涉及大量数据交互),远低于多进程;
多进程比多线程好调试;
关系不亲密,适合用多进程模型;否则用多线程模型 ;
线程的概念
由于同一进程的多个线程共享同一地址空间,因此 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;