多线程编程与互斥锁

2023-10-27

一、线程理论基础

在操作系统原理的术语中,线程是进程的一条执行路径。线程在Unix系统下,通常被称为轻量级的进程,线程虽然不是进程,但却可以看作是Unix进程的表亲,所有的线程都是在同一进程空间运行,这也意味着多条线程将共享该进程中的全部系统资源,如虚拟地址空间,文件描述符和信号处理等等。但同一进程中的多个线程有各自的调用栈(call stack),自己的寄存器环境(register context),自己的线程本地存储(thread-local storage)。 一个进程可以有很多线程,每条线程并行执行不同的任务。在这里插入图片描述
线程可以提高应用程序在多核环境下处理诸如文件I/O或者socket I/O等会产生堵塞的情况的表现性能。在Unix系统中,一个进程包含很多东西,包括可执行程序以及一大堆的诸如文件描述符地址空间等资源。在很多情况下,完成相关任务的不同代码间需要交换数据。如果采用多进程的方式,进程的创建所花的时间片要比线程大些,另外进程间的通信比较麻烦,需要在用户空间和内核空间进行频繁的切换,开销很大。但是如果使用多线程的方式,因为可以使用共享的全局变量,所以线程间的通信(数据交换)变得非常高效。

有了进程,为何还要引入线程呢?使用多线程有哪些好处?多进程和多线程是两种实现并发的不同方式。它们的主要区别在于它们所创建的执行单元的不同,以及它们在共享和保护系统资源方面的不同方式。

执行单元:多进程创建不同的进程,每个进程有自己的独立地址空间和资源,它们通过进程间通信(IPC)来进行通信和同步。多线程创建在同一进程内的不同线程,它们共享相同的地址空间和资源,通过线程间通信和同步机制来交互。

资源共享:多进程之间共享资源需要显式地进行通信,通常使用管道、消息队列、共享内存等IPC机制,而多线程之间则可以直接访问共享的变量和数据结构。

资源保护:多进程之间不会共享内存,因此不需要特别的同步机制来保护共享资源。但是,多线程共享内存,需要通过同步机制(如互斥锁、条件变量、信号量等)来确保对共享资源的访问正确性。

上下文切换:在多进程中,由于进程间切换时需要切换地址空间和上下文环境,因此进程间切换的代价较高。在多线程中,由于线程共享地址空间和资源,上下文切换的代价较低,因此线程间切换的效率更高。

总的来说,多进程和多线程都可以用来实现并发,但在不同的情况下会有不同的优缺点。多进程适用于需要独立管理和保护资源的情况,而多线程适用于需要更高的并发性和更低的上下文切换代价的情况。Linux系统下的多线程遵循POSIX线程接口,称为pthread。编写Linux下的多线程程序,需要使用头文件pthread.h,连接(编译)时需要加libpthread.a

二、创建线程

一个进程创建后,会首先生成一个缺省的线程,通常称这个线程为主线程(或称控制线程),C/C++程序中,主线程就是通过main函数进入的线程,由主线程调pthread_create()创建的线程称为子线程,子线程也可以有自己的入口函数,该函数由用户在创建的时候指定。每个线程都有自己的线程ID,可以通过pthread_self()函数获取。最常见的线程模型中,除主线程较为特殊之外,其他线程一旦被创建,相互之间就是对等关系,不存在隐含的层次关系。每个进程可创建的最大线程数由具体实现决定。
无论在windows中还是Posix中,主线程和子线程的默认关系是:无论子线程执行完毕与否,一旦主线程执行完毕退出,所有子线程执行都会终止。这时整个进程结束或僵死,部分线程保持一种终止执行但还未销毁的状态,而进程必须在其所有线程销毁后销毁,这时进程处于僵死状态。线程函数执行完毕退出,或以其他非常方式终止,线程进入终止态,但是为线程分配的系统资源不一定释放,可能在系统重启之前,一直都不能释放,终止态的线程,仍旧作为一个线程实体存在于操作系统中,什么时候销毁,取决于线程属性。在这种情况下,主线程和子线程通常定义以下两种关系:

  1. 可会合(joinable):这种关系下,主线程需要明确执行等待操作,在子线程结束后,主线程的等待操作执行完毕,子线程和主线程会合,这时主线程继续执行等待操作之后的下一步操作。主线程必须会合可会合的子线程。在主线程的线程函数内部调用子线程对象的wait函数实现,即使子线程能够在主线程之前执行完毕,进入终止态,也必须执行会合操作,否则,系统永远不会主动销毁线程,分配给该线程的系统资源也永远不会释放。
  2. 相分离(detached):表示子线程无需和主线程会合,也就是相分离的,这种情况下,子线程一旦进入终止状态,这种方式常用在线程数较多的情况下,有时让主线程逐个等待子线程结束,或者让主线程安排每个子线程结束的等待顺序,是很困难或不可能的,所以在并发子线程较多的情况下,这种方式也会经常使用。

