多线程1
线程概念
一个进程当中一定存在一个主线程,执行main函数的线程就称为主线程
其他线程都被称之为工作线程
进程本质上是线程组,换句话说,线程组被称之为进程,线程也可以被称为轻量级进程(LWP),因为在操作系统内核当中不存在线程的概念
pid:轻量级进程id,也被称之为线程id
tgid:轻量级进程组id,也被称之为进程id
在一个进程中,不管这个进程有多少线程,在所有线程的PCB中,tgid都是相同的
主线程(执行main函数的LWP)的pid和tgid相等
除了主线程,工作线程的pid都是不一样的,可以用pid去区分到底是哪一个线程
线程的共享与独有
独有:
在共享去当中有自己的调用堆栈、寄存器、线程ID、errmo、信号屏蔽字、调度优先级(PR)
共享:
文件描述表(fd_array[xxx])、当前进程工作目录、用户id和用户组id、信号处理方式
线程的优缺点:
前提:
并行:每一个执行六在同一时间都拥有一个CPU,同时进行运算
并发:多个执行流在同一时刻只能由一个执行流拥有CPU进行运算
优点:
1、一个进程当中多个执行流可以并行的执行代码,就可以提高程序的运行效率
2、进程切换要比线程切换操作系统付出的代价大
3、线程占用的资源要比进程少很多
4、可并行的运行
缺点:
1、当一个进程当中的线程数量远远超过CPU数量的时候,有可能线程切换的开销会影响程序运行效率
总结:程序当中的线程数量不是越多越好
2、健壮性,也就是代码的鲁棒性,多线程的状态下代码并没有单线程那么健壮,原因在于一个线程的崩溃就会影响其他线程
3、缺乏访问控制
4、编程的难度高
线程是操作系统的调度的基本单位
进程是操作系统资源分配的基本单位
进程与线程的对比:
1、进程的健壮性比线程好
2、多线程要比多进程耗费资源小,而且切换快,程序运行效率高
线程控制
线程创建
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine)(void *), void *arg)
pthread_t:线程的标识符,本质上是线程在共享区独有空间的首地址
thread:是一个出参,该值是由pthread_create函数赋值的
pthread_attr_t:创建线程的属性,一般情况都指定为NULL,采用默认属性
void *(*start_routine)(void *):函数指针,接收一个返回值为void*,参数为void*的函数地址,本质上就是线程入口函数,即线程创建出来后执行的第一个函数
void *arg:给线程入口函数传递的参数,由于参数的类型是void*,所以给了程序无限的传递参数方式
返回值:
失败 < 0
成功:==0
使用此函数必须包含一个名为 “ pthread.h ” 的头文件
创建一个Makefile文件
在编译多线程程序时一定要链接 libpthread.so线程库,去头(lib)去尾(.so)后用 -l 链接
即 -lpthread
↑ 线程标识符
↑ LWP代表轻量级进程,后边的数字是轻量级进程id,也叫线程id
怎么区分哪个是主线程?
看哪个调用了main函数
这里看到Thread 1 调用了main函数,所以Thread 1 是主线程,同理Thread 2 便是工作线程
主线程的线程id和进程id是一样的,而工作线程不是
看堆栈的原则:从下往上去看
top -H -p [pid]
查看线程占用情况
线程终止
void pthread_exut(void *retval);
作用:谁调用,谁退出
retval:线程结束时传递给等待线程的参数
等待线程相当于进程中的父进程
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <pthread.h>
4
5 void* MyThreadStart(void* arg)
6 {
7 int* i = (int*)arg;
8 i++;
9 //while(1)
10 {
11 printf("i am MyThreadStart, i = %d\n", *i);
12 sleep(1);
13 }
14
15 pthread_exit(NULL);
16 printf("pthread_exit fail\n");
17
18 }
19
20 int main()//主线程
21 {
22 pthread_t tid;
23 int i = 1;
24 int ret = pthread_create(&tid, NULL, MyThreadStart, (void*)&i);
25 if (ret<0)
26 {
27 perror("pthread_create");
28 return 0;
29 }
30 while(1)
31 {
32 printf("i am main thread\n");
33 sleep(1);
34 }
35 return 0;
36 }
线程的入口函数代码执行完毕之后,线程就退出了
int pthread_cancel(pthread_t thread);
参数:thread:被终止的线程的标识符
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <pthread.h>
4
5 #define THREAD_NUM 4
6
7 struct ThreadId
8 {
9 int thread_id_;
10 };
11
12 void* MyThreadStart(void* arg)
13 {
14 struct ThreadId* ti = (struct ThreadId*)arg;
15 while(1)
16 {
17 printf("i am MyThreadStart, i = %d\n", ti ->thread_id_);
18 sleep(1);
19 }
20
21 delete ti;
22 }
23
24 int main()//主线程
25 {
26 pthread_t tid[THREAD_NUM];
27
28 for(int i = 0; i < THREAD_NUM; i++)
29 {
30 struct ThreadId* ti = new ThreadId();
31 ti->thread_id_= i;
32
33 int ret = pthread_create(&tid[i], NULL, MyThreadStart, (void*)ti);
34 if (ret<0)
35 {
36 perror("pthread_create");
37 return 0;
38 }
39 }
40
41 sleep(10);
42 pthread_cancel(tid[2]);//退出3号线程,执行到这行时应该只剩下4个线程
43
44 while(1)
45 {
46 printf("i am main thread\n");
47 sleep(1);
48 }
49 return 0;
50 }
获取当前自己线程的标识符:
pthread_t pthread_self(void);
可以退出别人,也可以退出自己
注意:如果主线程的代码当中调用pthread_cancel(pthread_self());,则主线程的状态变成僵尸状态,工作线程正常。整个线程并没有退出。
线程等待
原因:由于线程的默认属性为joinable属性,当线程退出的时候,其资源不远被操作系统回收,需要其他线程来进行线程等待,继续回收,否则就会造成内存泄漏
接口
int pthread_join(pthread_t thread,void **retval);
thread:需要等待的线程标识符
retval:线程退出的时候的返回值
①线程入口函数退出的时候,retval就是线程入口函数的返回值
②pthread_exit(void* retval):retval就pthread_exit函数的参数值
③pthread_cancel:retval的值是一个常数 PTHREAD_CANCELED
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <pthread.h>
4
5 #define THREAD_NUM 4
6
7 struct ThreadId
8 {
9 int thread_id_;
10 };
11
12 void* MyThreadStart(void* arg)
13 {
14 struct ThreadId* ti = (struct ThreadId*)arg;
15 //while(1)
16 {
17 printf("i am MyThreadStart, i = %d\n", ti ->thread_id_);
18 sleep(1);
19 }
20
21 delete ti;
22 return NULL;
23 }
24
25 int main()//主线程
26 {
27 pthread_t tid[THREAD_NUM];
28
29 for(int i = 0; i < THREAD_NUM; i++)
30 {
31 struct ThreadId* ti = new ThreadId();
32 ti->thread_id_= i;
33
34 int ret = pthread_create(&tid[i], NULL, MyThreadStart, (void*)ti);
35 if (ret<0)
36 {
37 perror("pthread_create");
38 return 0;
39 }
40 }
41
42 for(int i = 0; i < THREAD_NUM; i++)
43 {
44 pthread_join(tid[i], NULL);
45 }
46
47 while(1)
48 {
49 printf("i am main thread\n");
50 sleep(1);
51 }
52 return 0;
53 }
此时会打印“i am main thread”是因为线程等待到了,若是无法等待到呢
15 while(1)
调用 pthread_join 进行等待的执行流如果害没有等待到退出 线程,则当前调用 pthread_jon 函数的执行流就会阻塞
线程分离
一个线程的属性如果从joinable属性变成detach属性,则当前这个线程在退出的时候,不需要其他线程回收资源,操作系统会自己回收资源
接口
int pthread_detach(pthread_t thread);
参数:
thread:待要分离的线程的标识符
线程安全
多个执行流访问临界资源,不会导致程序产生二义性
执行流:理解为线程
访问:指的是对临界资源进行操作
临界资源:指的是多个线程都可以访问到的资源
例如:全局变量、某个结构体变量,某个类的实例化指针
临界区:代码操作临界资源的代码区域称之为临界区
二义性:结果会有多个
如何给面试官描述线程不安全的现象?
原理(对正常变量进行操作的原理)
如果想对一个变量i进行++操作,首先需要线程将数据传递到寄存器中,随后再由寄存器传入到CPU中进行++操作,在CPU++操作完后再回写到
寄存器中,再由寄存器回写到内存当中
1、假设场景,有几个线程,每个线程都想做什么事情
假设有两个线程AB,线程AB都想对全局变量i进行++
2、分线程去描述,体现出来:线程切换(上下文信息,程序计数器)
线程A从内存当中把全局变量i读到CPU的寄存器当中,i 原始的值为10,此时线程A的时间片到了,线程A被切换出来,线程A中的上下文信息保
存的是寄存器当中的值,程序计数器中保存的是线程A下一步即将进行的+指令,此时线程B如果获取CPU资源或时间片,线程B也想对当前的全局
变量i进行++,此时B从内存当中把i的值10读到了寄存器当中,顺利的进行了++,并且回写到了寄存器当中再回写到了内存当中,此时内存中全
局变量 i的值就从10变成了11,此时线程B的时间片到了,让出了CPU资源,当线程A再次切换回来时,A想对全局变量i继续进行++,但是A此时
i的值是从上下文信息当中获取,而上下文信息当中的值是10,进行++后回写到内存当中,内存当中的值也是11
3、总结
线程A对全局变量i加了一次,线程B也对全局变量i加了一次,但最终i的值变成了11,并不是12,由此就产生了线程不安全的情况,即产生了多
个线程使用临界资源时有可能产生的二义性
线程不安全的情况(模拟黄牛抢票)
1 #include <stdio.h>
3 #include <unistd.h>
4
5 #define THREAD_NUM 4 //设定黄牛的数量为4
6
7 int g_tickets = 10000;//设定总票数为10000张
8
W> 9 void* MyThreadStart(void* arg)
10 {
11 while(1)//循环抢票
12 {
13 if(g_tickets > 0)
14 {
15 //票的数量大于0代表能抢
W> 16 printf("i have %d, i am %p\n", g_tickets, pthread_self());
17 //打印线程标识符(黄牛的代号)以及黄牛拿到了第几张票
18 g_tickets--;
19 }
20 else//没抢到票
21 {
22 pthread_exit(NULL);//直接退出
23 }
24 }
25 return NULL;
26 }
27
28 int main()
29 {
30 pthread_t tid[THREAD_NUM];
31 for(int i = 0; i < THREAD_NUM; i++)
32 {
33 int ret = pthread_create(&tid[i], NULL, MyThreadStart, NULL);
34 if(ret < 0)
35 {
36 perror("pthread_create fail\n");
37 return 0;
38 }
39 }
40
41 for(int i = 0; i < THREAD_NUM; i++)
42 {
43 pthread_join(tid[i], NULL);
44 //主线程在创建完成工作线程之后调用pthread_join进行等待
45 //等待两个工作线程将票抢完,工作线程退出后主线程等待到了则pthread_join结束
46 }
47
48 printf("pthread_join end ...\n");
49 return 0;
50 }
运行一下
好家伙,这直接出现了三个不同的黄牛抢到了同样的9789号票的情况,而且很有可能第四号黄牛也抢到了同样的票,不过记录被顶掉了,所以看不到
互斥
互斥锁的原理
互斥锁的底层是一个互斥量,而互斥量的本质是一个计数器
计数器的取值只有两种,一种是1,一种是0
1:表示当前临界资源可以被访问
0:表示当前临界资源不可以被访问
获取/释放互斥锁的逻辑:
1、调用加锁接口,加锁接口内部判断计数器的值是否为1
如果为1,则能访问,当加锁成功偶,会将计数器的值从1变成0
如果为0,则不能访问
2、调用解锁逻辑,计数器的值从0变成1,表示资源可用
互斥锁的接口
初始化互斥锁变量
互斥锁的类型:pthread_mutex_t
静态初始化
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
PTHREAD_MUTEX_INITIALIZER 是一个宏定义,包含了多个值
动态初始化
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
mutex:该参数为出参,由调用者传递一个互斥锁变量的地址,由 pthread_mutex_init 函数进行初始化
attr:互斥锁的属性信息,一般置为NULL,采用默认属性
注意:动态初始化互斥锁变量的情况需要动态销毁互斥锁,否则就会造成内存泄漏
加锁
阻塞加锁接口
int pthread_mutex_lock(pthread_mutex_t *mutex);
参数为传递一个互斥锁变量的地址
注意:
如果互斥锁变量当中的计数器的值为1,调用该接口,加锁成功,该接口调用完毕,函数返回
如果互斥锁变量当中的计数器的值为0,调用该接口,调用该接口的执行流阻塞
非阻塞加锁接口
int pthread_mutex_trylock(pthread_mutex_t *mutex);
注意:不管有没有加锁成功,都会返回,所以需要对加锁进行判断是否加锁成功,如果成功则操作临界资源,失败则需要循环获取互斥锁直到拿到
带有超时时间的加锁接口
int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex, const struct timespec *restrict abs_timeout);
struct timespec:
typedef long time_t
#ifndef _TIMESPEC
#define _TIMESPEC
struct timespec {
time_t tv_sec;//seconds
long tv_nsec;//nanoseconds
};
#endif
struct timespec 有两个成员,一个是秒,一个是纳秒,即最高精确度是纳秒
超时时间内,如果还没有获取到互斥锁,则返回;
超时时间内,如果获取了互斥锁直接返回
解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);
注意:不管是阻塞加锁/非阻塞加锁/timelock加锁成功互斥锁,都可以用该接口进行解锁
销毁接口
int pthread_mutex_destory(pthread_mutex_t *mutex);
释放动态开辟的互斥锁的资源
互斥锁的使用(结合黄牛抢票)
1 #include <stdio.h>
2 #include <pthread.h>
3 #include <unistd.h>
4
5 #define THREAD_NUM 2
6
7 pthread_mutex_t my_lock;//定义一个全局变量
8
9 int g_tickets = 100;
10
W> 11 void* MyThreadStart(void* arg)
12 {
13 while(1)
14 {
15
16 pthread_mutex_lock(&my_lock);//加锁
17
18 if(g_tickets > 0)
19 {
W> 20 printf("i have %d, i am %p\n", g_tickets, pthread_self());
21 g_tickets--;
22 }
23 else
24 {
25 pthread_mutex_unlock(&my_lock);//解锁1
26 pthread_exit(NULL);
27 }
28 pthread_mutex_unlock(&my_lock);//解锁2
29 }
30 return NULL;
31 }
32
33 int main()
34 {
35
36 pthread_mutex_init(&my_lock, NULL);//初始化
37
38 pthread_t tid[THREAD_NUM];
39 for(int i = 0; i < THREAD_NUM; i++)
40 {
41 int ret = pthread_create(&tid[i], NULL, MyThreadStart, NULL);
42 if(ret < 0)
43 {
44 perror("pthread_create fail\n");
45 return 0;
46 }
47 }
48
49 for(int i = 0; i < THREAD_NUM; i++)
50 {
51 pthread_join(tid[i], NULL);
52 }
53
54 pthread_mutex_destroy(&my_lock);//释放互斥锁资源
55
56 printf("pthread_join end ...\n");
57 return 0;
58 }
什么时候要初始化互斥锁?
在创建工作线程之前,进行初始化互斥锁
什么时候进行加锁
在执行流访问临界资源前必须加锁操作
注意:如果一个执行流加锁成功后再去获取互斥锁,该执行流也会阻塞
如果只加锁运行程序会发生什么?
哦豁,卡死了
使用 gdb attach [pid] 来将gdb附加到进程上
查看当前线程所有线程调用堆栈 back trace ==> bt
thread apply all bt
所有线程的信息就都显示出来了
再回头找刚才抢到了票的黄牛
看来是线程3拿到了票
跳转到某个线程的堆栈
t [线程编号]
其他的都是库里边的玩意,唯一一个能被我们掌握的也就只有3号了
__owner代表当前互斥锁被线程编号27373拿着,也就是线程3
说明:线程3第一次加锁成功打印后第二次再去加锁时卡在pthread_lock接口中,此时我们再打印互斥锁变量时发现它被自己拿着
加锁之后一定要记得解锁,否则就会导致死锁
什么时候解锁?
在执行流所有可能退出的地方进行解锁
什么时候释放互斥锁资源?
在所有使用该互斥锁的线程退出之后就可以释放该互斥锁了