1.守护进程

守护进程(Daemon Process),也就是通常说的Daemon(精灵)进程,是 Linux 中的后台服务进程,通常独立于控制终端并且周期性地执行某种任务或等待处理 某些发生的事件。一般采用以 d 结尾的名字。

Linux 后台的一些系统服务进程,没有控制终端,不能直接和用户交互。不受用户登录、注销的影响,一直在运行着,他们都是守护进程。如:预读入缓输出机制的实现;ftp 服务器;nfs 服务器等。

创建守护进程,最关键的一步是调用 setsid 函数创建一个新的 Session,并成为 Session Leader。

1.1 进程组

多个进程的集合就是进程组,也称之为作业。BSD 于 1980 年前后向 Unix 中增加的一个新特性。代表一个或多个进程的集合。每个进程都属于一个进程组。在waitpid函数和 kill 函数的参数中都曾使用到。操作系统设计的进程组的概念,是为了简化对多个进程的管理。

当父进程,创建子进程的时候,默认子进程与父进程属于同一进程组。进程组 ID == 第一个进程 ID(组长进程)。 所以,组长进程标识:其进程组 ID == 其进程 ID

可以使用 kill -SIGKILL -进程组 ID(负的)来将整个进程组内的进程全部杀死。
组长进程可以创建一个进程组,创建该进程组中的进程,然后终止。只要进程组中有一个进程存在,进程组就存在,与组长进程是否终止无关。

进程组生存期:进程组创建到最后一个进程离开(终止或转移到另一个进程组)。

一个进程可以为自己或子进程设置进程组 ID

下面介绍几个常用的进程组函数:

得到当前进程所在的进程组的组 ID

pid_t getpgrp(void);

获取指定的进程所在的进程组的组 ID,参数 pid 就是指定的进程

pid_t getpgid(pid_t pid);

将某个进程移动到其他进程组中或者创建新的进程组

int setpgid(pid_t pid, pid_t pgid);

函数参数:

pid: 某个进程的进程 ID

pgid: 某个进程组的组 ID

如果 pgid 对应的进程组存在,pid 对应的进程会移动到这个组中,pid != pgid

如果 pgid 对应的进程组不存在,会创建一个新的进程组,因此要求 pid == pgid, 当前进程就是组长了

函数返回值:

函数调用成功返回 0,失败返回 - 1

1.2 会话

会话 (session) 是由一个或多个进程组组成的,一个会话可以对应一个控制终端,也可以没有。一个普通的进程可以调用 setsid() 函数使自己成为新 session 的领头进程(会长),并且这个 session 领头进程还会被放入到一个新的进程组中。先来看一下 setsid() 函数的原型:

1.3 setsid()函数

创建一个会话,并以自己的 ID 设置进程组 ID,同时也是新会话的 ID。

函数原型

1
2
#include <unistd.h>
pid_t setsid(void);

函数返回值

  • 成功:返回调用进程的会话 ID
  • 失败:-1,设置 errno

1.4 getsid()函数

获取进程所属的会话 ID

函数原型

1
2
#include <unistd.h>
pid_t getsid(pid_t pid);

函数返回值

  • 成功:返回调用进程的会话 ID
  • 失败:-1,设置 errno

==pid 为 0 表示察看当前进程 session ID==

使用这两个函数的注意事项:

1.调用这个函数的进程不能是组长进程,如果是,该函数调用失败,如何保证这个函数能调用成功呢?

  • 先 fork () 创建子进程,终止父进程,让子进程调用这个函数

2.如果调用这个函数的进程不是进程组长,会话创建成功

  • 这个进程会变成当前会话中的第一个进程,同时也会变成新的进程组的组长
  • 该函数调用成功之后,当前进程就脱离了控制终端,因此不会阻塞终端

1.5 创建守护进程

  1. 创建子进程,父进程退出
  2. 在子进程中创建新会话 setsid()函数 使子进程完全独立出来,脱离控制
  3. 改变当前目录位置 chdir()函数 防止占用可卸载的文件系统, 也可以换成其它路径
  4. 重设文件权限掩码 umask()函数 防止继承的文件创建屏蔽字拒绝某些权限,增加守护进程灵活性
  5. 通常根据需要,关闭/重定向 文件描述符
  6. 开始执行守护进程业务逻辑,通常是while()循环