线程的分离状态决定一个线程以什么样的方式来终止自己,在默认的情况下,线程是非分离状态的,这种情况下,原有的线程等待创建的线程结束,只有当pthread_join函数返回时,创建的线程才算终止,释放自己占用的系统资源,而分离线程没有被其他的线程所等待,自己运行结束了,线程也就终止了,马上释放系统资源。

下面使用一个简单的例子来讲解线程创建的基本使用和相关概念:

 1 #include <stdio.h>
 2 #include <string.h>
 3 #include <errno.h>
 4 #include <stdlib.h>
 5 #include <unistd.h>
 6 #include <pthread.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;
15        pthread_attr_t thread_attr;    //定义了创建线程的属性变量thread_attr 
16
17
18       if( pthread_attr_init(&thread_attr) )
19       {
20              printf("pthread_attr_init() failure: %s\n", strerror(errno));
21              return -1;
22       }
23
24       if( pthread_attr_setstacksize(&thread_attr, 120*1024) ) //设置线程的栈大小为120K
25       {
26             printf("pthread_attr_setstacksize() failure: %s\n", strerror(errno));
27             return -1;
28       }
29
30       if( pthread_attr_setdetachstate(&thread_attr, PTHREAD_CREATE_DETACHED) ) //设置线程的属性为分离状态
31       {
32            printf("pthread_attr_setdetachstate() failure: %s\n", strerror(errno));
33            return -1;
34       }
35
36      pthread_create(&tid, &thread_attr, thread_worker1, &shared_var);
37      printf("Thread worker1 tid[%ld] created ok\n", tid);
38
39      pthread_create(&tid, NULL, thread_worker2, &shared_var);
40      printf("Thread worker2 tid[%ld] created ok\n", tid);
41
42      pthread_attr_destroy(&thread_attr);
43
44      /* Wait until thread worker2 exit() */
45      pthread_join(tid, NULL); //导致主线程阻塞
46
47
48      while(1)
49      {
50           printf("Main/Control thread shared_var: %d\n", shared_var);
51           sleep(10);
52      }
53 }
54
55 void *thread_worker1(void *args)
56 {
57         int *ptr = (int *)args;
58
59         if( !args )
60         {
61                  intf("%s() get invalid arguments\n", __FUNCTION__);
62                  pthread_exit(NULL);
63         }
64
65         printf("Thread workder 1 [%ld] start running...\n", pthread_self());
66
67         while(1)
68         {
69              printf("+++: %s before shared_var++: %d\n", __FUNCTION__, *ptr);
70             *ptr += 1;
71              sleep(2);
72              printf("+++: %s after sleep shared_var: %d\n", __FUNCTION__, *ptr);
73 }
74
75              printf("Thread workder 1 exit...\n");
76
77              return NULL;
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 workder 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 workder 2 exit...\n");
101
102            return NULL;
103}

代码分析:

36行和39行调用pthread_create()函数用来创建了两个子线程,该函数的原型是:

#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void
*arg);
Compile and link with -pthread.

说明: pthreand_create()用来创建一个线程,并执行第三个参数start_routine所指向的函数。
第三个参数start_routine是一个函数指针,它指向的函数原型是 void *func(void *),这是所创建的子线程要执行的任务(函数);
第四个参数arg就是传给了所调用的函数的参数,如果有多个参数需要传递给子线程则需要封装到一个结构体里传进去;
第一个参数thread是一个pthread_t类型的指针,他用来返回该线程的线程ID。每个线程都能够通过pthread_self()来获取自己的线程ID(pthread_t类型)。
第二个参数是线程的属性,其类型是pthread_attr_t类型,其定义如下:

