1 多线程编写的漏洞
上一篇博客中写介绍了多线程,并且写了一个多线程代码,如下:
1 #include <stdio.h>
2 #include <string.h>
3 #include <errno.h>
4 #include <unistd.h>
5 #include <pthread.h>
6 #include <stdlib.h>
7
8 void *thread_worker1(void *args);
9 void *thread_worker2(void *args);
10
11 int main(int argc, char **argv)
12 {
13 int shared_var = 1000; //设置一个参数用于传给子线程
14 pthread_t tid; //返回的线程id
15 pthread_attr_t thread_attr; //用于定义线程属性的结构体
16
17 if( pthread_attr_init(&thread_attr) ) //初始化线程对象的属性
18 {
19 printf("pthread_attr_init() failure: %s\n", strerror(errno));
20 return -1;
21 }
22
23 if( pthread_attr_setstacksize(&thread_attr, 120*1024) ) //设置栈的大小
24 {
25 printf("pthread_attr_setstacksize() failure: %s\n", strerror(errno));
26 return -2;
27 }
28
29 if( pthread_attr_setdetachstate(&thread_attr, PTHREAD_CREATE_DETACHED) ) //设置分离状态为可分离
30 {
31 printf("pthread_attr_setdetachstate() failure: %s\n", strerror(errno));
32 return -3;
33 }
34 //在创建两个线程时,我们都通过第四个参数将主线程栈中的 shared_var 变量地址传给了子线程,因为所有线程都是在同一进程空间中运行,而只是子线程有自己独立的栈空间,所以这时所有子线程都可以访问主线程空间的shared_var变量。
35 pthread_create(&tid, &thread_attr, thread_worker1, &shared_var); //创建线程1
36 printf("Thread worker1 tid[%ld] created successfully!\n", tid);
37
38 pthread_create(&tid, NULL, thread_worker2, &shared_var); //创建线程2
39 printf("Thread worker2 tid[%ld] create successfully!\n", tid);
40
41 pthread_attr_destroy(&thread_attr); //摧毁释放线程属性结构体
42
43 /*Wait until thread worker2 exit */
44 pthread_join(tid, NULL); //阻塞主线程,等线程2退出
45
46 while(1)
47 {
48 printf("Main/Control thread shared_var: %d\n", shared_var);
49 sleep(10);
50 }
51 }
52
53 void *thread_worker1(void *args)
54 {
55 int *ptr = (int *)args;
56
57 if( !args )
58 {
59 printf("%s() get invalid arguments\n", __FUNCTION__);
60 pthread_exit(NULL);
61 }
62
63 printf("Thread worker 1 [%ld] start running...\n", pthread_self());
64
65 while(1)
66 {
67 printf("+++: %s before shared_var++: %d\n", __FUNCTION__, *ptr);
68 *ptr += 1;
69 sleep(2);
70 printf("+++: %s after sleep shared_var: %d\n", __FUNCTION__, *ptr);
71 }
72
73
74 printf("Thread worker 1 exit...\n");
75
76 return NULL;
77
78 }
79
80 void *thread_worker2(void *args)
81 {
82 int *ptr = (int *)args;
83
84 if( !args )
85 {
86 printf("%s() get invalid arguments\n", __FUNCTION__);
87 pthread_exit(NULL);
88 }
89
90 printf("Thread worker 2 [%ld] start running...\n", pthread_self());
91
92 while(1)
93 {
94 printf("---: %s before shared_var++: %d\n", __FUNCTION__, *ptr);
95 *ptr += 1;
96 sleep(2);
97 printf("---: %s after sleep shared_var: %d\n", __FUNCTION__, *ptr);
98 }
99
100 printf("Thread worker 2 exit...\n");
101
102 return NULL;
103
104 }
好像也没什么问题,我们来看一下运行结果
在这里我们发现,进程1(就是+++:)第一次运行的值为1000,那继续运行应该为1001。而我们看到的是,在打印结果里,进程2(也就是—:)在进程1运行之后对shared_var进行了处理变成了1001,并非进程1一直自加,进程1又接着对进程2的运行结果进行处理,变成了1002!!!
thread_worker1 在创建后首先开始运行,在开始自加之前值为初始值1000,然后让该值自加后休眠2秒后再打印该值发现不是1001而是1002了, 这是由于shared_var 这个变量会被两个子线程同时访问修改导致。如果一个资源会被不同的线程访问修改,那么我们把这个资源叫做临界资源,那么对于该资源访问修改相关的代码就叫做临界区。那么怎么解决多个线程之间共享同一个共享资源,是多线程编程需要考虑的一个问题
这就造成了我代码上的漏洞,我只想让两个子线程独立自加,这就要解决多个线程之间访问同一个共享资源,所以就找到了“锁”的用处!!!
2 解决漏洞方法——锁
(1)阻塞锁
多个线程同时调用同一个临界资源的时候,所有线程都被排队处理了。让线程进入阻塞状态进行等待,当获得相应的信号(唤醒,时间),才可以进入线程的准备就绪状态,准备就绪状态的所有线程,通过竞争,进入运行状态。
(2)非阻塞锁
多个线程同时调用一个临界资源的时候,当某一个线程最先获取到锁,这时其他线程判断没拿到锁,这时就直接返回,只有当最先获取到锁的线程释放,其他线程才能进来,在它释放之前其它线程都会获取失败。
(3)自旋锁
一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。获取锁的线程一直处于活跃状态,由于一直调用while循环,但是并没有执行任何有效的任务,使用这种锁会造成busy-waiting。
(4)互斥锁
试想一下,我们宿舍里只有一个洗手间, 那么多个人是怎么解决马桶共享的问题? 那就是“锁”的机制!在这里,马桶就是临界资源, 我们在进入洗手间(临界区)后,就首先上锁;然后用完马桶之后离开洗手间,把锁释放供给别人使用。如果有人想上厕所发现门上锁了,它有两种策略:1.在洗手间门口等(阻塞),2.暂时先离开等会再过来看(非阻塞)
现在我们把代码修改一下, 通过锁的机制解决资源共享的问题:
1 #include <stdio.h>
2 #include <string.h>
3 #include <errno.h>
4 #include <unistd.h>
5 #include <pthread.h>
6 #include <stdlib.h>
7
8 void *thread_worker1(void *args);
9 void *thread_worker2(void *args);
10
11 typedef struct worker_ctx_s //因为在创建线程给线程执行函数传参的时候只能传一个参数,而我们要传递共享的变量shared_var和它相应的互斥锁lock,所以在这里需要用结构体(worker_ctx_t, ctx: context)将它们封装在一块传进去。
12 {
13 int shared_var;
14 pthread_mutex_t lock;
15 }worker_ctx_t;
16
17 int main(int argc, char **argv)
18 {
19 worker_ctx_t worker_ctx; //使用work_ctx_t结构体类型定义了传给子线程的变量参数
20 pthread_t tid;
21 pthread_attr_t thread_attr;
22
23 worker_ctx.shared_var = 1000;
24 pthread_mutex_init(&worker_ctx.lock, NULL); //互斥锁在使用之前,需要先调用 pthread_mutex_init() 函数来初始化互斥锁;
25
26
27 if( pthread_attr_init(&thread_attr) )
28 {
29 printf("pthread_attr_init() failure: %s\n", strerror(errno));
30 return -1;
31 }
32
33 if( pthread_attr_setstacksize(&thread_attr, 120*1024) )
34 {
35 printf("pthread_attr_setstacksize() failure: %s\n", strerror(errno));
36 return -2;
37 }
38
39 if( pthread_attr_setdetachstate(&thread_attr, PTHREAD_CREATE_DETACHED) )
40 {
41 printf("pthread_attr_setdetachstate() failure: %s\n", strerror(errno));
42 return -3;
43 }
44
45 pthread_create(&tid, &thread_attr, thread_worker1, &worker_ctx);
46 printf("Thread worker1 tid[%ld] created successfully!\n", tid);
47
48 pthread_create(&tid, &thread_attr, thread_worker2, &worker_ctx); //在创建第二个线程时也设置了分离属性,这时主线程后面的while(1)循环就会执行了
49 printf("Thread worker2 tid[%ld] create successfully!\n", tid);
50
51
52 while(1)
53 {
54 printf("Main/Control thread shared_var: %d\n", worker_ctx.shared_var);
55 sleep(10);
56 }
57
58 pthread_mutex_destroy(&worker_ctx.lock); //互斥锁在使用完之后,我们应该调用pthread_mutex_destroy()将他摧毁释放
59 }
60
61 void *thread_worker1(void *args)
62 {
63 worker_ctx_t *ctx = (worker_ctx_t *)args;
64
65 if( !args )
66 {
67 printf("%s() get invalid arguments\n", __FUNCTION__);
68 pthread_exit(NULL);
69 }
70
71 printf("Thread worker 1 [%ld] start running...\n", pthread_self());
72
73 while(1)
74 {
75 pthread_mutex_lock(&ctx->lock); // 这里调用pthread_mutex_lock() 来申请锁,这里是阻塞锁,如果锁被别的线程持有则该函数不会返回
76
77 printf("+++: %s before shared_var++: %d\n", __FUNCTION__, ctx->shared_var);
78 ctx->shared_var ++;
79 sleep(2);
80 printf("+++: %s after sleep shared_var: %d\n", __FUNCTION__, ctx->shared_var);
81
82 pthread_mutex_unlock(&ctx->lock); //在访问临界资源(shared_var)完成退出临界区时,我们调用pthread_mutex_unlock来释放锁,这样其他线程才能再次访问
83
84 sleep(1); //这里都要加上延时,否则一个线程拿到锁之后会一直占有该锁;另外一个线程则不能获取到锁
85 }
86
87
88 printf("Thread worker 1 exit...\n");
89
90 return NULL;
91
92 }
93
94 void *thread_worker2(void *args)
95 {
96 worker_ctx_t *ctx = (worker_ctx_t *)args;
97
98 if( !args )
99 {
100 printf("%s() get invalid arguments\n", __FUNCTION__);
101 pthread_exit(NULL);
102 }
103
104 printf("Thread worker 2 [%ld] start running...\n", pthread_self());
105
106 while(1)
107 {
108 if( 0 != pthread_mutex_trylock(&ctx->lock)) //第二个线程我们使用pthread_mutex_trylock 来申请锁,这里使用的是非阻塞锁;如果锁现在被别的线程占用则返回非0值,如果没有被占用则返回0
109 {
110 continue;
111 }
112
113 printf("---: %s before shared_var++: %d\n", __FUNCTION__, ctx->shared_var);
114 ctx->shared_var ++;
115 sleep(2);
116 printf("---: %s after sleep shared_var: %d\n", __FUNCTION__, ctx->shared_var);
117
118 pthread_mutex_unlock(&ctx->lock);
119
120 sleep(1); //这里都要加上延时,否则一个线程拿到锁之后会一直占有该锁;另外一个线程则不能获取到锁
121 }
122
123 printf("Thread worker 2 exit...\n");
124
125 return NULL;
126
127 }
运行结果
这样就解决了共享资源的问题,完成了两个线程的独立自加!
(5)死锁
- 什么是死锁
如果多个线程要调用多个对象,则在上锁的时候可能会出现“死锁”。举个例子: A、B两个线程会同时使用到两个共享变量m和n,同时每个变量都有自己相应的锁M和N。 这时A线程首先拿到M锁访问m,接下来他需要拿N锁访问变量n; 而如果此时B线程拿着N锁等待着M锁的话,就造成了线程“死锁”。
- 死锁产生的4个必要条件
1.互斥:某种资源一次只允许一个进程访问,即该资源一旦分配给某个进程, 其他进程就不能再访问,直到该进程访问结束
2.占有且等待:一个进程本身占有资源(一种或多种),同时还有资源未得到满足,正等着其他进程释放该资源
3.不可抢占:别人已经占有了某项,你不能因为自己也需要该资源,就去把别人的资源抢过来
4.循环等待:存在一个进程链,使得每个进程都占有下一个进程所需的至少一种资源
当以上四个条件均满足,必然会造成死锁,发生死锁的进程无法进行下去,它们所持有的资源也无法释放。这样会导致CPU的吞吐量下降。所以死锁情况是会浪费系统资源和影响计算机的使用性能的。那么,解决死锁问题就是相当有必要的了。
- 解决死锁
产生死锁需要四个条件,那么,只要这四个条件中至少有一个条件得不到满足,就不可能发生死锁了。由于互斥条件是非共享资源所必须的,不仅不能改变,还应加以保证,所以,主要是破坏产生死锁的其他三个条件。
a.破坏“占有且等待”条件
方法1:所有的进程在开始运行之前,必须一次性地申请其在整个运行过程中所需要的全部资源。
优点:简单易实施且安全。
缺点:因为某项资源不满足,进程无法启动,而其他已经满足了的资源也不会得到利用,严重降低了资源的利用率,造成资源浪费。使进程经常发生饥饿现象。
方法2:该方法是对第一种方法的改进,允许进程只获得运行初期需要的资源,便开始运行,在运行过程中逐步释放掉分配到的已经使用完毕的资源,然后再去请求新的资源。这样的话,资源的利用率会得到提高,也会减少进程的饥饿问题。
b.破坏“不可抢占”条件
当一个已经持有了一些资源的进程在提出新的资源请求没有得到满足时,它必须释放已经保持的所有资源,待以后需要使用的时候再重新申请。这就意味着进程已占有的资源会被短暂地释放或者说是被抢占了。该种方法实现起来比较复杂,且代价也比较大。释放已经保持的资源很有可能会导致进程之前的工作实效等,反复的申请和释放资源会导致进程的执行被无限的推迟,这不仅会延长进程的周转周期,还会影响系统的吞吐量。
c.破坏“循环等待”条件
可以通过定义资源类型的线性顺序来预防,可将每个资源编号,当一个进程占有编号为i的资源时,那么它下一次申请资源只能申请编号大于i的资源。