小案例

写一个守护进程,每隔 2s 获取一次系统时间,并将得到的时间写入到磁盘文件中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <signal.h>
#include <sys/time.h>
#include <time.h>

// 信号的处理动作
void writeFile(int num)
{
// 得到系统时间
time_t seconds = time(NULL);
// 时间转换, 总秒数 -> 可以识别的时间字符串
struct tm* loc = localtime(&seconds);
// sprintf();
char* curtime = asctime(loc); // 自带换行
// 打开一个文件, 如果文件不存在, 就创建, 文件需要有追加属性
// ./对应的是哪个目录? /home/robin
// 0664 & ~022
int fd = open("./time+++++++.log", O_WRONLY|O_CREAT|O_APPEND, 0664);
write(fd, curtime, strlen(curtime));
close(fd);
}
void sys_err(const char *str)
{
perror(str);
exit(1);
}

int main()
{
int ret;
// 1. 创建子进程, 杀死父进程
pid_t pid = fork();
if(pid > 0)
{
// 父进程终止
exit(0); // kill(getpid(), 9); raise(9); abort();
}

// 2. 子进程, 将其变成会话, 脱离当前终端
pid = setsid();
if(pid == -1)
{
sys_err("setsid error\n");
}
// 3. 修改进程的工作目录, 修改到一个不能被修改和删除的目录中 /home/robin
ret = chdir("/home/robin");
if(ret == -1)
{
sys_err("chdir error\n")
}
// 4. 设置掩码, 在进程中创建文件的时候这个掩码就起作用了
umask(022);

// 5. 重定向和终端关联的文件描述符 -> /dev/null
int fd = open("/dev/null", O_RDWR);
dup2(fd, STDIN_FILENO);
dup2(fd, STDOUT_FILENO);
dup2(fd, STDERR_FILENO);

// 5. 委托内核捕捉并处理将来发生的信号-SIGALRM(14)
struct sigaction act;
act.sa_flags = 0;
act.sa_handler = writeFile;
sigemptyset(&act.sa_mask);
sigaction(SIGALRM, &act, NULL);

// 6. 设置定时器
struct itimerval val;
val.it_value.tv_sec = 2;
val.it_value.tv_usec = 0;
val.it_interval.tv_sec = 2;
val.it_interval.tv_usec = 0;
setitimer(ITIMER_REAL, &val, NULL);

while(1)
{
sleep(100);
}

return 0;
}

2.线程

2.1 什么是线程?

线程是轻量级的进程(LWP:light weight process),在 Linux 环境下线程的本质仍是进程。在计算机上运行的程序是一组指令及指令参数的组合,指令按照既定的逻辑控制计算机运行。操作系统会以进程为单位,分配系统资源,可以这样理解,==进程是资源分配的最小单位,线程是操作系统调度执行的最小单位==。

进程:独立地址空间,拥有 PCB

线程:有独立的 PCB,但没有独立的地址空间(共享)

区别:在于是否共享地址空间。 独居(进程);合租(线程)。

2.2 Linux 内核线程实现原理(了解即可)

类 Unix 系统中,早期是没有“线程”概念的,80 年代才引入,借助进程机制实现出了线程的概念。因此在这类系统中,进程和线程关系密切。

  1. 轻量级进程(light-weight process),也有 PCB,创建线程使用的底层函数和进程一样,都是 clone
  2. 从内核里看进程和线程是一样的,都有各自不同的 PCB,但是 PCB 中指向内存资源的三级页表是相同的
  3. 进程可以蜕变成线程
  4. 线程可看做寄存器和栈的集合
  5. 在 linux 下,线程最是小的执行单位;进程是最小的分配资源单位
    查看 LWP 号:ps –Lf pid 查看指定线程的 lwp 号(线程号)。