typedef struct
{
        int detachstate; //线程的分离状态
        int schedpolicy; //线程调度策略
        struct sched_param schedparam;// 线程的调度参数
        int inheritsched;// 线程的继承性
        int scope; //线程的作用域
        size_t guardsize; //线程栈末尾的警戒缓冲区大小
        int stackaddr_set;
        void * stackaddr; //线程栈的位置
        size_t stacksize; //线程栈的大小
}pthread_attr_t;

对于这些属性,我们需要设定的是线程的分离状态,如果有必要也需要修改每个线程的栈大小。每个线程创建后默认是joinable状态,该状态需要主线程调用 pthread_join 等待它退出,否则子线程在结束时,内存资源不能得到释放造成内存泄漏。所以我们创建线程时一般会将线程设置为分离状态,具体有两种方法:

  1. 线程里面调用 pthread_detach(pthread_self()) 这个方法最简单
  2. 在创建线程的属性设置里设置PTHREAD_CREATE_DETACHED属性

代码15行我们定义了创建线程的属性变量thread_attr ,在对该属性进行设置前,我们需要先调用pthread_attr_init 函数初始化它(第18行),在第24行我们设置线程的栈大小为120K,同时在第30行设置线程的属性为分离状态。第36行创建线程时使用了该属性创建线程,这时创建的子进程就是分离状态了。线程属性在使用完之后,我们应该调用pthread_attr_destroy (第45行)把他摧毁释放。

而代码39行创建子线程时并没有使用该线程,同时在thread_worker2() 里并没有调用pthread_detach()设置线程为分离状态。这时就需要主线程在45行处调用pthread_join() 等待第二个子线程退出。当然主线程也就阻塞在这里不会往下继续执行了。

在创建两个线程时,我们都通过第四个参数将主线程 栈中的 shared_var 变量地址传给了子线程,因为所有线程都是在同一进程空间中运行,而只是子线程有自己独立的栈空间,所以这时所有子线程都可以访问主线程空间的shared_var变量。

现在我们编译运行一下程序看看效果,注意对于多线程编程在编译时,一定要加上-lpthread 选项告诉链接器在链接的时候要连接pthread库:

pi@raspberrypi:~/apue/ch4_thread $ gcc thread.c -o thread -lpthread
pi@raspberrypi:~/apue/ch4_thread $ ./thread
Thread worker1 tid[1993757808] created ok
Thread workder 1 [1993757808] start running...
+++: thread_worker1 before shared_var++: 1000
Thread worker2 tid[1993634928] created ok
Thread workder 2 [1993634928] start running...
---: thread_worker2 before shared_var++: 1001
+++: thread_worker1 after sleep shared_var: 1002
+++: thread_worker1 before shared_var++: 1002
---: thread_worker2 after sleep shared_var: 1002
---: thread_worker2 before shared_var++: 1003
+++: thread_worker1 after sleep shared_var: 1004
+++: thread_worker1 before shared_var++: 1004
---: thread_worker2 after sleep shared_var: 1005
---: thread_worker2 before shared_var++: 1005

主线程创建子线程后究竟是子线程还是主线程先执行,或究竟哪个子线程先运行系统并没有规定,这个依赖操作系统的进程调度策略。当然因为代码45行处主线程调用了pthread_join会导致主线程阻塞,所以主线程不会往下继续执行while(1)循环。我们再来深入分析各个线程的代码:

我们在创建子线程之后,在子线程的执行函数里一般都会用while(1)的死循环来让子线程一直运行,否则子线程将按代码顺序执行,执行完毕就线程退出了。同样的,我们主线程也应该要用一个while(1)循环一直运行,否则主线程退出会导致进程退出,而进程退出会导致所有子线程退出了。

接下来我们在分析一下子线程所做的事,在两个子线程中做的任务是首先打印一下当前 shared_var 变量的值然后让它自加,之后睡眠2s后再打印一下这两个值,代码如下:

while(1)
 {
       printf("+++: %s before shared_var++: %d\n", __FUNCTION__, *ptr);
       *ptr += 1;
       sleep(2);
       printf("+++: %s after sleep shared_var: %d\n", __FUNCTION__, *ptr);
 }

