文章目录
1. 线程的概念
1.1 基本概念
线程是进程内部的一条执行序列(执行流),线程不能单独存在,它是进程里面的一部分,一个进程至少有一条线程,该条线程称之为主线程(main方法代表的执行序列)
对于Linux系统而言,线程就是一个轻量级的进程
执行序列:一组有序指令的集合
函数线程:可以通过线程库创建其他的线程(给线程指定一个它要执行的函数),将创建的线程称之为函数线程
单线程与多线程示意图如下:(蓝色的框代表进程,竖线代表该进程里的线程)
1.2 线程的实现方式
在操作系统中,线程的实现有以下三种方式:
内核级线程:线程的实现(线程的创建、调度、销毁)由操作系统内核来完成
优点:用户程序比较简单,内核复杂一些,所有的工作都交给内核来完成
缺点:只要涉及到线程的切换,就要从用户空间进入到内核空间,增大了时间的开销
用户级线程:操作系统内核并不支持多线程,线程的实现都是在用户态,即用户态里是多线程,但在内核态是单线程,如下图左1
优点:操作系统内核简单一些
缺点:用户程序比较复杂,程序员要自己控制线程的切换;如果用户态一条线程发生阻塞,操作系统会认为整个进程都阻塞
组合(混合)级线程:结合以上两种方式来实现,即一部分在内核态实现,一部分在用户态实现,如下图最右:
1.3 Linux系统实现多线程的方式
Linux 实现线程的机制非常独特。从内核的角度来说,它并没有线程这个概念。Linux 把所有的线程都当做进程来实现。内核并没有准备特别的调度算法或是定义特别的数据结构来表示线程。相反,线程仅仅被视为一个与其他进程共享某些资源进程。每个线程都拥有唯一隶属于自己的task_struct(PCB),所以在内核中,它看起来就像是一个普通的进程(只是线程和其他一些进程共享某些资源,如地址空间)
1.4 线程和进程的区别
① 进程是系统资源分配的最小单位,线程是CPU调度的最小单位
② 进程的切换开销大,线程的切换开销较小,线程间的切换效率相比于进程间的要高
③ 进程的创建消耗资源大,线程的创建相对较小,所以创建线程的效率相对高一些
④ 进程都是相互独立的,而一个进程内的线程共享很多资源(如地址空间)
⑤ 因为进程间是相互独立的,所以进程间的通信需要一些特殊的技术(管道,信号量,共享内存,消息队列),而对于一个进程中的线程之间来说,由于共享了很多资源,线程间的通信就比较简单(线程就要考虑安全问题)
2. 线程库的使用
2.0 头文件
#include <pthread.h>
2.1 创建线程
int pthread_create(pthread_t *id, pthread_attr_t, *attr, void*(*fun)(void*), void *arg)
id:传递一个pthread_t类型的变量的地址,用来获取创建成功后创建的线程的id(TID)
attr:传递线程的属性,默认使用NULL
fun:线程函数的地址
arg:传递给线程函数fun的参数
返回值:成功返回0,失败返回错误码
创建线程示例:
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
void* fun(void *);//声明一个线程函数
int main()
{
printf("main start\n");
pthread_t id;//用来保存创建的线程的id值
int res = pthread_create(&id, NULL, fun, NULL);//创建函数线程并且指定函数线程要执行的函数fun
assert(res == 0);
int i = 0;
for(;i<5;++i)
{
printf("main running\n");
sleep(1);
}
printf("main over\n");
exit(0);
}
void* fun(void *arg)//定义线程函数
{
printf("fun start\n");
int i = 0;
for(;i<3;++i)
{
printf("fun running\n");
sleep(1);
}
printf("fun over\n");
}
注意,编译的时候要链接线程库
-lpthread
gcc -o pthread pthread.c -lpthread
如果gcc编译代码时报"undefined reference to …"错误,是因为程序中调用了一些方法,但是没有链接该方法所在的库文件
执行结果:
根据执行结果可以得出:
- 创建线程并执行线程函数,和调用函数是完全不同的概念,并不是fun执行完毕main才接着执行,main与fun穿插着执行
- 主线程与函数线程是并发执行的
- 创建函数线程后,哪个线程先被执行是由操作系统的调度算法和机器环境决定的
2.2 给线程函数传参
值传递:将变量的值直接强转成void*类型进行传递,示例如下:
注意:因为线程函数接收的是一个void*类型的指针,只要是指针,32位系统上都是4个字节。值传递就只能传递小于等于4字节的值
地址传递:将变量(所有类型的变量)的地址强转成void*类型进行传递,就和在普通函数调用之间传递变量的地址类似。主线程和函数线程通过这个地址就可以共享地址所指向的空间,示例如下:
一个进程内的所有线程共享这个进程的物理地址空间
2.3 多线程下进程的4G虚拟地址空间
一个进程内的所有线程对于全局数据、静态数据、堆区空间都是共享的
所以线程之间传递数据很简单,但是随之带来的问题就是线程并发运行时无法保证线程安全
共享数据代码验证如下:
在函数线程里面对数据修改会影响到主线程
2.4 线程库的其他方法
结束线程
//结束线程
int pthread_exit(void *result);
pthread_exit使用示例:
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
void* fun(void *);//声明一个线程函数
int main()
{
printf("main start\n");
pthread_t id;//用来保存创建的线程的id值
int res = pthread_create(&id, NULL, fun, NULL);//创建函数线程并且指定函数线程要执行的函数fun
assert(res == 0);
int i = 0;
for(;i<3;++i)
{
printf("main running\n");
sleep(1);
}
printf("main over\n");
exit(0);
}
void* fun(void *arg)//定义线程函数
{
printf("fun start\n");
int i = 0;
for(;i<5;++i)
{
printf("fun running\n");
sleep(1);
}
printf("fun over\n");
}
如上代码中主线程for循环3次,函数线程for循环5次,则会导致主线程执行完毕直接结束掉整个进程,而函数线程还没有执行完毕,运行结果如下图:(fun本来要打印5次)主线程结束时用的是exit方法,exit会直接结束掉进程
要想让主线程退出,但是其他线程继续运行,就要用pthread_exit方法来结束主线程
main执行结束后,函数线程仍在继续执行
等待一个线程的结束
int pthread_join(pthread_t id, void **result);
//result用来获取等待的线程通过pthread_exit结束时设置的退出数据
一个线程只要知道另一个线程的id,就可以通过join方法来等待该线程的结束,join方法示例如下:
由运行结果可以看出:调用join方法的主线程会被阻塞,直到指定的fun函数线程结束
3. 线程同步
线程同步和进程同步一样,同步就是线程或者进程需要协同执行。(直接或者间接制约)
3.1 互斥锁
互斥锁只有两种状态,解锁状态和加锁状态,如果一个线程对已经处于加锁状态的互斥锁进行加锁操作,则加锁操作会阻塞,直到正在对互斥锁加锁状态线程进行解锁操作
互斥锁的接口
#include <pthread.h>
pthread_mutex_t mutex;//创建一个互斥锁mutex,pthread_mutex_t是互斥锁的类型
//锁的初始化, 互斥锁使用前要进行初始化
int pthread_mutex_init(pthread_mutex *mutex, pthread_mutexattr_t *attr);
//加锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex *mutex);//尝试加锁,不会阻塞
//解锁
int pthread_mutex_unlock(pthread_mutex *mutex);
//销毁互斥锁
int pthread_mutex_destriy(pthread_mutex_t *mutex);
示例:模拟两个线程竞争一个打印机,A线程使用打印机输出一个a,使用完成后输出一个a,这样我们的输出结果必须是成对出现
两个线程要操作同一个互斥锁:把互斥锁定义在全局中就可以
//mutex.c
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <time.h>
pthread_mutex_t mutex;//定义互斥锁
void *threadFun(void *arg)//函数线程,B线程
{
int i = 0;
for(;i<5;++i)
{
pthread_mutex_lock(&mutex);//加锁
printf("B");
fflush(stdout);
int n = rand()%3;
sleep(n);//模拟打印机正在执行
printf("B");
fflush(stdout);//打印机执行完毕
pthread_mutex_unlock(&mutex);//解锁
n = rand()%3;
sleep(n);
}
}
void threadMain() //主线程里执行的函数,主线程模拟A线程
{
int i = 0;
for(;i<5;++i)
{
pthread_mutex_lock(&mutex);//加锁
printf("A");
fflush(stdout);
int n = rand()%3;
sleep(n);//模拟打印机正在执行
printf("A");
fflush(stdout);//打印机执行完毕
pthread_mutex_unlock(&mutex);//解锁
n = rand()%3;
sleep(n);
}
}
int main()
{
srand((unsigned int)time(NULL));
pthread_mutex_init(&mutex, NULL);//互斥锁使用之前必须初始化
pthread_t id;
int res = pthread_create(&id, NULL, threadFun, NULL);//创建函数线程
assert(res == 0);
threadMain();
pthread_join(id,NULL);//等待函数线程结束
pthread_mutex_destroy(&mutex);//销毁互斥锁
exit(0);
}
执行结果如下:
3.2 信号量
线程级信号量和进程级信号量原理是相同的,使用接口不同。信号量是特殊的计数器,当值大于0时,记录临界资源个数,值等于0时,表示没有临界资源可用,这时对信号量执行P操作,则线程会被阻塞
信号量接口
#include <semaphore.h>
sem_t sem;//sem_t是线程级信号量的类型
//初始化信号量并且给定初值
int sem_init(sem_t *sem, int shared, int val);//shared代表是否在进程间共享,0表示不共享
//对信号量执行P操作
int sem_wait(sem_t *sem);
//对信号量执行V操作, 注意P操作是wait,V操作是post
int sem_post(sem_t *sem);
//销毁信号量
int sem_destroy(sem_t *sem);
信号量和互斥锁区别:(接口很像)
- 互斥锁只有两种状态,而信号量的值可以大于1
- 在一个线程中对互斥锁的加锁和解锁必须成对出现,而信号量PV操作可以在线程间交叉使用(整体是成对的)
3.3 读写锁
读写锁在互斥锁的基础上,允许一个更高的并行性。读写锁一共有三种状态:解锁状态,读加锁状态,写加锁状态
- 解锁状态:任何线程任何方式都可以加锁成功
- 读加锁状态:一个线程对锁执行读加锁,则可以成功返回;但是一个线程如果执行写加锁,则会被阻塞,直到所有读加锁的线程都执行了解锁操作,即所有读操作结束,才可写
- 写加锁状态:一个线程只要处于写加锁状态,所有的加锁操作都会被阻塞
读写锁的接口
#include <pthread.h>
pthread_rwlock_t rwlock;//pthread_rwlock_t是读写锁的类型
//初始化读写锁
int pthread_rwlock_init(pthread_rwlock_t *rwlock, pthread_rwlockattr_t *attr);
//读加锁
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
//写加锁
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
//解锁
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
//销毁
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
扩充:
自旋锁(Spin Locks):和互斥锁类似,只是当加锁操作被阻塞,阻塞的方式不同:互斥锁是通过将线程睡眠,而自旋锁则是通过忙等待的方式。自旋锁一般的适用场景是:锁被其他线程短期持有(很快会被释放),而且等待该锁的线程不希望在阻塞期间被取消调度(被取消调度会带来一些开销),这时使用自旋锁,避免了等待该锁的线程被睡眠,避免了调度切换的开销自旋锁通常被用作实现其他类型的锁的低级原语。当一个线程在自旋等待一个锁的时候,CPU不能做其他任何事了,这就浪费了CPU的资源。这就是为什么要求自旋锁只能被短期保持的原因
参考博客:https://www.cnblogs.com/nufangrensheng/p/3521654.html
悲观锁:任何情况下都去加锁,(悲观地认为只要不加锁就会出问题),执行效率就低了
乐观锁:不加锁,(乐观地认为不加锁也没问题),只是监视一下
3.4 条件变量
条件变量的作用是用于多线程之间关于共享数据状态变化的通信。当一个动作需要另外一个动作完成时才能进行,即:当一个线程的行为依赖于另外一个线程对共享数据状态的改变时,这时候就可以使用条件变量。
与互斥锁不同,条件变量是用来等待而不是用来上锁的。条件变量用来自动阻塞一个线程,直到某特殊情况发生为止。通常条件变量和互斥锁同时使用。它给多个线程提供了一个会和的场所。条件变量与互斥量一起使用时,允许线程以无竞争的方式等待特定的条件发生
条件变量的接口
#include <pthread.h>
pthread_cond_t cond;//pthread_cond_t是条件变量的类型
//初始化条件变量
int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *attr);
//等待条件的发生
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);//与互斥锁一起使用,传递的是加锁状态的锁(内核中会对mutex解锁,内核退出时又会对mutex加锁), 使用mutex目的是:以允许线程以无竞争的方式等待特定的条件发生
//唤醒一个线程
int pthread_cond_signal(pthread_cond_t *cond);
//唤醒所有等待的线程
int pthread_cond_broadcast(pthread_cond_t *cond);
//销毁条件变量
int pthread_cond_destroy(pthread_cond_t *cond);
代码示例:
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
pthread_mutex_t mutex;//互斥锁
pthread_cond_t cond;//条件变量
char buff[128] = {0};
void* fun(void * arg)
{
int flag = (int)arg;
while(1)
{
pthread_mutex_lock(&mutex);
pthread_cond_wait(&cond,&mutex);//mutex是一个互斥锁,以互斥方式将当前线程添加到等待条件变量的队列中
pthread_mutex_unlock(&mutex);
if(strncmp(buff, "end", 3)==0)
{
break;
}
printf("fun%d: %s\n",flag, buff);
memset(buff,0,128);
}
printf("fun end!\n");
}
int main()
{
//初始化
pthread_mutex_init(&mutex, NULL);
pthread_cond_init(&cond, NULL);
//创建线程
pthread_t id1,id2;
pthread_create(&id1, NULL, fun, (void*)1);
pthread_create(&id2, NULL, fun, (void*)2);
while(1)
{
printf("input: ");
fflush(stdout);
fgets(buff, 127, stdin);
if(strncmp(buff,"end",3)==0)
{
pthread_mutex_lock(&mutex);
pthread_cond_broadcast(&cond);//唤醒全部线程
pthread_mutex_unlock(&mutex);
break;
}
else//输入的不是end
{
pthread_mutex_lock(&mutex);
pthread_cond_signal(&cond);//唤醒一个线程
pthread_mutex_unlock(&mutex);
}
}
pthread_join(id1, NULL);
pthread_join(id2, NULL);
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);
exit(0);
}
执行结果:
刚开始两个函数线程都在等待队列中,当用户输入数据时,主线程进入if else去唤醒函数线程,函数线程才执行。最后输入end,执行的是pthread_cond_broadcast,唤醒全部线程,所以两个函数线程会都打印fun end!