三级映射:进程 PCB –> 页目录(可看成数组,首地址位于 PCB 中) –> 页表 –> 物理页面 –> 内存单元
对于进程来说,相同的地址(同一个虚拟地址)在不同的进程中,反复使用而不冲突。原因是他们虽虚拟地址一样,但,页目录、页表、物理页面各不相同。相同的虚拟地址,映射到不同的物理页面内存单元,最终访问不同的物理页面。
但!线程不同!两个线程具有各自独立的 PCB,但共享同一个页目录,也就共享同一个页表和物理页面。所以两个 PCB 共享一个地址空间。 实际上,无论是创建进程的 fork,还是创建线程的 pthread_create,底层实现都是调用同一个内核函数 clone。 如果复制对方的地址空间,那么就产出一个“进程”;如果共享对方的地址空间,就产生一个“线程”。 因此:==Linux 内核是不区分进程和线程的==。只在用户层面上进行区分。所以,线程所有操作函数 pthread_* 是库函数,而非系统调用。

2.4 线程共享资源和非共享资源

共享资源

1.文件描述符表

2.每种信号的处理方式

3.当前工作目录

4.用户 ID 和组 ID

5.内存地址空间 (.text/.data/.bss/heap/共享库)

非共享资源

1.线程 id

2.处理器现场和栈指针(内核栈)

3.独立的栈空间(用户空间栈)

4.errno 变量

5.信号屏蔽字

6.调度优先级

线程优缺点

优点: 1. 提高程序并发性 2. 开销小 3. 数据通信、共享数据方便

缺点: 1. 库函数,不稳定 2. 调试、编写困难、gdb 不支持 3. 对信号支持不好

优点相对突出,缺点均不是硬伤。Linux 下由于实现方法导致进程、线程差别不是很大。

3.创建线程

3.1 线程函数

每一个线程都有一个唯一的线程 ID,ID 类型为 pthread_t,这个 ID 是一个无符号长整形数(%lu),如果想要得到当前线程的线程 ID,可以调用如下函数:

pthread_t pthread_self(void); // 返回当前线程的线程ID

在一个进程中调用线程创建函数,就可得到一个子线程,和进程不同,需要给每一个创建出的线程指定一个处理函数,否则这个线程无法工作。

1
2
3
4
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
// Compile and link with -pthread, 线程库的名字叫pthread, 全名: libpthread.so libptread.a
  • 参数:

    • thread: 传出参数,是无符号长整形数,线程创建成功,会将线程 ID 写入到这个指针指向的内存中
    • attr: 线程的属性,一般情况下使用默认属性即可,写 NULL
    • start_routine: 函数指针,创建出的子线程的处理动作,也就是该函数在子线程中执行。
    • arg: 作为实参传递到 start_routine 指针指向的函数内部
  • 返回值:线程创建成功返回 0,创建失败直接返回对应的错误号,不会设置errono,所以用perror()是行不通的,这点要注意,应直接用strerror()

小案例

循环创建多个子线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <fcntl.h>

void *tfn(void *arg)
{
int i = *((int *)arg); //接收从主线程传递过来的地址,因此后面的i读取的都是主线程中的i
sleep(i);
printf("i am %dth thread:pid = %d, tid = %lu\n", i+1, getpid(), pthread_self());
return NULL;
}

int main(int argc, char *argv[])
{
int i;
int ret;
pthread_t tid;
for(i = 0; i < 5; i++)
{
ret = pthread_create(&tid, NULL, tfn, (void *)&i); //将主线程的i的地址传递进去,会出错
if(ret != 0)
{
fprintf(stderr ,"create error:%s\n", strerror(ret));
exit(1);
}

}
sleep(i); //主线程执行完之后,就销毁了整个进程的地址空间,子线程就会无法打印,所以应该让主线程睡一会,等待子线程的执行
printf("i am main pid = %d, tid = %lu\n", getpid(), pthread_self());

return 0;
}

编译运行,结果如下:

可以看到,不符合我们的预期想法,错误原因在于,子线程将主线程中i的地址传递了进去,因此子线程运行时会去读取主线程里的i值,而主线程里的i是动态变化的,不固定。所以,应该传递值而不是地址。

修改的代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <fcntl.h>

void *tfn(void *arg)
{
int i = (long)arg; //注意这里要强转成long类型,因为64位linux系统,指针占8个字节,int占4个字节,如果强转成int会造成精度的丢失
sleep(i);
printf("i am %dth thread:pid = %d, tid = %lu\n", i+1, getpid(), pthread_self());
return NULL;
}