从上面的运行结果的部分我们可以看到,thread_worker1 在创建后首先开始运行,在开始自加之前值为初始值
1000,然后让该值自加后休眠2秒后再打印该值发现不是1001而是1002了。这是由于shared_var 这个变量会被两个子线程同时访问修改导致。如果一个资源会被不同的线程访问修改,那么我们把这个资源叫做临界资源,那么对于该资源访问修改相关的代码就叫做临界区那么怎么解决多个线程之间共享同一个共享资源,是多线程编程需要考虑的一个问题。

三、互斥锁

试想一下,我们寝室/实验室只有一个洗手间,那多个人是怎么解决马桶共享的问题?对,那就是锁的机制!在这里马桶就是临界资源,我们在进入到洗手间(临界区)后,就首先上锁; 然后用完离开洗手间(临界区)之后,把锁释放供别人使用。如果有人想去洗手间时发现门锁上了,他也有两种策略:1,在洗手间那里等(阻塞);2,暂时先离开等会再过来看(非阻塞);

现在我们把代码修改一下,通过锁的机制解决共享资源的问题:

 1 #include <stdio.h>
 2 #include <string.h>
 3 #include <errno.h>
 4 #include <stdlib.h>
 5 #include <unistd.h>
 6 #include <pthread.h>
 7
 8 void *thread_worker1(void *args);
 9 void *thread_worker2(void *args);
10
11 typedef struct worker_ctx_s
12 {
13        int shared_var;
14        pthread_mutex_t lock;
15 } worker_ctx_t;                  //传递共享的变量shared_var和它相应的互斥锁lock
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);    //初始化互斥锁
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 -1;
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 -1;
43       }
44
45       pthread_create(&tid, &thread_attr, thread_worker1, &worker_ctx);
46       printf("Thread worker1 tid[%ld] created ok\n", tid);
47
48       pthread_create(&tid, &thread_attr, thread_worker2, &worker_ctx);
49       printf("Thread worker2 tid[%ld] created ok\n", tid);
50
51       while(1)
52       {
53               printf("Main/Control thread shared_var: %d\n", worker_ctx.shared_var);
54               sleep(10);
55       }
56
57       pthread_mutex_destroy(&worker_ctx.lock);  //摧毁释放互斥锁
58 }
59
60 void *thread_worker1(void *args)
61 {
62           worker_ctx_t *ctx = (worker_ctx_t *)args;
63
64      if( !args )
65      {
66             printf("%s() get invalid arguments\n", __FUNCTION__);
67             pthread_exit(NULL);
68      }
69
70      printf("Thread workder 1 [%ld] start running...\n", pthread_self());
71
72      while(1)
73     {
74              pthread_mutex_lock(&ctx->lock);   //申请阻塞锁
75
76              printf("+++: %s before shared_var++: %d\n", __FUNCTION__, ctx->shared_var);
77              ctx->shared_var ++;
78              sleep(2);
79              printf("+++: %s after sleep shared_var: %d\n", __FUNCTION__, ctx->shared_var);
80
81              pthread_mutex_unlock(&ctx->lock);    //在访问临界资源(shared_var)完成退出临界区时,调用pthread_mutex_unlock来释放锁
82
83               sleep(1);
84     }
85
86            printf("Thread workder 1 exit...\n");
87
88           return NULL;
89 }
90
91 void *thread_worker2(void *args)
92 {
93       worker_ctx_t *ctx = (worker_ctx_t *)args;
94
95      if( !args )
96      {
97            printf("%s() get invalid arguments\n", __FUNCTION__);
98            pthread_exit(NULL);
99      }
100
101     printf("Thread workder 2 [%ld] start running...\n", pthread_self());
102
103           while(1)
104           {
105               if(0 != pthread_mutex_trylock(&ctx->lock) ) //使用非阻塞锁
106               {
107               continue;
108               }
109
110               printf("---: %s before shared_var++: %d\n", __FUNCTION__, ctx->shared_var);
111               ctx->shared_var ++;
112               sleep(2);
113               printf("---: %s after sleep shared_var: %d\n", __FUNCTION__, ctx->shared_var);
114
115               pthread_mutex_unlock(&ctx->lock);
116
117               sleep(1);
118           } 
119
120      printf("Thread workder 2 exit...\n");
121
122      return NULL;
123 }

