线程

基本概念

LWP:light weight process

轻量级的进程,本质仍是进程(linux环境下)

与win不同,区别挺大的,实现原理都不一样。

独立的进制空间,拥有PCB

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

区别:在于是否共享地址空间。

进程可以理解为独居

线程可以理解为合租

linux下的最小执行单位:线程

最小的资源分配单位:进程。

简单原理

进程进行类似fork的操作,但是没有对父进程地址空间进行克隆clone,而是使用原有进程的三级页表,页表最终指向物理页面,中间有三次映射,也被称为三级映射

进程->虚拟地址(页目录->页表->物理页面)MMU->物理地址

不过线程不是完全使用进程的三级页表,实际上线程与进程的区别在于寄存器和栈的内容,不一样

函数指针调用分配在栈中(用户程序之间的栈)

两个指针(ebp,esp)组合规划(滑动窗口)一个栈帧,一个函数(还有局部变量等)占用的就是一个栈帧

进程在切换时要保存寄存器的值,这些值分配在内核区的栈空间之中

线程共享资源

  • 文件文件描述符表

  • 共享信号处理方式(信号建议不要与线程混用)

  • 除了栈空间都共享

线程非共享资源

  • 线程Id

  • 处理器现场(寄存器)和内核栈

  • 独立的栈空间

  • errno变量(在代码段中)

  • 信号屏蔽字

  • 调度优先级

线程优缺

优点:程序并发性高,开销小,数据通信,共享数据方便

缺点:库函数不稳定,调试,编写困难,gdb不支持,对信号支持不好。

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

控制原语

pthread_self()

获取线程id 注意不是LWP。

gcc编译的时候需要加入 -pthread。

pthread_creat()

1
2
3
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);

  • thread:线程id(传出)
  • attr:线程属性(传入)
  • start_routine:线程的主控函数(传入)
  • arg :线程主控函数的参数(传入)

返回值:int,成功0 ,出错返回error的错误编号

代码示例

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
#include<stdio.h>
#include<pthread.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
int var = 1;
void *thrd_func(void *arg){

int i = (int)arg;
var += 1;
printf("In %d thread : %ld\n", i+1, pthread_self());
return NULL;
}

int main(void){
pthread_t tid;
int ret;
int i;
for (i = 0; i < 5; i++)
{
ret = pthread_create(&tid, NULL, thrd_func, (void *)i);
if(ret != 0){
fprintf(stderr, "errno :%d: %s", ret, strerror(ret));
exit(1);
}
}
sleep(1);
printf("var = %d\n", var);
return 0;
}

pthread_exit()

单个线程退出

与exit退出进程不同

1
void pthread_exit(void *retval);
  • retval 退出时当前线程状态,传出参数

在线程中执行exit()会直接将进程和线程退出

pthread_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
#include<stdio.h>
#include<pthread.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>

typedef struct
{
char ch;
int var;
char str[64];
}test_t;

void *thrd_func(void* retvar){

test_t* retval = (test_t*)retvar;
retval->ch = 'a';
retval->var = 200;
strcpy(retval->str, "hello\n");
pthread_exit((void *)retval);

}

int main(void){
pthread_t tid;
int ret;
int i;
test_t* retval = (test_t*)malloc(sizeof(test_t));
ret = pthread_create(&tid, NULL, thrd_func, (void*)retval);
if(ret != 0){
fprintf(stderr, "errno :%d: %s", ret, strerror(ret));
exit(1);
}
pthread_join(tid, (void **)&retval);
printf("ch = %c , var = %d , str = %s", retval->ch, retval->var, retval->str);
free(retval);
pthread_exit((void *)1);
}

回收COUNT个子进程

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<pthread.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#define COUNT 5

int var = 100;

void tfn(void* arg){
int i = (int)arg;
sleep(i);
if(i == 1){
var = 333;
pthread_exit((void *)var);
}else if(i == 3){
var = 777;
pthread_exit((void *)var);
}else if(i == 4){
var = 666;
pthread_exit((void *)var);
}else{
pthread_exit((void *)var);
}
}

int main(){

int *tid[COUNT];
int *ret[COUNT];
int i;
for (i = 0; i < COUNT; i++){
pthread_create(&tid[i], NULL, tfn, (void*)i);
}
for(i = 0; i < COUNT; i++){
pthread_join(tid[i], (void**)&ret[i]);
printf("回收了第%d个子线程,返回值ret= %d\n", i+1, ret[i]);
}
}

prthread_detach()

线程设定为分离态,设定一个线程退出时自行回收。

1
int pthread_detach(pthread_t thread);

再进行回收就会报错了,因为已经自行完事了回收了寂寞。

prthread_cancel()

取消线程

1
int pthread_cancel(pthread_t thread);

这个线程的取消并不是实时的,需要达到一个取消点才会取消,比如说内核调用,系统调用。或者自己设定1个取消点在线程中

具体取消点函数可看man 7 pthreads 或者apue 12.7

线程属性

创建线程的第二个参数,pthread_attr_t;

