1、基本概念
1.1 线程的基本概念
- 线程就是程序的执行路线,即进程内部的控制序列,或者说是进程的子任务
- 一个进程可以同时拥有多个线程,即同时被系统调度的多条执行路线,但至少要有一个主线程
- 一个进程的所有线程都共享进程的代码区、数据区、BSS区、堆区、命令行参数和环境变量,唯有栈区是一个线程一个。
- 一个进程的所有线程共享系统内核中与进程有关的部分资源,如文件描述符表、信号处理函数、工作目录、各种ID等。
- 一个进程的每个线程都有一个独立的D、独立的寄存器上下文、独立的调度策略和优先级、独立的信号掩码、独立的errno以及线程私有数据。
1.2 线程调度
- 系统内核中专门负责线程调度的处理单元被称为调度器。调度器将所有处于就绪状态(没有阻塞于任何系统调用上)的线程排成一个队列,即所谓就绪队列
- 调度器从就绪队列中获取队首线程,为其分配一个时间片,并令处理机执行该线程,过了一段时间:
- 该线程的时间片耗尽,调度器立即中止该线程,并将其排到就绪队列的尾端,接着从队首获取下一个线程
- 该线程的时间片未耗尽,但需阻塞于某系统调用,比如等待/O或者睡眠。调度器会中止该线程,将其从就绪队列移至等待队列,直到其等待的条件满足后,在被移回就绪都队列
- 在低优先级线程执行期间,有高优级线程就绪,后者会抢占前者的时间片
- 若就绪队列为空,侧系统内核进入空闲状态,直至其非空
总结: - 进程是资源分配的最小单位
- 线程是系统调度的最小单位
2、POSIX线程
- 相关函数
1:pthread_create 创建新线程
// 头文件 pthread.h
int pthread_create(pthread_t* tid,pthread_attr_t const* attr,void*(* start_routine)(void*),void* arg)
- 功能:创建新线程
- 参数:
- tid:输出线程ID。pthread_t即unsigned long int
- attr:线程属性,NULL表示缺省属性。
- start_routine:线程过程函数指针,所指向的函数将在被创建的线程中执行。
- arg:传递给线程过程函数的参数。
- 返回值:成功返回O,失败返回错误码!
说明:
① pthread_create函数本身并不调用线程过程函数,而是在系统内核中开启独立的线程,并立即返回,在该线程中执行线程过程函数。
② pthread_create函数返回时,所指定的线程过裎函数可能尚未执行,也可能正在执行,甚至可能已经执行完毕。
③ 主线程和通过pthread_create函数创建的多个子线程,在时间上"同时"运行,如果不附加任何同步条件,则它们每一个执行步骤的先后顺序是完全无法确定的,这就叫做自由并发。
2:thread_proc 线程过程函数
void* thread_proc(void* arg){...}
- 定义线程过程函数,该函数会在所创建的线程中被执行,代表了线程的任务。
- 而main函数其实就是主线程的线程过程函欲。
- main函数一旦返回,主线程即告结束、主线程一旦结束,进程即告结束。进程一旦结束,其所有的子线程统统结束
- 案例
#include <stdio.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
// 线程过程函数
void* pthread_fun(void* arg){
printf("%lu线程:我是子线程,内容为%s\n",pthread_self(),(char*)arg);
return NULL;
}
int main(void){
printf("%lu线程:我是主线程\n",pthread_self());
pthread_t tid;// 用来输出子线程的ID
int error = pthread_create(&tid,NULL,pthread_fun,"hello");
if(error){
fprintf(stderr," pthread_create:%s\n",strerror(error));
return -1;
}
sleep(1);// 延时
return 0;
}
3、汇合线程
1:pthread_join 等待子线程终止
// 头文件 pthread.h
int pthread_join(pthread_t tid,void** retval);
- 功能:等待子线程终止,即其线程过程函数返回,并与之会合,同时回收该线程的资源
- 参数:
- tid:线程ID。
- retval:输出线程过程函数的返回值
- 返回值:成功返回0,失败返回错误码。
在父线程中调用pthread_join函数等待子线程的终止,并回收其资源。如果调用该函数时子线程已经终止,该函数会立即返回,如果调用该函数时子线程尚在运行中,该函数会阻塞,直至子线程终止。该函数在返回成功的同时通过其retval参数向调用者输出子线程线程过程函数的返回值。
#include <stdio.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#define PI 3.14
// 线程过程函数
void* area(void* arg){
double* r = (double*)arg;
double *p = malloc(sizeof(double));
*p = PI*(*r)*(*r);
printf("圆的面积是:%lg\n",*p);
return p;
}
int main(){
double r =10;
// 创建子线程,计算圆的面积
pthread_t tid;
int errno = pthread_create(&tid,NULL,area,&r);
if(errno){
fprintf(stderr,"pthread_create:%s",strerror(errno));
return -1;
}
double *p;
errno = pthread_join(tid,(void**)&p);
if(errno){
fprintf(stderr,"pthread_join:%s",strerror(errno));
}
printf("圆的面积是:%lg\n",*p);
free(p);
return 0;
}
4、分离线程
- 一个线程在默认情况下都是可汇合线程,这样的线程在终止以后其资源会被保留,其中含有线程过程函数的返回值及线程ID等信息,父线程可以通过pthread_join函数获得线程过程函数的返回值,同时释放这些资源。
- 如栗在创建线程以后,对其调用pthread_detach函数并返回成功,该线程即成为分离线程,这样的线程终止以后其资源会被系统自动回收,不需要也无法通过pthread_join函数汇合它。
// 头文件 pthread.h
int pthread_detach(pthread_t tid);
- 功能:使指定的线程进入分离状态
- 参数:
- tid线程的D
- 返回值:成功返回0,失败返回错误码。
案例
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
// 线程过程函数
void* pthread_fun(void* arg){
for(int i=0;i<100;i++){
putchar('-');
usleep(50000);
}
return NULL;
}
int main(){
setbuf(stdout,NULL);
pthread_t tid;
pthread_create(&tid,NULL,pthread_fun,NULL);
pthread_detach(tid);
for(int i=0;i<100;i++){
putchar('+');
usleep(100000);
}
return 0;
}
5、线程ID
pthread_t类型的线程ID是POSIX线程库内部维护的线程的唯一标识,通常表现为一个很大的整数,跨平台,可移植。
// 头文件 pthread.h
pthread_t pthread_self(void); //返回调用线程的(POSIX线程库的)TID。
syscall(SYS_gettid)函数返回是一个long int类型整数,是系统内核产生线程唯一标识。一个进程的PID其实就是它的主线程的TID。
// 头文件 sys/syscall.h
long int syscall(SYS_gettid);//返回调用线程的(系统内核的)TID
案例
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/syscall.h>
// 线程过程函数
void* pthread_fun(void* arg){
printf("子线程:进程PID %d\n",getpid());
printf("子线程:POSIX TID %lu\n",pthread_self());
printf("子线程:内核 TID %lu\n",syscall(SYS_gettid));
return NULL;
}
int main(){
printf("主线程:进程PID %d\n",getpid());
printf("主线程:POSIX TID %lu\n",pthread_self());
printf("主线程:内核 TID %lu\n",syscall(SYS_gettid));
pthread_t tid;
pthread_create(&tid,NULL,pthread_fun,NULL);
pthread_join(tid,NULL);
return 0;
}
pthread_t类型在不同系统会被实现为不同的数据类型,甚至可能会使用结构体。因此判断两个线程ID是否相等或不相等,最好不要使用"==“或”!="运算符,因为这些关系运算符只能用于C语言内置的简单类型,而对于结构类型的数据不适用。
// 头文件 pthread.h
int pthread_equal(pthread_t t1,pthread_t t2);// 若两个参数所表示的TID相等返回非零,否则返回0。
6、并发冲突
- 案例
#include <stdio.h>
#include <pthread.h>
int g_cn = 0;
void* pthread_fun(void* arg){
for(int i=0;i<1000;i++){
g_cn++;
}
return NULL;
}
int main(){
pthread_t t1,t2;
pthread_create(&t1,NULL,pthread_fun,NULL);
pthread_create(&t2,NULL,pthread_fun,NULL);
pthread_join(t1,NULL);
pthread_join(t2,NULL);
printf("%d\n",g_cn);
return 0;
}
- 并发冲突问题
任何时候只允许一个线程持有共享数据,其它线程必须阻塞于调度队列之外,直到数据持有者不再持有该数据为止 - 资源竞争问题
任何时候只允许部分线程拥有有限的资源,其它线程必须阻塞于调度队列之外,直到资源拥有者主动释放其所拥有的资源为止 - 条件等待问题
当某些条件一时无法满足时,一些线程必须阻塞于调度队列之外,直到令该条件满足的线程用信号唤醒它们为止 - 线程同步
某些情况下,需要在线程之间建立起某种停等机制,即一或多个线程有时必须停下来,等待另外一或多个线程执行完一个特定的步骤以后才能继续执行,这就叫同步
7、互斥锁
- 线程间可以通过互斥锁解决资源竞争的问题。
- 任何时候都只能一个线程持有互斥锁,即加锁成功,在其持有该互斥锁的过程中,其它线程对该锁的加锁动作都会引发阻塞,只有当持有互斥锁的线程主动解锁,那些在加锁动作上阻塞的线程中的一个采用恢复运行并加锁成功。
- pthread_mutex_t表示互斥锁数据类型
相关函数
1:pthread_mutex_init 初始化互斥体
// 头文件 pthread.h
int pthread_mutex_init(pthread_mutex_t* mutex,pthread_mutexattr_t const* attr);
- 功能:初始化互斥体
- 参数:
- mutex:互斥体
- attr:互斥体属性
- 返回值:成功返回0,失败返回错误码。
- 也可以静态方式初始化互斥锁:pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
2:pthread_mutex_destroy 销毁互斥体
// 头文件 pthread.h
int pthread_mutex_destroy(pthread_mutex_t* mutex);
- 功能:销毁互斥体
- 参数:
- mutex:互斥体
- 返回值:成功返回0,失败返回错误码
3:pthread_mutex_lock 锁定互斥体
// 头文件 pthread.h
int pthread_mutex_lock (pthread_mutex_t* mutex);
- 功能:锁定互斥体
- 参数:mutex互斥体
- 成功:返回0,失败返回错误码
4:pthread_mutex_unlock 解锁互斥锁
// 头文件 pthread.h
int pthread_mutex_unlock(pthread_mutex_t* mutex);
- 功能:解锁互斥锁
- 参数:mutex互斥锁
- 成功:返回0,失败返回错误码
案例
#include <stdio.h>
#include <pthread.h>
int g_cn = 0;
// 互斥锁
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
void* pthread_fun(void* arg){
pthread_mutex_lock(&mutex);
for(int i=0;i<100000;i++){
g_cn++;
}
pthread_mutex_unlock(&mutex);
return NULL;
}
int main(){
pthread_t t1,t2;
pthread_create(&t1,NULL,pthread_fun,NULL);
pthread_create(&t2,NULL,pthread_fun,NULL);
pthread_join(t1,NULL);
pthread_join(t2,NULL);
printf("%d\n",g_cn);
pthread_mutex_destroy(&mutex);
return 0;
}
8、条件变量
- 一个线程在某种条件不满足的情况下,无法进行后续工作,这时它就可以睡入某个条件变量,这时会有其它线程为其创建条件,一旦条件满足可以唤醒,那些在相应条件变量中睡眠的线程继续运行。
- 通过pthread_cond_t类型来表示条件变量
相关函数
1:pthread_cond_init 初始化条件变量
// 头文件 pthread.h
int pthread_cond_init(pthread_cond_t* cond,const pthread_condattr_t* attr);
- 功能:初始化条件变量
- 参数:
- cond条件变量
- attr条件变量属性
- 返回值:成功返回0,失败返回错误码
- 也可以静态方式初始化条件变量:pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
2:pthread_cond_destroy 销毁条件变量
// 头文件 pthread.h
int pthread_cond_destroy(pthread_cond_t* cond)
- 功能:销毁条件变量
- 参数:cond条件变量
- 返回值:成功返回0,失败返回错误码
3:pthread_cond_wait 睡入条件变量
// 头文件 pthread.h
int pthread_cond_wait(pthread_cond_t* cond,pthread_mutex_t* mutex);
- 功能:睡入条件变量
- 参数:
- cond 条件变量
- mutex 互斥锁
- 返回值:成功返回O,失败返回错误码
// 这是函数会让当前线程睡眠,并解开mutex锁,当条件满足时,线程睡醒,会为当前线程加上mutex锁
4:pthread_cond_signal 唤醒条件变量
// 头文件 pthread.h
int pthread_cond_signal(pthread_cond_t* cond);
- 功能:唤醒在条件变量中睡眠的一个线程
- 参数:cond条件变量
- 返回值:成功返回O,失败返回错误码
- 一个线程调用pthread_cond_waiti函数进入阻塞状态,直到条件变量cond收到信号为止,阻塞期间互斥锁mutex会被释放。另一个线程通过pthread_cond_signal函数向条件变量cond发送信号,唤醒在其中睡眠的一个线程,该线程即从pthread_cond_wait函数中返回,同时重新获得互斥锁mutex。
案例
//通过条件变量解决生产者和消费者问题
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
char g_storage[10];// 仓库
int g_stock=0;// 库存量
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;// 互斥锁
pthread_cond_t pcond = PTHREAD_COND_INITIALIZER;// 生产者条件变量
pthread_cond_t ccond = PTHREAD_COND_INITIALIZER;// 消费者条件变量
// 显示生产和消费过程
void show(char* who,char* op,char prod){
printf("%s:",who);
for(int i=0;i<g_stock;i++){
printf("%c",g_storage[i]);
}
printf("%s%c\n",op,prod);
}
// 生产这线程
void* producer(void* arg){
char* who = (char*)arg;
for(;;){
// 加锁
pthread_mutex_lock(&mutex);
// 生产
// 判满
if(g_stock == sizeof(g_storage)){
printf("%s:满仓\n",who);
// 睡入
pthread_cond_wait(&pcond,&mutex);
}
char prod='A'+rand()%26;
show(who,"<---",prod);
g_storage[g_stock++]=prod;
// 唤费者
pthread_cond_signal(&ccond);
// 解锁
pthread_mutex_unlock(&mutex);
usleep((rand()%100)*1000);
}
return NULL;
}
// 消费者线程
void* consumer(void* arg){
char* who = (char*)arg;
for(;;){
// 加锁
pthread_mutex_lock(&mutex);
// 消费
// 判空
if(g_stock ==0){
printf("%s:空仓\n",who);
pthread_cond_wait(&ccond,&mutex);
}
char prod = g_storage[--g_stock];
show(who,"--->",prod);
// 唤醒生产者
pthread_cond_signal(&pcond);
// 解锁
pthread_mutex_unlock(&mutex);
usleep((rand()%100)*1000);
}
return NULL;
}
int main(){
srand(getpid());// 随机数种种子
pthread_t t1,t2;
pthread_create(&t1,NULL,producer,"生产者");
pthread_create(&t2,NULL,consumer,"消费者");
getchar();
return 0;
}