代码分析:
代码的11~15行:因为在创建线程给线程执行函数传参的时候只能传一个参数,而我们要传递共享的变量shared_var和它相应的互斥锁lock,所以在这里需要用结构体(worker_ctx_t, ctx: context)将它们封装在一块传进去。
代码19行:使用work_ctx_t结构体类型定义了传给子线程的变量参数;
代码24行:互斥锁在使用之前,需要先调用 pthread_mutex_init() 函数来初始化互斥锁;
代码48行:在创建第二个线程时也设置了分离属性,这时主线程后面的while(1)循环就会执行了;
代码57行:互斥锁在使用完之后,我们应该调用pthread_mutex_destroy()将他摧毁释放;
代码74行: 这里调用pthread_mutex_lock() 来申请锁,这里是阻塞锁,如果锁被别的线程持有则该函数不会返回;
代码81行: 在访问临界资源(shared_var)完成退出临界区时,我们调用pthread_mutex_unlock来释放锁,这样其他线程才能再次访问;
代码105行: 第二个线程我们使用pthread_mutex_trylock 来申请锁,这里使用的是非阻塞锁;如果锁现在被别的线程占用则返回非0值,如果没有被占用则返回0;
代码83行、117行: 这里都要加上延时,否则一个线程拿到锁之后会一直占有该锁;另外一个线程则不能获取到锁;

pi@raspberrypi:~/apue/ch4_thread $ gcc thread_lock.c -o thread_lock -lpthread
pi@raspberrypi:~/apue/ch4_thread $ ./thread_lock
Thread worker1 tid[1994032240] created ok
Thread workder 1 [1994032240] start running...
+++: thread_worker1 before shared_var++: 1000
Thread worker2 tid[1993909360] created ok
Main/Control thread shared_var: 1001
Thread workder 2 [1993909360] start running...
+++: thread_worker1 after sleep shared_var: 1001
---: thread_worker2 before shared_var++: 1001
---: thread_worker2 after sleep shared_var: 1002
+++: thread_worker1 before shared_var++: 1002
+++: thread_worker1 after sleep shared_var: 1003
---: thread_worker2 before shared_var++: 1003
---: thread_worker2 after sleep shared_var: 1004
+++: thread_worker1 before shared_var++: 1004
Main/Control thread shared_var: 1005
+++: thread_worker1 after sleep shared_var: 1005
---: thread_worker2 before shared_var++: 1005

四、死锁

如果多个线程要调用多个对象,则在上锁的时候可能会出现“死锁”。举个例子: 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的资源。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

多线程编程与互斥锁 的相关文章