attr_t结构

  • int etachstate
    • 线程的分离状态
  • int schedpolicy
    • 线程调度策略
  • struct sched_param schedparam
    • 线程调度参数
  • int inheritsched
    • 线程调度继承性
  • int scope
    • 作用域
  • size_t guardsize
    • 线程栈末尾的警戒缓冲区大小
    • 栈空间的函数地址的线程栈并非连着的,之间有一个警戒区分割
  • int stackaddr_set
    • 线程的栈设置
  • void* stackaddr
    • 线程栈的地址位置
  • size_t stacksize
    • 线程栈的大小
    • 最大线程大小,N个线程对这块大小进行均分分配

加粗的是一般比较常用的属性

属性初始化

提前定义一个结构体pthread_attr

调用pthread_attr_init(),进行值的初始化

1
int pthread_attr_init(pthread_attr_t *attr);

属性设置

常用的:

设置状态

1
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);

参数二有两个宏参数可以使用

  • PTHREAD_CREATE_DETACHED
    • 分离态
  • PTHREAD_CREATE_JOINABLE
    • 可回收态

需要注意的是,在分离态的子线程如果结束过快,create函数的返回的线程id,可能就是错误的id,需要在该子线程中调用一个pthread_cond_timedwait()函数,保证在结束前返回线程id。

修改线程栈空间

1
int pthread_attr_setstack(pthread_attr_t *attr, void *stackaddr, size_t stacksize);

参数二传入一个栈地址,参数三就是要设定的大小

需要先在在堆空间申请一个空间(毕竟说到底,用户能修改使用的还是堆)

stackaddr = malloc (SIZE);

设定好栈空间后,创建的函数就会放到这个设置好的新的空间中

大小的限制取决于用户的malloc的最大大小。

而当过小过大都会回到栈默认值中

线程使用注意点

  • 需要注意NPTL:线程库的版本
  • 避免僵尸。
  • 线程中一般不使用fork创建子进程
    • 子进程中只有调用子进程的线程存在
  • 线程中避免使用信号

多线程拷贝

之前有做过的多进程拷贝,这次再做个多线程拷贝,在linux系统下线程本质上还是进程,所以区别不大,IPC依旧选择MMAP。

这里直接上代码,没有难点,巩固一下线程的基本原语和基本特性

这里试了试最大线程数:我可以开辟的最大线程为15297个线程,如果需要更大的线程数,就需要手动将内存空间开辟到堆中,自己设定进程大小。在这个代码中就不演示了想要加的话可以在写一个判断设置线程空间大小,单纯图一乐

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
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
#include<stdio.h>
#include<pthread.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<fcntl.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<sys/mman.h>

size_t fsize = 0;
int tnums = 1;
char* fmw = NULL;
char* fmr = NULL;

void fileCopy(void* arg){
int i = (int)arg;
int len = fsize/tnums;
if(i < tnums){
if(i == tnums-1){
memmove(fmw + (len*i), fmr + (len*i), len + (int)(fsize%len));
}else{
memmove(fmw + (len*i), fmr + (len*i), len);
}
}
pthread_exit(NULL);
}
void progressBar(){
char buff[40];
memset(buff,'\0',40);
char label[] = "\\/\\/";
for (int i = 0; i < 40; i++)
{
printf("[%-39s][%c][%.lf%%]\r",buff, label[i%4], (i+1)*2.5);
fflush(stdout);
buff[i] = '>';
usleep(50000/tnums);
}
putchar('\n');
}


int main (int argc, char *argv[]){
if(argc != 4){
printf("食用方法:源文件名,新文件名,使用的线程数\n");
exit(1);
}

int fdr = open(argv[1], O_RDONLY);
if(fdr < 0){
perror("源文件");
exit(1);
}
int fdw =open(argv[2], O_RDWR | O_CREAT | O_TRUNC, 0777);
if(fdw < 0){
perror("目的文件");
exit(1);
}
struct stat finfo;
fstat(fdr,&finfo);
fsize = finfo.st_size;
int ret = ftruncate(fdw, fsize);
if(ret < 0){
printf("truncate error");
exit(1);
}

fmr = mmap(NULL, fsize, PROT_READ, MAP_SHARED, fdr, 0);
if(fmr == MAP_FAILED){
perror("目的文件映射");
exit(1);
}
close(fdr);
fmw = mmap(NULL, fsize, PROT_WRITE, MAP_SHARED, fdw, 0);
if(fmw == MAP_FAILED){
perror("源文件映射");
exit(1);
}
close(fdw);

//根据线程数创建线程
tnums = atoi(argv[3]);
if(tnums >= 10000){
printf("你要复制什么玩意?确定要这么多进程数?");
}
pthread_t tid[tnums + 1];//这里是之前测试回收是否成功的记录
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
int i;
for (i = 0; i < tnums; i++)
{
ret = pthread_create(&tid[i], &attr, fileCopy, (void *)i);
if(ret < 0){
fprintf(stderr, "errno :%d: %s", ret, strerror(ret));
exit(1);
}
}
ret = pthread_create(&tid[tnums], &attr, progressBar, (void *)i);
if(ret < 0){
fprintf(stderr, "errno :%d: %s", ret, strerror(ret));
exit(1);
}
munmap(fmr, fsize);
munmap(fmw, fsize);
pthread_exit(NULL);
}