Web Server3
第二章 Linux 多线程开发
0. 遇到一个小问题
VScode 远程连接虚拟机出问题了,可能是我刚刚配置好了 WSL2 的环境导致的。
解决方案:
在任意一个文件夹中打开 VScode 别用 WSL2 登录就行
VSCode -> View -> Command Palette -> input "remote" -> Select Remote-SSH: Kill VS Code Server on Host...
重新启动即可正常连接
1. 进程概述
1.1 进程的概述
◼ 与进程(process)类似,线程(thread)是允许应用程序并发执行多个任务的一种机制。 一个进程可以包含多个线程。 同一个程序中的所有线程均会独立执行相同程序,且共享同一份全局内存区域,其中包括初始化数据段、未初始化数据段,以及堆内存段。(传统意义上的 UNIX 进程只是多线程程序的一个特例,该进程只包含一个线程)
Linux 下线程的底层是进程实现的。
◼ 进程是 CPU 分配资源的最小单位,线程是操作系统调度执行的最小单位。
◼ 线程是轻量级的进程(LWP:Light Weight Process),在 Linux 环境下线程的本质仍是进程。
◼ 查看指定进程的 LWP 号:ps –Lf pid
1.2 线程和进程的区别
进程是计算机资源分配的最小单位
线程是计算机调度的最小单位
◼ 进程间的信息难以共享。由于除去只读代码段外,父子进程并未共享内存,因此必须采用一些进程间通信方式,在进程间进行信息交换。
◼ 调用 fork() 来创建进程的代价相对较高,即便利用写时复制技术,仍然需要复制诸如内存页表和文件描述符表之类的多种进程属性,这意味着 fork() 调用在时间上的开销依然不菲。
◼ 线程之间能够方便、快速地共享信息。只需将数据复制到共享(全局或堆)变量中即可。
◼ 创建线程比创建进程通常要快 10 倍甚至更多。线程间是共享虚拟地址空间的,无需采用写时复制来复制内存,也无需复制页表。
1.3 线程和进程虚拟地址空间
线程共享空间,将空间划分开,供各个线程使用,main
是主线程,其余是子线程,如下图所示:
1.4 线程之间共享和非共享资源
◼ 共享资源
进程 ID 和父进程 ID
进程组 ID 和会话 ID
用户 ID 和 用户组 ID
文件描述符表
信号处置
文件系统的相关信息:文件权限掩码(umask)、当前工作目录
虚拟地址空间(除栈、.text)
◼ 非共享资源
线程 ID
信号掩码
线程特有数据
error 变量
实时调度策略和优先级
栈,本地变量和函数的调用链接信息
1.5 NPTL
◼ 当 Linux 最初开发时,在内核中并不能真正支持线程。但是它的确可以通过 clone() 系统调用将进程作为可调度的实体。这个调用创建了调用进程(calling process)的一个拷贝,这个拷贝与调用进程共享相同的地址空间。LinuxThreads 项目使用这个调用来完成在用户空间模拟对线程的支持。不幸的是,这种方法有一些缺点,尤其是在信号处理、调度和进程间同步等方面都存在问题。另外,这个线程模型也不符合 POSIX 的要求。
◼ 要改进 LinuxThreads,需要内核的支持,并且重写线程库。有两个相互竞争的项目开始来满足这些要求。一个包括 IBM 的开发人员的团队开展了 NGPT(Next-Generation POSIX Threads)项目。同时,Red Hat 的一些开发人员开展了 NPTL 项目。NGPT 在 2003 年中期被放弃了,把这个领域完全留给了 NPTL。
◼ NPTL,或称为 Native POSIX Thread Library,是 Linux 线程的一个新实现,它克服了 LinuxThreads 的缺点,同时也符合 POSIX 的需求。与 LinuxThreads 相比,它在性能和稳定性方面都提供了重大的改进。
◼ 查看当前 pthread 库版本:getconf GNU_LIBPTHREAD_VERSION
2. 线程操作
2.1 创建线程
◼ int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 #include <stdio.h> #include <pthread.h> void *callback (void *arg) { printf ("child thread...\n" ); return NULL ; } int main () { pthread_t tid; int ret = pthread_create(&tid, NULL , callback, NULL ); if (ret != 0 ) { char *errstr = strerror(ret); printf ("error : %s\n" , errstr); } for (int i = 0 ; i < 5 ; i++) { printf ("%d\n" , i); } return 0 ; }
编译命令:由于线程库是非官方库,所以需要 -pthread
参数来动态链接
1 gcc pthread_create.c -o create -pthread
或者
1 gcc pthread_create.c -o create -lpthread
可以看到直接执行的话,并没有打印 callback()
中的内容,这是因为线程共享内存空间,但是主进程执行完后调用 return 0;
即 exit(0);
终止了,所以空间释放了,那么子进程也无法执行。
加入一个 sleep(1);
这样子线程很大概率能拿到执行权,运行结果如下:
再给线程的 callback()
函数加入一个参数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 #include <stdio.h> #include <pthread.h> void *callback (void *arg) { printf ("child thread...\n" ); printf ("arg_value = %d\n" , *(int *)arg); return NULL ; } int main () { pthread_t tid; int num = 520 ; int ret = pthread_create(&tid, NULL , callback, (void *)&num); if (ret != 0 ) { char *errstr = strerror(ret); printf ("error : %s\n" , errstr); } for (int i = 0 ; i < 5 ; i++) { printf ("%d\n" , i); } sleep(1 ); return 0 ; }
编译运行结果如下:
2.2 终止线程
◼ pthread_t pthread_self(void);
◼ int pthread_equal(pthread_t t1, pthread_t t2);
◼ void pthread_exit(void *retval);
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 #include <stdio.h> #include <pthread.h> #include <string.h> void *callback (void *arg) { printf ("child thread id : %ld\n" , pthread_self()); return NULL ; } int main () { pthread_t tid; int ret = pthread_create(&tid, NULL , callback, NULL ); if (ret != 0 ) { char *errstr = strerror(ret); printf ("error: %s\n" , errstr); } for (int i = 0 ; i < 5 ; i++) { printf ("%d\n" , i); } printf ("tid : %ld, main thread id : %ld\n" , tid, pthread_self()); pthread_exit(NULL ); printf ("main thread exit\n" ); return 0 ; }
可以看到线程交替执行,子线程和主线程中查到的子线程 ID 是一样的。
2.3 连接已终止的线程
◼ int pthread_join(pthread_t thread, void **retval);
线程使用结束后需要回收资源,不过任何一个线程都可以回收其他线程的资源,不一定非要主线程回收子线程。
但是不应该用不相干的线程相互回收,所以一般是主线程回收子线程。不相关的线程就建立相关再回收。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 #include <stdio.h> #include <pthread.h> #include <string.h> #include <unistd.h> void *callback (void *arg) { printf ("child thread id : %ld\n" , pthread_self()); sleep(3 ); int value = 10 ; pthread_exit((void *)&value); } int main () { pthread_t tid; int ret = pthread_create(&tid, NULL , callback, NULL ); if (ret != 0 ) { char *errstr = strerror(ret); printf ("error : %s\n" , errstr); } for (int i = 0 ; i < 5 ; i++) { printf ("%d\n" , i); } printf ("tid : %ld, man thread id : %ld\n" , tid, pthread_self()); int *thread_retval; ret = pthread_join(tid, (void **)&thread_retval); if (ret != 0 ) { char *errstr = strerror(ret); printf ("error : %s\n" , errstr); } printf ("exit data : %d\n" , *thread_retval); printf ("回收子线程资源成功!\n" ); return 0 ; }
运行程序可以看到,不同子线程回收的返回值是不一样的:
2.4 线程的分离
◼ int pthread_detach(pthread_t thread);
detach 分离后的子线程在结束时资源自动回收,但不能再使用 pthread_join()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 #include <stdio.h> #include <pthread.h> #include <string.h> #include <unistd.h> void *callback (void *arg) { printf ("chlid thread id : %ld\n" , pthread_self()); return NULL ; } int main () { pthread_t tid; int ret = pthread_create(&tid, NULL , callback, NULL ); if (ret != 0 ) { char *errstr = strerror(ret); printf ("error1 : %s\n" , errstr); } printf ("tid : %ld, main thread id : %ld\n" , tid, pthread_self()); ret = pthread_detach(tid); if (ret != 0 ) { char *errstr = strerror(ret); printf ("error2 : %s\n" , errstr); } ret = pthread_join(tid, NULL ); if (ret != 0 ) { char *errstr = strerror(ret); printf ("error3 : %s\n" , errstr); } pthread_exit(NULL ); return 0 ; }
可以看到对分离的子线程进行 join 操作会报错
去掉 join 部分代码,可以看到子线程正常回收(也就是没啥现象)
2.5 线程取消
◼ int pthread_cancel(pthread_t thread);
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 #include <stdio.h> #include <pthread.h> #include <string.h> #include <unistd.h> void *callback (void *arg) { printf ("child id : %ld\n" , pthread_self()); for (int i = 0 ; i < 100 ; i++) { printf ("child : %d\n" , i); } return NULL ; } int main () { pthread_t tid; int ret = pthread_create(&tid, NULL , callback, NULL ); if (ret != 0 ) { char *strerr = strerror(ret); printf ("error1: %s\n" , strerr); } pthread_cancel(tid); for (int i = 0 ; i < 5 ; i++) { printf ("%d\n" , i); } printf ("tid : %ld, main thread id : %ld\n" , tid, pthread_self()); pthread_exit(NULL ); return 0 ; }
可以看到子线程没执行完就被取消了,并且多次执行被取消的位置可能不一样:
3. 线程属性
◼ 线程属性类型 pthread_attr_t
◼ int pthread_attr_init(pthread_attr_t *attr);
◼ int pthread_attr_destroy(pthread_attr_t *attr);
◼ int pthread_attr_getdetachstate(const pthread_attr_t *attr, int *detachstate);
◼ int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 #include <stdio.h> #include <pthread.h> #include <string.h> #include <unistd.h> void *callback (void *arg) { printf ("child thread id : %ld\n" , pthread_self()); return NULL ; } int main () { pthread_attr_t attr; pthread_attr_init(&attr); pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); pthread_t tid; int ret = pthread_create(&tid, &attr, callback, NULL ); if (ret != 0 ) { char *strerr = strerror(ret); printf ("error1 : %s\n" , strerr); } size_t size; pthread_attr_getstacksize(&attr, &size); printf ("thread stack size : %ld\n" , size); printf ("tid : %ld, main thread id : %ld\n" , tid, pthread_self()); pthread_attr_destroy(&attr); pthread_exit(NULL ); return 0 ; }
运行结果如下:
线程栈的大小为 8388608 bytes
4. 线程同步
4.1 线程同步
线程同步几乎是线程中最重要的知识了,先看个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 #include <stdio.h> #include <pthread.h> #include <unistd.h> int tickets = 100 ;void *sellticket (void *arg) { while (tickets > 0 ) { usleep(6000 ); printf ("%ld 正在卖第 %d 张门票\n" , pthread_self(), tickets); tickets--; } return NULL ; } int main () { pthread_t tid1, tid2, tid3; pthread_create(&tid1, NULL , sellticket, NULL ); pthread_create(&tid2, NULL , sellticket, NULL ); pthread_create(&tid3, NULL , sellticket, NULL ); pthread_join(tid1, NULL ); pthread_join(tid2, NULL ); pthread_join(tid3, NULL ); pthread_exit(NULL ); return 0 ; }
多运行几次程序,可以看到运行过程中代码的执行结果是有问题的,尤其是开头和结束的时候问题很明显:
三个线程都卖出了同一张票 第100张
一种可能是三个线程相继经过 6ms 的休眠,然后第一个线程刚执行完打印函数,第二个线程就抢占了资源,也完成了打印函数,然后紧接着被第三个线程抢占了资源,执行了打印函数,所以同时卖出了 3 张标号为 100 的票。
同时在第二轮的休眠时间里,三个线程又相继完成了 tickets--
,于是乎下一次直接卖出了标号为 97 的门票。
1 2 3 4 5 6 7 8 9 10 11 int tickets = 100 ;void *sellticket (void *arg) { while (tickets > 0 ) { usleep(6000 ); printf ("%ld 正在卖第 %d 张门票\n" , pthread_self(), tickets); tickets--; } return NULL ; }
最后有两个线程卖出了不存在的票 第0张、第-1张
tickets == 1
的时候,三个线程都已经进入了循环,所以避免了判断过程,打印了不存在门票。
◼ 线程的主要优势在于,能够通过全局变量来共享信息。不过,这种便捷的共享是有代价的:必须确保多个线程不会同时修改同一变量,或者某一线程不会读取正在由其他线程修改的变量。
◼ 临界区是指访问某一共享资源的代码片段,并且这段代码的执行应为原子操作,也就是同时访问同一共享资源的其他线程不应中断该片段的执行。
◼ 线程同步:即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作,其他线程才能对该内存地址进行操作,而其他线程则处于等待状态。
4.2 互斥量
◼ 为避免线程更新共享变量时出现问题,可以使用互斥量(mutex 是 mutual exclusion 的缩写)来确保同时仅有一个线程可以访问某项共享资源。可以使用互斥量来保证对任意共享资源的原子访问。
◼ 互斥量有两种状态:已锁定(locked)和未锁定(unlocked)。任何时候,至多只有一个线程可以锁定该互斥量。试图对已经锁定的某一互斥量再次加锁,将可能阻塞线程或者报错失败,具体取决于加锁时使用的方法。
◼ 一旦线程锁定互斥量,随即成为该互斥量的所有者,只有所有者才能给互斥量解锁。一般情况下,对每一共享资源(可能由多个相关变量组成)会使用不同的互斥量,每一线程在访问同一资源时将采用如下协议:
⚫ 针对共享资源锁定互斥量
⚫ 访问共享资源
⚫ 对互斥量解锁
◼ 如果多个线程试图执行这一块代码(一个临界区),事实上只有一个线程能够持有该互斥量(其他线程将遭到阻塞),即同时只有一个线程能够进入这段代码区域,如下图所示:
4.3 互斥量相关操作函数
◼ 互斥量的类型 pthread_mutex_t
◼ int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
◼ int pthread_mutex_destroy(pthread_mutex_t *mutex);
◼ int pthread_mutex_lock(pthread_mutex_t *mutex);
◼ int pthread_mutex_trylock(pthread_mutex_t *mutex);
◼ int pthread_mutex_unlock(pthread_mutex_t *mutex);
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 #include <stdio.h> #include <pthread.h> #include <unistd.h> int tickets = 100 ;pthread_mutex_t mutex;void *sellticket (void *arg) { while (1 ) { pthread_mutex_lock(&mutex); if (tickets > 0 ) { usleep(6000 ); printf ("%ld 正在卖第 %d 张门票\n" , pthread_self(), tickets); tickets--; } else { pthread_mutex_unlock(&mutex); break ; } pthread_mutex_unlock(&mutex); } return NULL ; } int main () { pthread_mutex_init(&mutex, NULL ); pthread_t tid1, tid2, tid3; pthread_create(&tid1, NULL , sellticket, NULL ); pthread_create(&tid2, NULL , sellticket, NULL ); pthread_create(&tid3, NULL , sellticket, NULL ); pthread_join(tid1, NULL ); pthread_join(tid2, NULL ); pthread_join(tid3, NULL ); pthread_exit(NULL ); pthread_mutex_destroy(&mutex); return 0 ; }
编译运行,可以看到票正常售出,没有了前面的问题。
4.4 死锁
◼ 有时,一个线程需要同时访问两个或更多不同的共享资源,而每个资源又都由不同的互斥量管理。当超过一个线程加锁同一组互斥量时,就有可能发生死锁。
◼ 两个或两个以上的进程在执行过程中,因争夺共享资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁。
◼ 死锁的几种场景:
5. 读写锁
◼ 当有一个线程已经持有互斥锁时,互斥锁将所有试图进入临界区的线程都阻塞住。但是考虑一种情形,当前持有互斥锁的线程只是要读访问共享资源,而同时有其它几个线程也想读取这个共享资源,但是由于互斥锁的排它性,所有其它线程都无法获取锁,也就无法读访问共享资源了,但是实际上多个线程同时读访问共享资源并不会导致问题。
◼ 在对数据的读写操作中,更多的是读操作,写操作较少,例如对数据库数据的读写应用。为了满足当前能够允许多个读出,但只允许一个写入的需求,线程提供了读写锁来实现。
◼ 读写锁的特点:
如果有线程读数据,则允许其它线程执行读操作,但不允许写操作。
如果有线程写数据,则其它线程都不允许读、写操作。
写是独占的,写的优先级高。
◼ 读写锁的类型 pthread_rwlock_t
◼ int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
◼ int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
◼ int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
◼ int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
◼ int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
◼ int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
◼ int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
首先使用普通的进程锁:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 #include <stdio.h> #include <pthread.h> #include <unistd.h> int num = 1 ;pthread_mutex_t mutex;void *writeNum (void *arg) { while (1 ) { pthread_mutex_lock(&mutex); num++; printf ("++write, tid : %ld, num : %d\n" , pthread_self(), num); pthread_mutex_unlock(&mutex); usleep(100 ); } return NULL ; } void *readNum (void *arg) { while (1 ) { pthread_mutex_lock(&mutex); printf ("===read, tid : %ld, num : %d\n" , pthread_self(), num); pthread_mutex_unlock(&mutex); usleep(100 ); } return NULL ; } int main () { pthread_mutex_init(&mutex, NULL ); pthread_t wtids[3 ], rtids[5 ]; for (int i = 0 ; i < 3 ; i++) { pthread_create(&wtids[i], NULL , writeNum, NULL ); } for (int i = 0 ; i < 5 ; i++) { pthread_create(&rtids[i], NULL , readNum, NULL ); } for (int i = 0 ; i < 3 ; i++) { pthread_detach(wtids[i]); } for (int i = 0 ; i < 5 ; i++) { pthread_detach(rtids[i]); } pthread_exit(NULL ); pthread_mutex_destroy(&mutex); return 0 ; }
编译运行后可以看到,一次只能一个进程写,一次也只能有一个进程读,写一次后读到的数据是刚刚写的数据,即进程是同步的:
使用读写锁:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 #include <stdio.h> #include <pthread.h> #include <unistd.h> int num = 1 ;pthread_rwlock_t rwlock;void *writeNum (void *arg) { while (1 ) { pthread_rwlock_wrlock(&rwlock); num++; printf ("++write, tid : %ld, num : %d\n" , pthread_self(), num); pthread_rwlock_unlock(&rwlock); usleep(100 ); } return NULL ; } void *readNum (void *arg) { while (1 ) { pthread_rwlock_rdlock(&rwlock); printf ("===read, tid : %ld, num : %d\n" , pthread_self(), num); pthread_rwlock_unlock(&rwlock); usleep(100 ); } return NULL ; } int main () { pthread_rwlock_init(&rwlock, NULL ); pthread_t wtids[3 ], rtids[5 ]; for (int i = 0 ; i < 3 ; i++) { pthread_create(&wtids[i], NULL , writeNum, NULL ); } for (int i = 0 ; i < 5 ; i++) { pthread_create(&rtids[i], NULL , readNum, NULL ); } for (int i = 0 ; i < 3 ; i++) { pthread_detach(wtids[i]); } for (int i = 0 ; i < 5 ; i++) { pthread_detach(rtids[i]); } pthread_exit(NULL ); pthread_rwlock_destroy(&rwlock); return 0 ; }
编译运行后,可以看到也是正常执行,但是效率会高很多,因为此时读数据不会被阻塞可以并行执行:
6. 生产者消费者模型
如下图,生产者消费者模型,可能存在多个生产者同时生产产品,也可能有多个消费者一起消费产品。
要避免:1)仓库满了,生产者还在生产;2)仓库空了(没有产品了),消费者还在消费产品
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 #include <stdio.h> #include <pthread.h> #include <stdlib.h> #include <unistd.h> pthread_mutex_t mutex;struct Node { int num; struct Node *pre ; }; struct Node *head ;void *producer (void *arg) { while (1 ) { pthread_mutex_lock(&mutex); struct Node *newNode = (struct Node *)malloc (sizeof (struct Node)); newNode->pre = head; head = newNode; newNode->num = rand() % 1000 ; printf ("add node, num : %d, tid : %ld\n" , newNode->num, pthread_self()); pthread_mutex_unlock(&mutex); usleep(100 ); } return NULL ; } void *customer (void *arg) { while (1 ) { pthread_mutex_lock(&mutex); struct Node *tmp = head; head = head->pre; printf ("del node, num : %d, tid : %ld\n" , tmp->num, pthread_self()); free (tmp); pthread_mutex_unlock(&mutex); usleep(100 ); } return NULL ; } int main () { pthread_mutex_init(&mutex, NULL ); pthread_t ptids[5 ], ctids[5 ]; for (int i = 0 ; i < 5 ; i++) { pthread_create(&ptids[i], NULL , producer, NULL ); pthread_create(&ctids[i], NULL , customer, NULL ); } for (int i = 0 ; i < 5 ; i++) { pthread_detach(ptids[i]); pthread_detach(ctids[i]); } while (1 ) { sleep(10 ); } pthread_mutex_destroy(&mutex); pthread_exit(NULL ); return 0 ; }
首先来个有 bug 的代码,温习一下如何使用 core
来查看段错误原因:
编译运行后可以看到,发生段错误:
1 2 3 4 5 6 ulimit -a ulimit -c 1024 sudo service apport stop gcc prodcust.c -o prodcust -pthread -g ./prodcust ls
此时可以看到 core
文件,使用 gdb 调试
1 2 gdb prodcust core-file core
可以看到错误提示告诉我们在 customer 函数中,访问了空指针,这是由于没有产品了,而消费者还试图消费导致的,修改如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 void *customer (void *arg) { while (1 ) { pthread_mutex_lock(&mutex); struct Node *tmp = head; if (head != NULL ) { head = head->pre; printf ("del node, num : %d, tid : %ld\n" , tmp->num, pthread_self()); free (tmp); pthread_mutex_unlock(&mutex); usleep(100 ); } else { pthread_mutex_unlock(&mutex); } } return NULL ; }
编译运行,可以看到产品生产和消费的过程
7. 条件变量
条件变量不是锁,但是可以在满足条件时阻塞线程,或者释放阻塞线程。
◼ 条件变量的类型 pthread_cond_t
◼ int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
◼ int pthread_cond_destroy(pthread_cond_t *cond);
◼ int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
◼ int pthread_cond_timedwait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex, const struct timespec *restrict
abstime);
◼ int pthread_cond_signal(pthread_cond_t *cond);
◼ int pthread_cond_broadcast(pthread_cond_t *cond)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 #include <stdio.h> #include <pthread.h> #include <stdlib.h> #include <unistd.h> pthread_mutex_t mutex;pthread_cond_t cond;struct Node { int num; struct Node *pre ; }; struct Node *head ;void *producer (void *arg) { while (1 ) { pthread_mutex_lock(&mutex); struct Node *newNode = (struct Node *)malloc (sizeof (struct Node)); newNode->pre = head; head = newNode; newNode->num = rand() % 1000 ; printf ("add node, num : %d, tid : %ld\n" , newNode->num, pthread_self()); pthread_cond_signal(&cond); pthread_mutex_unlock(&mutex); usleep(100 ); } return NULL ; } void *customer (void *arg) { pthread_mutex_init(&mutex, NULL ); while (1 ) { pthread_mutex_lock(&mutex); struct Node *tmp = head; if (head != NULL ) { head = head->pre; printf ("del node, num : %d, tid : %ld\n" , tmp->num, pthread_self()); free (tmp); pthread_mutex_unlock(&mutex); usleep(100 ); } else { pthread_cond_wait(&cond, &mutex); pthread_mutex_unlock(&mutex); } } return NULL ; } int main () { pthread_mutex_init(&mutex, NULL ); pthread_cond_init(&cond, NULL ); pthread_t ptids[5 ], ctids[5 ]; for (int i = 0 ; i < 5 ; i++) { pthread_create(&ptids[i], NULL , producer, NULL ); pthread_create(&ctids[i], NULL , customer, NULL ); } for (int i = 0 ; i < 5 ; i++) { pthread_detach(ptids[i]); pthread_detach(ctids[i]); } while (1 ) { sleep(10 ); } pthread_cond_destroy(&cond); pthread_mutex_destroy(&mutex); pthread_exit(NULL ); return 0 ; }
可以在消费者发现没有产品可以消费时阻塞,等待生产者生产产品。
8. 信号量
信号量也可以用于阻塞。
线程安全需要利用互斥量。
◼ 信号量的类型 sem_t
◼ int sem_init(sem_t *sem, int pshared, unsigned int value);
◼ int sem_destroy(sem_t *sem);
◼ int sem_wait(sem_t *sem);
◼ int sem_trywait(sem_t *sem);
◼ int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
◼ int sem_post(sem_t *sem);
◼ int sem_getvalue(sem_t *sem, int *sval);
使用信号量就可以避免无聊的等待(去掉了延时)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 #include <stdio.h> #include <pthread.h> #include <stdlib.h> #include <unistd.h> #include <semaphore.h> pthread_mutex_t mutex;sem_t psem;sem_t csem;struct Node { int num; struct Node *next ; }; struct Node * head = NULL ;void * producer (void * arg) { while (1 ) { sem_wait(&psem); pthread_mutex_lock(&mutex); struct Node * newNode = (struct Node *)malloc (sizeof (struct Node)); newNode->next = head; head = newNode; newNode->num = rand() % 1000 ; printf ("add node, num : %d, tid : %ld\n" , newNode->num, pthread_self()); pthread_mutex_unlock(&mutex); sem_post(&csem); } return NULL ; } void * customer (void * arg) { while (1 ) { sem_wait(&csem); pthread_mutex_lock(&mutex); struct Node * tmp = head; head = head->next; printf ("del node, num : %d, tid : %ld\n" , tmp->num, pthread_self()); free (tmp); pthread_mutex_unlock(&mutex); sem_post(&psem); } return NULL ; } int main () { pthread_mutex_init(&mutex, NULL ); sem_init(&psem, 0 , 8 ); sem_init(&csem, 0 , 0 ); pthread_t ptids[5 ], ctids[5 ]; for (int i = 0 ; i < 5 ; i++) { pthread_create(&ptids[i], NULL , producer, NULL ); pthread_create(&ctids[i], NULL , customer, NULL ); } for (int i = 0 ; i < 5 ; i++) { pthread_detach(ptids[i]); pthread_detach(ctids[i]); } while (1 ) { sleep(10 ); } pthread_mutex_destroy(&mutex); pthread_exit(NULL ); return 0 ; }
可以看到代码正常执行:
头插法,后生成的产品先被消费