随机推荐

  • 疼失鸡鸡,吊唁鸡鸡

    为了忘却的纪念 今天是我痛失鸡鸡的日子 一个好鸡鸡是很难找的 一个集群的鸡鸡跟难找的 为了吊唁我的鸡鸡 我觉得为它写一篇文章
  • Matplot pyplot绘制单图,多子图不同样式详解,这一篇就够了

    Matplot pyplot绘制单图 多子图不同样式详解 这一篇就够了 1 单图单线 2 单图多线不同样式 红色圆圈 蓝色实线 绿色三角等 2 1 4 支持的所有颜色及样式 3 使用关键字字符串绘图 data 可指定依赖值为 numpy r
  • 【BLE】蓝牙抓包器 Ellisys 使用说明

    BLE 蓝牙抓包器 Ellisys 使用说明 常用功能 设备过滤 抓包的类型 添加观察的项目 协议解析 连接过程 Connection Indication LLCP Feature Request Response LLCP Length
  • Java多线程通信-CountDownLatch(闭锁)

    一 CountDownLatch 闭锁 闭锁是一个同步工具类 它可以延迟线程的进度直到其到达终止状态 闭锁的作用相当于一扇门 在到达结束状态之前 这扇门是关闭的 并且不允许任何进程通过 当到达结束状态时 这扇门会打开并允许所有的线程通过 当
  • wangeditor3.0上传本地图片和本地视频至服务器

    1 效果 2 注意 我下载的3 0版本 3 组件 在components文件里创建一个wangEditoe vue文件
  • Java基于opencv实现图像数字识别(一)

    Java基于opencv实现图像数字识别 一 最近分到了一个任务 要做数字识别 我分配到的任务是把数字一个个的分开 当时一脸懵逼 直接百度java如何分割图片中的数字 然后就百度到了用BufferedImage这个类进行操作 尝试着做了一下
  • Vue生命周期和钩子函数详解

    Vue生命周期和钩子函数详解 Vue生命周期介绍 组件每个阶段它的内部构造是不一样的 所以一般特定的钩子做特定的事 比如Ajax获取数据就可以在mounted阶段 从Vue实例被创建开始到该实例最终被销毁的整个过程叫做VUE的生命周期 在这
  • A*算法学习笔记

    1 算法思路 1 Dijkstra算法与A 算法 1 Dijkstra算法 贪心策略 优先队列 集合S 已确定的顶点集合 初始只含源点s 集合T 尚未确定的顶点集合 算法反复从集合T中选择当前到源点s最近的顶点u 将u加入集合S 然后对所有
  • 使用myisamchk命令修复表 只能修复myisam表 速度块

    快速检查 myisamchk im var lib mysql db1 只检查没有正常关闭的表 myisamchk iFm var lib mysql db1 仅显示标的重要信息 myisamchk eis var lib mysql db
  • 【Markdown】Typora配置图片上传

    文章目录 0 前言 1 确定需求 2 开始配置 2 1 软件储备 2 2 插件安装 2 3 gitee配置 3 其他配置 3 1 获取SMMS token 参考链接 0 前言 对于喜欢写Markdown文档的人来说 Typora无疑是一个写
  • 30分钟学会如何使用Shiro

    http www cnblogs com learnhow p 5694876 html 一 架构 要学习如何使用Shiro必须先从它的架构谈起 作为一款安全框架Shiro的设计相当精妙 Shiro的应用不依赖任何容器 它也可以在JavaS
  • 宏定义详细知识点

    一 不带参数的宏定义 1 格式 define 宏名 字符串 例 define a 6 则a是宏名 凡是出现a的地方均用6替换 2 注意 宏替换是一种机械替换 不做语法检查 不是下一个语句 其后不加 define命令出现在函数的外面 有效范围
  • 框架中常见的设计模式有哪些学习总结第一篇

    框架中常见的设计模式有哪些 设计模式的作用 通过设计模式写代码 设计模式可以解耦 解耦只是一种思想 代码开发的时候 把代码分开便于维护和管理 运行的时候再合并起来运行 回顾软件设计原则 开闭原则 对扩展开放 对修改关闭 使用范围特变广 单一
  • c语言之实现fastcgi协议的代码完整实现

    FastCGI协议是在CGI协议的基础上发展出来的 如果想了解CGI协议 可以看我另一篇文章 动态web技术 二 CGI FastCGI程序本身监听某个socket然后等待来自web服务器的连接 而不是像CGI程序是由web服务器 fork
  • Android Studio 快速跳转到XML布局界面

    http www jianshu com p 8ca15b831b31 我们开发Android应用程序时 Activity或者Fragment会有一个相对应的布局 在Eclipse中或者一般的做法 我们会在Java代码中找到对应的代码 然后
  • jquery之ajax——全局事件引用方式以及各个事件(全局/局部)执行顺序

    jquery中各个事件执行顺序如下 1 ajaxStart 全局事件 2 beforeSend 局部事件 3 ajaxSend 全局事件 4 success 局部事件 5 ajaxSuccess 全局事件 6 error 局部事件 7 aj
  • Python画图示例(1) 一维数据集绘图

    Python画图示例 1 一维数据集绘图 Python画图示例 2 二维数据集绘图 Python画图示例 3 其他绘图样式 散点图 直方图等 Python画图示例 4 3D绘图 目录 1 用 Numpy ndarray 作为数据传入 ply
  • Trie 前缀树 字典树 简介+实现

    简介 最上面的是根结点 这棵树中存的单词是apple app all bat 如果IsWord为True 就说明从根节点连到这个结点的字母组成的是一个单词 使用前缀树查询的时候时间复杂度只和单词的长度相关 实现 import java ut
  • ctfshow-内部赛

    登录就有flag签退蓝瘦出题人不想跟你说话 jpg 登录就有flag 经过一番固定的注入尝试发现 1 长度限制为5 2 存在过滤且过滤的字符会有回显 能留下来的字符很少这里列出 gt lt 在排除一下 gt lt 可以只留等于号 逗号和点号
  • 多线程编程与互斥锁

    一 线程理论基础 在操作系统原理的术语中 线程是进程的一条执行路径 线程在Unix系统下 通常被称为轻量级的进程 线程虽然不是进程 但却可以看作是Unix进程的表亲 所有的线程都是在同一进程空间运行 这也意味着多条线程将共享该进程中的全部系