int main(int argc, char *argv[])
{
int i;
int ret;
pthread_t tid;
for(i = 0; i < 5; i++)
{
ret = pthread_create(&tid, NULL, tfn, (void *)i); //按值进行传递
if(ret != 0)
{
fprintf(stderr ,"create error:%s\n", strerror(ret));
exit(1);
}

}
sleep(i); //主线程执行完之后,就销毁了整个进程的地址空间,子线程就会无法打印,所以应该让主线程睡一会,等待子线程的执行
printf("i am main pid = %d, tid = %lu\n", getpid(), pthread_self());

return 0;
}

编译运行,结果如下:

注意

编译时,要指定参数-lpthread指定动态库libpthread.so

3.2 线程间全局变量共享

直接看个代码,在子线程里更改全局变量,看主线程里的该变量有啥变化:

编译运行,结果如下

可以看到,子线程里更改全局变量后,主线程里也跟着发生变化。

3.线程退出

在编写多线程程序的时候,如果想要让线程退出,但是不会导致虚拟地址空间的释放(针对于主线程),我们就可以调用线程库中的线程退出函数,只要调用该函数当前线程就马上退出了,并且不会影响到其他线程的正常运行,不管是在子线程或者主线程中都可以使用。

1
2
#include <pthread.h>
void pthread_exit(void *retval);
  • 参数:线程退出的时候携带的数据,当前子线程的主线程会得到该数据。如果不需要使用,指定为 NULL

注意区别三者

  • exit(); 退出当前进程。
  • return: 返回到调用者那里去。
  • pthread_exit(): 退出当前线程。

下面是线程退出的示例代码,可以在任意线程的需要的位置调用该函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>

// 子线程的处理代码
void* working(void* arg)
{
sleep(1);
printf("我是子线程, 线程ID: %ld\n", pthread_self());
for(int i=0; i<9; ++i)
{
if(i==6)
{
pthread_exit(NULL); // 直接退出子线程
}
printf("child == i: = %d\n", i);
}
return NULL;
}

int main()
{
// 1. 创建一个子线程
pthread_t tid;
pthread_create(&tid, NULL, working, NULL);

printf("子线程创建成功, 线程ID: %ld\n", tid);
// 2. 子线程不会执行下边的代码, 主线程执行
printf("我是主线程, 线程ID: %ld\n", pthread_self());
for(int i=0; i<3; ++i)
{
printf("i = %d\n", i);
}

// 主线程调用退出函数退出, 地址空间不会被释放
pthread_exit(NULL);

return 0;
}

编译运行,结果如下:

4.线程回收

4.1线程回收函数

线程和进程一样,子线程退出的时候其内核资源主要由主线程回收,线程库中提供的线程回收函叫做 pthread_join(),这个函数是一个阻塞函数,如果还有子线程在运行,调用该函数就会阻塞,子线程退出函数解除阻塞进行资源的回收,函数被调用一次,只能回收一个子线程,如果有多个子线程则需要循环进行回收。

另外通过线程回收函数还可以获取到子线程退出时传递出来的数据,函数原型如下:

1
2
3
4
#include <pthread.h>
// 这是一个阻塞函数, 子线程在运行这个函数就阻塞
// 子线程退出, 函数解除阻塞, 回收对应的子线程资源, 类似于回收进程使用的函数 wait()
int pthread_join(pthread_t thread, void **retval);
  • 参数:

    • thread: 要被回收的子线程的线程 ID
    • retval: 二级指针,指向一级指针的地址,是一个传出参数,这个地址中存储了 pthread_exit () 传递出的数据,如果不需要这个参数,可以指定为 NULL
  • 返回值:线程回收成功返回 0,回收失败返回错误号。

4.2 回收子线程数据

在子线程退出的时候可以使用 pthread_exit() 的参数将数据传出,在回收这个子线程的时候可以通过 phread_join() 的第二个参数来接收子线程传递出的数据。接收数据有很多种处理方式,下面来举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>

// 定义结构
struct Persion
{
int id;
char name[36];
int age;
};

struct Persion p; // 定义全局变量

// 子线程的处理代码
void *working(void *arg)
{
int i;
printf("我是子线程, 线程ID: %ld\n", pthread_self());
for (i = 0; i < 9; ++i)
{
printf("child == i: = %d\n", i);
if (i == 6)
{
// 使用全局变量
p.age = 12;
strcpy(p.name, "tom");
p.id = 100;
// 该函数的参数将这个地址传递给了主线程的pthread_join()
pthread_exit(&p);
}
}
return NULL;
}

