线程模型:
我们前面学过,进程是是一个程序所分配的资源,是程序执行一次的过程,其参与内核的调用,并且互不影响。在Linux系统里,每当有进程被创建,系统就会为其创建一个叫task_sturck的存储空间(类似文件夹这种,方便管理和操控)。
但是进程与进程是不共用一个存储空间的,因此在系统每次调用程序的时候创建进程的时候,都需要把那个程序所处的存储空间复制一遍到高速缓存(cache)中,但是高速缓存的空间有限,往往只能存储一两个程序的资源,那我们在进程和进程切换的时候,高速缓存就要不停的洗数据、存数据,因此系统会开销会很大。
所以系统引入了轻量级进程LWP,也就是线程。
同一个进程里的不同线程都是共用一个存储空间,这样系统调用的时候就可以让高速缓存直接调用当前的存储空间的数据,切换的时候直接在这个数据中找对应的程序就可以了。
(ts:Linux系统是不区分线程和进程的,对于系统来说,两者是几乎一样的对象,所以线程也会被系统包成task_sturck。)
Linux线程库:
linux线程库是<pthread.h>。
线程的创建:
通过库函数
int pthread_create(pthread_t *thread,const pthread_arr_t *arr,void *(*routine)(void *),void *arg);
成功返回0,失败返回错误码,其中:
pthread_t 是 pthread 库定义的变量,thread 是线程对象。
attr是线程属性,写NULL就是默认属性。
routine是指针函数,线程执行的函数。
arg就是和进程一样的,传给线程的参数文件。
线程参数有:
| 属性名 | 作用 | 相关函数 |
|---|---|---|
| detach state | 设置线程是否为分离线程(detached),分离线程结束后资源自动回收,不需要 pthread_join。 | pthread_attr_setdetachstate() |
| stack size | 设置线程栈大小(单位:字节)。 | pthread_attr_setstacksize() |
| stack address | 设置线程栈的起始地址(一般不推荐手动设置)。 | pthread_attr_setstackaddr() |
| guard size | 设置栈保护区大小(防止栈溢出)。 | pthread_attr_setguardsize() |
| inherit scheduler | 设置是否继承父线程的调度策略。 | pthread_attr_setinheritsched() |
| scheduling policy | 设置线程调度策略(如 SCHED_FIFO, SCHED_RR, SCHED_OTHER)。 | pthread_attr_setschedpolicy() |
| scheduling priority | 设置线程调度优先级(需与调度策略配合使用)。 | pthread_attr_setschedparam() |
| scope | 设置线程竞争范围(PTHREAD_SCOPE_SYSTEM 或 PTHREAD_SCOPE_PROCESS)。 | pthread_attr_setscope() |
使用:
pthread_attr_t attr;
// 初始化属性对象
pthread_attr_init(&attr);
// 设置为分离线程
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
// 设置栈大小为 1MB
pthread_attr_setstacksize(&attr, 1024 * 1024);
线程回收:
int pthread_join(pthread_t thread,void **retval);
成功返回0,失败返回错误码,其中:
thread 是要回收的线程对象(!!!不是指针,不是传地址)。
调用此函数的时候,主程序会一直阻塞直至线程结束。
retval 是线程的返回值,一个二级指针,使用需要实现创建一个指针,然后把地址传进函数即可。
线程的结束:
void pthread_exit(void *retval);
结束当前的前程,不能用exit,因为所有线程共用一个空间,关闭会一起关。
retval的值可以被其他线程用pthread_join接收(如主线程)。
例子:
#include
#include
#include
#include
#include
char message[32] = "Hello World!";
void *thread_func(void *arg);
int main(){pthread_t a_thread;void *result;if(pthread_create(&a_thread,NULL,thread_func,NULL) != 0){perror("pthread_create");exit(-1);}//会一直阻塞直到a_thread结束pthread_join(a_thread,&result);printf("result is %s\n",(char *)result);printf("message is %s\n",message);return 0;
}
void *thread_func(void *arg){sleep(1);strcpy(message,"maked by thread");pthread_exit("Thank you for waiting for me");
}
编译的时候要
gcc -o test test.c -lpthread
因为pthread属于第三方库。
线程间通信:
我们前面都说了,同一个进程里的不同线程都是共用一个存储空间,所以他们直接的通信会简单很多——通过全局变量进行交换数据,但是,代码中间怎么知道那些是给自己的呢,哪些适合要自己写东西给别人,靠的就是同步和互斥的机制。(互斥后面再说)
同步:
同步不是我们经常说的一起做,而是多个任务按照约定的先后顺序,配合完成一件事情。
在1968年,Edsger Dijkstra 基于信息量的概念提出了一种同步机制,由信息量决定程序的继续运行还是阻塞。
信息量:
其代表一类资源,其值表示系统中该资源的数量。
信息量是一个受保护的变量,只能通过以下三种操作访问:
1、初始化
2、P操作(申请资源)
3、V操作(释放资源)
P(s)操作表示:
如果信号量s大于0,申请资源的任务继续,s--;
如果等于0,就阻塞。
V(s)操作表示:
s++;
如果有在等资源的任务,即P(s)阻塞的,会唤醒它,让它继续工作。
信号量Posix有两种:
1、无名信号量(基于内存的)
2、有名信号量(基于储存的)
前者用于线程间,后者线程进程都可。
信号量的库函数:
其库函数在头文件<semaphore.h>中
信号量初始化:
int sem_init(sem_t *sem,int pshared,unsigned int val);
成功返回0,失败返回EOF,其中:
sem_t是库中定义的数据,sem是信号量对象。
pshared 0表示在线程间 , 1表示在进程间。
val是信号量的初值。
信号量的操作:
int sem_wait(sem_t *sem); p操作
int sem_post(sem_t *sem); v操作
成功返回0,失败返回EOF。
记忆小技巧,post含p所以不是p操作。wait等待,借走了资源让别人等待。
例子:
#include
#include
#include
#include
#include
char message[32] = "Hello World!";
void *thread_func(void *arg);
int main(){pthread_t a_thread;void *result;if(pthread_create(&a_thread,NULL,thread_func,NULL) != 0){perror("pthread_create");exit(-1);}//会一直阻塞直到a_thread结束pthread_join(a_thread,&result);printf("result is %s\n",(char *)result);printf("message is %s\n",message);return 0;
}
void *thread_func(void *arg){sleep(1);strcpy(message,"maked by thread");pthread_exit("Thank you for waiting for me");
}
我们在启动程序的时候可以使用第一节课学的 ps aux -L | grep test查看这两个线程

这两个进制都处于等待状态。
代码解析:
定义了两个信号量r和w,主线程负责写入数据,创建的线程负责读出写入的长度。
w初始为1,主线程P操作使其减一,是为了防止无限制写入数据导致子进程还没读出就改变了缓冲区,在写入数据后V操作r使其加一。
处于阻塞的子线工作,但为了防止一直执行函数,P操作使r减一,读取后给wV操作,让主线程继续工作。
以此循环。
类比:
其实同步有点像小孩吃糖,a给b一颗糖,b吃完后给a一颗,a吃完再给b......没糖的话两个小孩都不会动,同个时间只能有一颗糖。