int main()
{
int i;
// 1. 创建一个子线程
pthread_t tid;
pthread_create(&tid, NULL, working, NULL);

printf("子线程创建成功, 线程ID: %ld\n", tid);
// 2. 子线程不会执行下边的代码, 主线程执行
printf("我是主线程, 线程ID: %ld\n", pthread_self());
for (i = 0; i < 3; ++i)
{
printf("i = %d\n", i);
}

// 阻塞等待子线程退出
void *ptr = NULL;
// ptr是一个传出参数, 在函数内部让这个指针指向一块有效内存
// 这个内存地址就是pthread_exit() 参数指向的内存
pthread_join(tid, &ptr);
// 打印信息
struct Persion *pp = (struct Persion *)ptr;
printf("name: %s, age: %d, id: %d\n", pp->name, pp->age, pp->id);
printf("子线程资源被成功回收...\n");

return 0;
}

编译运行,结果如下:

5.线程分离

在某些情况下,程序中的主线程有属于自己的业务处理流程,如果让主线程负责子线程的资源回收,调用 pthread_join() 只要子线程不退出主线程就会一直被阻塞,主要线程的任务也就不能被执行了。

在线程库函数中为我们提供了线程分离函数 pthread_detach(),调用这个函数之后指定的子线程就可以和主线程分离,当子线程退出的时候,其占用的内核资源就被系统的其他进程接管并回收了。线程分离之后在主线程中使用 pthread_join() 就回收不到子线程资源了。

1
2
3
#include <pthread.h>
// 参数就子线程的线程ID, 主线程就可以和这个子线程分离了
int pthread_detach(pthread_t thread);

小案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>

// 子线程的处理代码
void* working(void* arg)
{
printf("我是子线程, 线程ID: %ld\n", pthread_self());
for(int i=0; i<9; ++i)
{
printf("child == i: = %d\n", i);
}
return NULL;
}

int main()
{
// 1. 创建一个子线程
pthread_t tid;
pthread_create(&tid, NULL, working, NULL);

printf("子线程创建成功, 线程ID: %ld\n", tid);
// 2. 子线程不会执行下边的代码, 主线程执行
printf("我是主线程, 线程ID: %ld\n", pthread_self());
for(int i=0; i<3; ++i)
{
printf("i = %d\n", i);
}

// 设置子线程和主线程分离
pthread_detach(tid);

// 让主线程自己退出即可
pthread_exit(NULL);

return 0;
}

6.线程取消

函数原型

1
2
3
//杀死一个线程。需要到达取消点(保存点),即进行一次系统调用
#include <pthread.h>
int pthread_cancel(pthread_t thread);
  • 参数:
    • thread: 待杀死的线程id
  • 返回值:
    • 成功:0
    • 失败:直接返回错误号errno

如果,子线程没有到达取消点(即系统调用), 那么 pthread_cancel 无效。 我们可以在程序中,手动添加一个取消点。使用 pthread_testcancel(); 成功被 pthread_cancel() 杀死的线程,返回 -1.使用pthead_join 回收。

小案例

主线程调用pthread_cancel杀死子线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <stdio.h>  
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>


void *tfn(void *arg){
while (1) {
printf("thread: pid = %d, tid = %lu\n", getpid(), pthread_self());
sleep(1);
}

return NULL;
}

int main(int argc, char *argv[]){
pthread_t tid;

int ret = pthread_create(&tid, NULL, tfn, NULL);
if (ret != 0) {
fprintf(stderr, "pthread_create error:%s\n", strerror(ret));
exit(1);
}

printf("main: pid = %d, tid = %lu\n", getpid(), pthread_self());

sleep(5);

ret = pthread_cancel(tid); // 终止线程
if (ret != 0) {
fprintf(stderr, "pthread_cancel error:%s\n", strerror(ret));
exit(1);
}

while (1);

pthread_exit(NULL);
}

编译运行,结果如下

终止线程方式

总结:终止某个线程而不终止整个进程,有三种方法:

  1. 从线程主函数 return。这种方法对主控线程不适用,从main 函数 return 相当于调用 exit。

  2. 一个线程可以调用 pthread_cancel 终止同一进程中的另一个线程。

  3. 线程可以调用 pthread_exit 终止自己。

7.控制原语对比

8.线程属性

8.1 基本概念

linux 下线程的属性是可以根据实际项目需要,进行设置。之前我们讨论的线程都是采用线程的默认属性,默认属性已经可以解决绝大多数开发时遇到的问题。如我们对程序的性能提出更高的要求那么需要设置线程属性,比如可以通过设置线程栈的大小来降低内存的使用,增加最大线程个数。

1
2
3
4
5
6
7
8
9
10
11
12
 typedef struct
{
int detachstate; //线程的分离状态
int schedpolicy; //线程的调度策略
struct sched schedparam;//线程的调度参数
int inheritsched; //线程的继承性
int scope; //线程的作用域
size_t guardsize; //线程栈末尾的警戒缓冲区大小
int stackaddr_set; //线程栈的设置
void* stackaddr; //线程栈的启始位置
size_t stacksize; //线程栈大小
}pthread_attr_t;

主要结构体成员:

  1. 线程分离状态

  2. 线程栈大小(默认平均分配)

  3. 线程栈警戒缓冲区大小(位于栈末尾)

属性值不能直接设置,须使用相关函数进行操作,初始化的函数为 pthread_attr_init,这个函数必须在 pthread_create 函数之前调用。之后须用 pthread_attr_destroy 函数来释放资源。

线程属性主要包括如下属性:作用域(scope)、栈尺寸(stack size)、栈地址(stack address)、优先级(priority)、 分离的状态(detached state)、调度策略和参数(scheduling policy and parameters)。默认的属性为非绑定、非分离、缺省的堆栈、与父进程同样级别的优先级。

8.1线程属性设置分离线程

1 .pthread_attr_t attr 创建一个线程属性结构体变量

2 .pthread_attr_init(&attr); 初始化线程属性

3 .pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); 设置线程属性为 分离态

4 .pthread_create(&tid, &attr, tfn, NULL); 借助修改后的 设置线程属性 创建为分离态的新线程

5 .pthread_attr_destroy(&attr); 销毁线程属性

小案例

调整线程状态,使线程创建出来就是分离态,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#include <stdio.h>  
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>


void *tfn(void *arg)
{
printf("thread: pid = %d, tid = %lu\n", getpid(), pthread_self());

return NULL;
}

int main(int argc, char *argv[])
{
pthread_t tid;

pthread_attr_t attr;

int ret = pthread_attr_init(&attr);
if (ret != 0) {
fprintf(stderr, "attr_init error:%s\n", strerror(ret));
exit(1);
}

ret = pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); // 设置线程属性为 分离属性
if (ret != 0) {
fprintf(stderr, "attr_setdetachstate error:%s\n", strerror(ret));
exit(1);
}

ret = pthread_create(&tid, &attr, tfn, NULL);
if (ret != 0) {
perror("pthread_create error");
}

ret = pthread_attr_destroy(&attr);
if (ret != 0) {
fprintf(stderr, "attr_destroy error:%s\n", strerror(ret));
exit(1);
}

ret = pthread_join(tid, NULL);
if (ret != 0) {
fprintf(stderr, "pthread_join error:%s\n", strerror(ret));
exit(1);
}

printf("main: pid = %d, tid = %lu\n", getpid(), pthread_self());

pthread_exit((void *)0);
}

编译运行,如下所示:

如图,pthread_join报错,说明线程已经自动回收,设置分离成功。

9.线程使用注意事项

  1. 主线程退出其他线程不退出,主线程应调用 pthread_exit

  2. 避免僵尸线程

    pthread_join

    pthread_detach

    pthread_create 指定分离属性

    被 join 线程可能在 join 函数返回前就释放完自己的所有内存资源,所以不应当返回被回收线程栈中的值;

  3. mallocmmap 申请的内存可以被其他线程释放

  4. 应避免在多线程模型中调用 fork 除非,马上 exec,子进程中只有调用 fork 的线程存在,其他线程在子进程中均 pthread_exit

  5. 信号的复杂语义很难和多线程共存,应避免在多线程引入信号机制