Linux 多线程编程
什么是线程?
与线程类似,线程是允许应用程序并发执行多个任务的一种机制
线程是轻量级的进程(LWP:Light Weight Process),在 Linux 环境下线程的本 质仍是进程。
一个进程可以包含多个线程。同一个程序中的所有线程均会独立执行相同程序,且共 享同一份全局内存区域,其中包括初始化数据段、未初始化数据段,以及堆内存段。(传 统意义上的 UNIX 进程只是多线程程序的一个特例,该进程只包含一个线程) ◼ 进程是 CPU 分配资源的最小单位,线程是操作系统调度执行的最小单位。
查看指定进程的 LWP 号:ps –Lf pid
进程与线程的区别?
-
进程间的信息难以共享。除去只读代码段外,父子进程并未共享内存,因此必须采用一些进程间通信方式,在进程间进行信息交换。
-
调用
fork()
来创建进程的相对代价较高,即便利用写时复制等技术,仍然需要复制诸如内存页表和文件描述符之类的进程属性,意味着fork()
在时间开销上依然不菲。 -
线程之间能够快速的,方便的分享信息,只需要将数据复制到(全局数据段或堆)变量即可。
-
创建线程比创建线程要快10倍甚至更多,线程间是共享虚拟地址空间的,无需使用写时拷贝技术复制内存,也无需复制页表。
线程间的共享资源和非共享资源
创建线程
包含头文件
#include<pthread>
函数原型
int pthread_create(pthread_t *__restrict__ __newthread, const pthread_attr_t *__restrict__ __attr, void *(*__start_routine)(void *), void *__restrict__ __arg)
返回值:
成功返回0,失败返回 -1
案例
#include <cstdio> #include <iostream> #include <sys/types.h> #include <unistd.h> #include <pthread.h> #include <thread> using namespace std; void *func(void *arg) { for (int i = 0; i < 4; i++) { cout << "子线程运行中.........." << endl; sleep(3); } return NULL; } int main() { pthread_t th; int ret = pthread_create(&th, NULL, func, NULL); if (ret != 0) { perror("pthread_create"); exit(-1); } int i = 0; while (i < 5) { i++; cout << i << ' '; } }
使用编译命令:
g++ -o main main.cpp -std=c++11 -pthread
使用线程库的时候需要加上-pthread
或-lpthread
这两个 其中一个参数,编译选项选择-std=c++11
不添加编译选项-pthread
,则运行出现Enable multithreading to use std::thread: Operation not permitted
。
终止线程
函数原型
void pthread_exit(void *res)
返回值
无
作用
终止当前任意线程
#include <cstdio> #include <iostream> #include <cstring> #include <pthread.h> #include <thread> using namespace std; void *func(void *arg) { printf("子线程 id = %d \n", pthread_self()); return NULL; } int main() { pthread_t th; // 创建一个子线程 int ret = pthread_create(&th, NULL, func, NULL); if (ret != 0) { perror("pthread_create"); exit(-1); } for (int i = 0; i < 10; i++) cout << i << endl; printf("主线程 id = %d \n", pthread_self()); pthread_exit(NULL); // 让主线程退出,不会影响其他运行的线程 return 0; }
输出
连接已终止的线程
函数原型:
int pthread_join(pthread_t __th, void **__thread_return) /* 参数 需要回收的子线程id 接受子线程回收时的返回值 */
返回值:
成功返回0,失败返回 error
功能:
和一个已终止的线程进行连接,回收子进程的资源;该函数是阻塞函数,调用一次只能回收一次资源,一般在主线程中使用。
案例
pthread.cpp
#include <cstdio> #include <iostream> #include <cstring> #include <pthread.h> #include <thread> #include<unistd.h> using namespace std; void *func(void *arg) { printf("子线程 id = %d \n", pthread_self()); sleep(2); return NULL; } int main() { pthread_t th; // 创建一个线程 int ret = pthread_create(&th, NULL, func, NULL); if (ret != 0) { perror("pthread_create"); exit(-1); } // 主线程调用pthread_join回收线程资源 ret = pthread_join(th,NULL); printf("ret = %d\n",ret); cout<<"回收子线程资源成功"<<endl; printf("主线程 id = %d \n", pthread_self()); pthread_exit(NULL); // 让主线程退出,不会影响其他运行的线程 return 0; }
输出
[chen@VM-24-13-centos test]$ g++ -o main main.cpp -std=c++11 -pthread [chen@VM-24-13-centos test]$ ./main 子线程 id = 2088679168 ret = 0 回收子线程资源成功 主线程 id = 2105562944
线程分离
函数原型
int pthread_detach(pthread_t pthread) /* 参数 : 需要分离的id */
返回值
成功返回0 ,失败返回 error
功能
分离一个线程,被分离的线程在终止的时候会自动释放资源给系统
不能多次分离一个线程 会产生不可预估的错误
不可以去连接一个自动分离的线程
案例
#include <cstdio> #include <iostream> #include <cstring> #include <pthread.h> #include <thread> #include <unistd.h> using namespace std; void *func(void *arg) { printf("chiad thread id = %d \n", pthread_self()); sleep(2); return NULL; } int main() { pthread_t th; int ret = pthread_create(&th, NULL, func, NULL); if (ret != 0) { perror("pthread_create"); exit(-1); } // 输出子线程和主线程的id printf("th id = %ld thread id = %ld\n", th, pthread_self()); pthread_detach(th); printf("子线程已分离!\n"); pthread_exit(NULL); // 设置子线程分离 }
输出
[chen@VM-24-13-centos test]$ g++ -o main main.cpp -std=c++11 -pthread [chen@VM-24-13-centos test]$ ./main th id = 140585734616832 thread id = 140585751500608 子线程已分离! chiad thread id = -1429883136
线程取消
函数原型
int pthread_cancel(pthread_t __th)// 函数参数:需要终止线程运行的线程 id
返回值:
成功返回0,失败返回 error
案例 :终止一个线程的运行
#include <cstdio> #include <iostream> #include <cstring> #include <pthread.h> #include <thread> #include <unistd.h> using namespace std; void *func(void *arg) { printf("chiad thread id = %d \n", pthread_self()); for (int i = 0; i < 6; i++) printf("chaid i = %d \n", i); return NULL; } int main() { pthread_t th; int ret = pthread_create(&th, NULL, func, NULL); if (ret != 0) { perror("pthread_create"); exit(-1); } pthread_cancel(th); printf("线程已取消!\n"); for (int i = 0; i < 5; i++) cout << i << endl; printf("chrid id = %d mian pthread id = %d\n", th, pthread_self()); }
线程属性操作函数
PSIOX标准中,线程属性使用pthread_arrt_t类型的变量表示
#include<pthread.h> pthread_arrt_t st;
pthread_arrt_t
是一个结构体,当中包括了线程的各种信息
#include <pthread.h> typedef struct __pthread_attr_s { int __detachstate; // 线程的分离状态 int __schedpolicy; // 线程调度策略 structsched_param __schedparam; // 线程的调度参数 int __inheritsched; // 线程的继承性 int __scope; // 线程的作用域 size_t __guardsize; // 线程栈末尾的警戒缓冲区大小 int __stackaddr_set; // 线程的栈设置 void* __stackaddr; // 线程栈的位置 size_t __stacksize; // 线程栈的大小 } pthread_attr_t; int pthread_attr_init(pthread_attr_t *attr); int pthread_attr_destroy(pthread_attr_t *attr);
接下来,我们将从中挑选出几个常用的属性,讲解它们的功能以及修改的方法。
(1) __detachstate
我们知道,默认属性的线程在执行完目标函数后,占用的私有资源并不会立即释放,要么执行完 pthread_join() 函数后释放,要么整个进程执行结束后释放。某些场景中,我们并不需要接收线程执行结束后的返回值,如果想让线程执行完后立即释放占用的私有资源,就可以通过修改 __detachstate 属性值来实现。
__detachstate 属性值用于指定线程终止执行的时机,该属性的值有两个,分别是:
-
PTHREAD_CREATE_JOINABLE(默认值):线程执行完函数后不会自行释放资源;
-
PTHREAD_CREATE_DETACHED:线程执行完函数后,会自行终止并释放占用的资源。
关于 __detachstate 属性,<pthread.h> 头文件中提供了 2 个与它相关的函数,分别是:
int pthread_attr_getdetachstate(const pthread_attr_t * attr,int * detachstate); int pthread_attr_setdetachstate(pthread_attr_t *sttr,int detachstate);
pthread_attr_getdetachstate() 函数用于获取 detachstate 属性的值,detachstate 指针用于接收 detachstate 属性的值;pthread_attr_setdetachstate() 函数用于修改 detachstate 属性的值,detachstate 整形变量即为新的 detachstate 属性值。两个函数执行成功时返回数字 0,反之返回非零数。
此外,<pthread.h> 头文件还提供有 pthread_detach() 函数,可以直接将目标线程的 __detachstate 属性改为 PTHREAD_CREATE_DETACHED,语法格式如下:
int pthread_detach(pthread_t thread);
函数执行成功时返回数字 0 ,反之返回非零数。
(2) __schedpolicy
__schedpolicy 属性用于指定系统调度该线程所用的算法,它的值有以下 3 个:
-
SCHED_OTHER(默认值):分时调度算法;
-
SCHED_FIFO:先到先得(实时调度)算法;
-
SCHED_RR:轮转法;
其中,SCHED_OTHER 调度算法不支持为线程设置优先级,而另外两种调度算法支持。
<pthread.h> 头文件提供了如下两个函数,专门用于访问和修改 __schedpolicy 属性:
int pthread_attr_getschedpolicy(const pthread_attr_t *, int * policy) int pthread_attr_setschedpolicy(pthread_attr_*, int policy)
pthread_attr_getschedpolicy() 函数用于获取当前 schedpolicy 属性的值;pthread_attr_setschedpolicy() 函数用于修改 schedpolicy 属性的值。函数执行成功时,返回值为数字 0,反之返回非零数。
(3) __schedparam
scheparam 用于设置线程的优先级(默认值为 0),该属性仅当线程的 schedpolicy 属性为 SCHED_FIFO 或者 SCHED_RR 时才能发挥作用。
<pthread.h> 头文件中提供了如下两个函数,用于获取和修改 __schedparam 属性的值:
int pthread_attr_getschedparam(const pthread_attr_t *attr, struct sched_param *param); int pthread_attr_setschedparam(pthread_attr_t *attr, const struct sched_param *param);
其中,param 参数用于接收或者修改 __scheparam 属性的优先级,它是 sched_param 结构体类型的变量,定义在 <sched.h> 头文件中,内部仅有一个 sched_priority 整形变量,用于表示线程的优先级。函数执行成功时返回数字 0,反之返回非零数。
当需要修改线程的优先级时,我们只需创建一个 sched_param 类型的变量并为其内部的 sched_priority 成员赋值,然后将其传递给 pthrerd_attr_setschedparam() 函数。
不同的操作系统,线程优先级的值的范围不同,您可以通过调用如下两个系统函数获得当前系统支持的最大和最小优先级的值:
int sched_get_priority_max(int policy); //获得最大优先级的值 int sched_get_priority_min(int policy); //获得最小优先级的值
其中,policy 的值可以为 SCHED_FIFO、SCHED_RR 或者 SCHED_OTHER,当 policy 的值为 SCHED_OTHER 时,最大和最小优先级的值都为 0。
(4) __inheritsched
新建线程的调度属性(schedpolicy 和 schedparam 属性)默认遵循父线程的属性(谁创建它,谁就是它的父线程),如果我们想自定义线程的调度属性,就需要借助 inheritsched 属性。
也就是说,新线程的调度属性要么遵循父线程,要么遵循 myAttr 规定的属性,默认情况下 inheritsched 规定新线程的调度属性遵循父线程,我们也可以修改 inheritsched 的值,使新线程的调度属性遵循自定义的属性变量(如文章开头定义的 myAttr)规定的值。
<pthread.h> 头文件提供了如下两个函数,分别用于获取和修改 __inheritsched 属性的值:
//获取 __inheritsched 属性的值 int pthread_attr_getinheritsched(const pthread_attr_t *attr,int *inheritsched); //修改 __inheritsched 属性的值 int pthread_attr_setinheritsched(pthread_attr_t *attr,int inheritsched);
其中在 pthread_attr_setinheritsched() 函数中,inheritsched 参数的可选值有两个,分别是:
-
PTHREAD_INHERIT_SCHED(默认值):新线程的调度属性继承自父线程;
-
PTHREAD_EXPLICIT_SCHED:新线程的调度属性继承自 myAttr 规定的值。
以上两个函数执行成功时返回数字 0,反之返回非零数。
(5) __scope
线程执行过程中,可以只和同进程内的其它线程争夺 CPU 资源,也可以和系统中所有的其它线程争夺 CPU 资源,__scope 属性用于指定目标线程和哪些线程抢夺 CPU 资源。
<pthread.h> 头文件中提供了如下两个函数,分别用于获取和修改 __scope 属性的值:
//获取 __scope 属性的值 int pthread_attr_getscope(const pthread_attr_t * attr,int * scope); //修改 __scope 属性的值 int pthread_attr_setscope(pthread_attr_t * attr,int * scope);
当调用 pthread_attr_setscope() 函数时,scope 参数的可选值有两个,分别是:
-
PTHREAD_SCOPE_PROCESS:同一进程内争夺 CPU 资源;
-
PTHREAD_SCOPE_SYSTEM:系统所有线程之间争夺 CPU 资源。
Linux系统仅支持 PTHREAD_SCOPE_SYSTEM,即所有线程之间争夺 CPU 资源。
当函数执行成功时,返回值为数字 0,反之返回非零数。
(6) __stacksize
每个线程都有属于自己的内存空间,通常称为栈(有时也称堆栈、栈空间、栈内存等)。某些场景中,线程执行可能需要较大的栈内存,此时就需要我们自定义线程拥有的栈的大小。
__stacksize 属性用于指定线程所拥有的栈内存的大小。<pthread.h> 提供有以下两个函数,分别用于获取和修改栈空间的大小:
//获取当前栈内存的大小 int pthread_attr_getstacksize(const pthread_attr_t * attr,size_t * stacksize); //修改栈内存的大小 int pthread_attr_setsstacksize(pthread_attr_t * attr,size_t * stacksize);
函数执行成功时,返回值为数字 0,反之返回非零数。
(8) __guardsize
每个线程中,栈内存的后面都紧挨着一块空闲的内存空间,我们通常称这块内存为警戒缓冲区,它的功能是:一旦我们使用的栈空间超出了额定值,警戒缓冲区可以确保线程不会因“栈溢出”立刻执行崩溃。
guardsize 属性专门用来设置警戒缓冲区的大小,<pthread.h> 头文件中提供了如下两个函数,分别用于获取和修改 guardsize 属性的值:
int pthread_attr_getguardsize(const pthread_attr_t *restrict attr,size_t *restrict guardsize); int pthread_attr_setguardsize(pthread_attr_t *attr ,size_t *guardsize);
pthread_attr_setguardsize() 函数中,设置警戒缓冲区的大小为参数 guardsize 指定的字节数。函数执行成功时返回数字 0,反之返回非零数。
这是一些设置线程属性的接口
int pthread_attr_init(pthread_attr_t * attr); // 初始化一个线程 int pthread_detach(pthread_t thread); // 设置线程分离设置线程是否和其他线程分离(能否调用pthread_join()回收), 运行时可以调用pthread_detach()完成。 int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate); int pthread_attr_getdetachstate(pthread_attr_t *attr, int *detachstate);
案例
#include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <unistd.h> #include <string.h> // 线程主控函数 void *tfn(void *arg) { int n = 3; while (n--) { printf("thread count %d\n", n); sleep(1); } return (void *)1; } int main(void) { pthread_t tid; void *tret; int err; pthread_attr_t attr; // 初始化线程属性 pthread_attr_init(&attr); // 设置线程属性为分离状态 pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); pthread_create(&tid, &attr, tfn, NULL); int state; // 获取线程属性 pthread_attr_getdetachstate(&attr, &state); // 判断线程属性是否为分离 if (state == PTHREAD_CREATE_DETACHED) { puts("pthread is PTHREAD_CREATE_DETACHED"); } // 销毁线程属性所占用的资源 pthread_attr_destroy(&attr); // 此时调用pthread_join会失败 err = pthread_join(tid, &tret); if (err != 0) { // 回收失败 fprintf(stderr, "thread %s\n", strerror(err)); } else { fprintf(stderr, "thread exit code %d\n", (int *)tret); } sleep(4); return 0; }
线程同步
在多线程编程中,我们需要使用多个线程完成某个任务但是需要并发的访问对应的资源达到并发执行任务的效果
-
线程的主要优势在于,能够通过全局变量来共享信息。不过,这种便捷的共享是有代价 的:必须确保多个线程不会同时修改同一变量,或者某一线程不会读取正在由其他线程 修改的变量。
-
临界区是指访问某一共享资源的代码片段,并且这段代码的执行应为原子操作,也就是 同时访问同一共享资源的其他线程不应终端该片段的执行。
-
线程同步:即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进 行操作,直到该线程完成操作,其他线程才能对该内存地址进行操作,而其他线程则处 于等待状态。
案例
#include <cstdio> #include <stdlib.h> #include <pthread.h> #include <unistd.h> #include <string.h> #include <iostream> // 线程主控函数 using namespace std; int N = 100; // 全局变量,所有线程都共享 void *tfn(void *arg) { // 买票逻辑 while (N > 0) { printf("%ld 号窗口 第 %d 票已卖出\n", pthread_self(), N); sleep(1); N--; } return NULL; } int main(void) { pthread_t t1, t2, t3; pthread_create(&t1, NULL, tfn, NULL); pthread_create(&t2, NULL, tfn, NULL); pthread_create(&t3, NULL, tfn, NULL); // 回收子线程的资源 pthread_join(t1, NULL); pthread_join(t2, NULL); pthread_join(t3, NULL); // 设置线程分离 pthread_cancel(t1); pthread_cancel(t2); pthread_cancel(t3); pthread_exit(NULL); // 退出主线程 }
互斥锁
-
为避免线程更新共享变量时出现问题,可以使用互斥量(mutex 是 mutual exclusion 的缩写)来确保同时仅有一个线程可以访问某项共享资源。可以使用互斥量来保证对任意共 享资源的原子访问。
-
互斥量有两种状态:已锁定(locked)和未锁定(unlocked)。任何时候,至多只有一 个线程可以锁定该互斥量。试图对已经锁定的某一互斥量再次加锁,将可能阻塞线程或者报 错失败,具体取决于加锁时使用的方法。
-
一旦线程锁定互斥量,随即成为该互斥量的所有者,只有所有者才能给互斥量解锁。一般情 况下,对每一共享资源(可能由多个相关变量组成)会使用不同的互斥量,
-
每一线程在访问 同一资源时将采用如下协议:
-
-
针对共享资源锁定互斥量
-
访问共享资源
-
对互斥量解锁
-
互斥量的类型
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); // 线程解锁
案例
#include <cstdio> #include <stdlib.h> #include <pthread.h> #include <unistd.h> #include <string.h> #include <iostream> // 线程主控函数 using namespace std; int N = 1000; // 全局变量,所有线程都共享 pthread_mutex_t st; void *tfn(void *arg) { // 买票逻辑 while (1) { pthread_mutex_lock(&st); // 线程加锁 if (N > 0) { printf("%ld 号窗口 第 %d 票已卖出\n", pthread_self(), N); } else { pthread_mutex_unlock(&st); // 线程解锁 break; } pthread_mutex_unlock(&st); // 线程解锁 N--; } return NULL; } int main(void) { pthread_t t1, t2, t3; pthread_mutex_init(&st, NULL); pthread_create(&t1, NULL, tfn, NULL); pthread_create(&t2, NULL, tfn, NULL); pthread_create(&t3, NULL, tfn, NULL); // 回收子线程的资源 pthread_join(t1, NULL); pthread_join(t2, NULL); pthread_join(t3, NULL); // 释放互斥量 pthread_mutex_destroy(&st); pthread_exit(NULL); // 退出主线程 }
线程死锁
有时一个线程需要访问一个或者多个线程资源的时候,而每个资源都由不同的互斥量管理,当超过一个线程加锁同一组互斥量的时候,就容易出现死锁的情况。
当两个或两个以上线程在执行过程中,在争夺共享资源的时候造成的一种互相等待的情况,如果不人为干预,任何一方都无法继续执行下去,此时称系统产生了死锁或者说系统处于死锁状态。
死锁发生的原因?
线程未及时释放锁
重复加锁
多线程多锁,抢占锁资源
案例
#include <cstdio> #include <stdlib.h> #include <pthread.h> #include <unistd.h> #include <string.h> #include <iostream> // 线程主控函数 using namespace std; // 创建2个互斥量 pthread_mutex_t mutex1, mutex2; void *workA(void *arg) { pthread_mutex_lock(&mutex1); sleep(1); pthread_mutex_lock(&mutex2); printf("workA....\n"); pthread_mutex_unlock(&mutex2); pthread_mutex_unlock(&mutex1); return NULL; } void *workB(void *arg) { pthread_mutex_lock(&mutex2); sleep(1); pthread_mutex_lock(&mutex1); printf("workB....\n"); pthread_mutex_unlock(&mutex1); pthread_mutex_unlock(&mutex2); return NULL; } int main() { // 初始化互斥量 pthread_mutex_init(&mutex1, NULL); pthread_mutex_init(&mutex2, NULL); // 创建2个子线程 pthread_t tid1, tid2; pthread_create(&tid1, NULL, workA, NULL); pthread_create(&tid2, NULL, workB, NULL); // 回收子线程资源 pthread_join(tid1, NULL); pthread_join(tid2, NULL); // 释放互斥量资源 pthread_mutex_destroy(&mutex1); pthread_mutex_destroy(&mutex2); return 0; }
读写锁
-
读写锁的类型
-
当有一个线程已经持有互斥锁时,互斥锁将所有试图进入临界区的线程都阻塞住。但是考 虑一种情形,当前持有互斥锁的线程只是要读访问共享资源,而同时有其它几个线程也想 读取这个共享资源,但是由于互斥锁的排它性,所有其它线程都无法获取锁,也就无法读 访问共享资源了,但是实际上多个线程同时读访问共享资源并不会导致问题。
-
在对数据的读写操作中,更多的是读操作,写操作较少,例如对数据库数据的读写应用。 为了满足当前能够允许多个读出,但只允许一个写入的需求,线程提供了读写锁来实现。
-
读写锁的特点:- 如果有其它线程读数据,则允许其它线程执行读操作,但不允许写操作。 如果有其它线程写数据,则其它线程都不允许读、写操作。 写是独占的,写的优先级高.
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)
案例
#include <cstdio> #include <iostream> #include <pthread.h> #include <unistd.h> using namespace std; /* 三个线程不定时写一个全局变量 五个线程不定时的读全局变量 */ // 共享资源 int num = 1; pthread_rwlock_t rwlock; // 读写锁 pthread_mutex_t mutex; void *Wnum(void *arg) { while (true) { pthread_rwlock_rdlock(&rwlock); ++num; printf("write : ++num = %d tid = %ld\n", num, pthread_self()); usleep(100); pthread_mutex_unlock(&mutex); } return NULL; } void *Rnum(void *arg) { while (true) { pthread_rwlock_rdlock(&rwlock); ++num; printf("read : ++num = %d tid = %ld\n", num, pthread_self()); usleep(100); pthread_mutex_unlock(&mutex); } return NULL; } pthread_mutex_t mux; // 全局互斥量 int main() { // 五个读线程 三个写线程 pthread_t w[3], r[5]; pthread_rwlock_init(&rwlock, NULL); for (int i = 0; i < 3; i++) { pthread_create(&w[i], NULL, Wnum, NULL); } for (int i = 0; i < 5; i++) { pthread_create(&r[i], NULL, Rnum, NULL); } // 设置线程分离 for (int i = 0; i < 3; i++) { pthread_detach(w[i]); } for (int i = 0; i < 5; i++) { pthread_detach(r[i]); } pthread_rwlock_destroy(&rwlock); pthread_exit(NULL); }
输出
可以看到线程有序的进行读写,而不影响效率,而且读写锁比互斥锁的效率高
来段代码对互斥锁和读写锁效率的测试
#include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <sys/time.h> #include <unistd.h> // 创建互斥锁 pthread_mutex_t mutex; // 创建读写锁 pthread_rwlock_t rwlock; // 全局变量,父子进程、线程间都是相同的 int data = 123456; // 读取次数 long count = 10000000; // 互斥读函数 void *mfunc(void *arg) { int read = 0; for (long i = 0; i < count; ++i) { // 加锁 pthread_mutex_lock(&mutex); read = data; // 解锁 pthread_mutex_unlock(&mutex); } // 结束线程 pthread_exit(NULL); } // 读写锁读 void *rwfunc(void *arg) { int read = 0; for (long i = 0; i < count; ++i) { // 加锁 pthread_rwlock_rdlock(&rwlock); read = data; // 解锁 pthread_rwlock_unlock(&rwlock); } // 结束线程 pthread_exit(NULL); } int main() { // 初始化互斥锁 pthread_mutex_init(&mutex, NULL); // 创建两个进程,父进程中使用互斥锁读取,子进程使用读写锁读取 int pid = fork(); if (pid > 0) { // parent process // 创建十个线程,并开始计时 pthread_t mtids[10]; struct timeval start; gettimeofday(&start, NULL); for (int i = 0; i < 10; ++i) { pthread_create(&mtids[i], NULL, mfunc, NULL); } // 在主线程中,调用join函数,回收线程,线程回收完成后,结束计时 for (int i = 0; i < 10; ++i) { pthread_join(mtids[i], NULL); } struct timeval end; gettimeofday(&end, NULL); long timediff = (end.tv_sec - start.tv_sec) * 1000000 + end.tv_usec - start.tv_usec; printf("互斥锁读全部线程执行完毕,总耗时: %ld us\n", timediff); // 回收子进程 wait(NULL); } else if (pid == 0) { // child process // 初始化读写锁 pthread_rwlock_init(&rwlock, NULL); // 创建十个线程,并开始计时 pthread_t rwtids[10]; struct timeval start; gettimeofday(&start, NULL); for (int i = 0; i < 10; ++i) { pthread_create(&rwtids[i], NULL, rwfunc, NULL); } // 在主线程中,调用join函数,回收线程,线程回收完成后,结束计时 for (int i = 0; i < 10; ++i) { pthread_join(rwtids[i], NULL); } struct timeval end; gettimeofday(&end, NULL); long timediff = (end.tv_sec - start.tv_sec) * 1000000 + end.tv_usec - start.tv_usec; printf("读写锁读全部线程执行完毕,总耗时: %ld us\n", timediff); // 结束子进程 exit(0); } return 0; }
输出
总结:当需要使用多个线程对共享资源进行读写操作时,使用读写锁效率更高
C++中的读写锁(ReadWrite Lock)是一种多线程同步机制,它具有以下特性:
-
读写互斥:读写锁允许多个线程同时读取共享数据,但只允许一个线程进行写操作。这意味着多个线程可以同时获取读锁,但写锁只能被单独的一个线程获取。
-
写写互斥:写锁之间是互斥的,即同时只能有一个线程持有写锁执行写操作。
-
读写互斥:如果有线程持有读锁,则写锁必须等待,直到所有读锁被释放。
-
写读互斥:如果有线程持有写锁,则读锁必须等待,直到写锁被释放。
相比于互斥锁,读写锁具有更高的效率,原因如下:
-
并发读取:读写锁允许多个线程同时读取共享数据,这对于读取密集型的操作非常有利。互斥锁则会导致读取操作串行化,效率较低。
-
写操作互斥:写锁保证了写操作的互斥性,避免了数据竞争。而互斥锁会导致所有的读写操作都变得串行化,降低了效率。
因此,读写锁在读取频繁、写入相对较少的场景中比互斥锁的效率高一倍。但需要注意的是,如果写入操作非常频繁,读写锁的性能优势可能会减弱,因为写锁会阻塞其他的读和写请求。
生产者消费者模型
生产者消费者模型是一种并发编程中常用的设计模式。它用于解决多个线程之间的协作问题,其中生产者负责生成数据并将其放入一个共享的缓冲区,而消费者则负责从缓冲区中取出数据进行处理或消费。
该模型的主要目的是控制生产者和消费者之间的数据传递,确保线程之间的同步和协调。它通过引入一个共享的缓冲区作为生产者和消费者之间的交互介质,实现了线程之间的解耦和资源的合理利用。
生产者和消费者模型的典型实现涉及以下几个关键操作:
-
生产者将数据放入缓冲区,如果缓冲区已满,则生产者将等待直到有空间可用。
-
消费者从缓冲区中取出数据进行处理,如果缓冲区为空,则消费者将等待直到有数据可用。
-
生产者和消费者都需要正确地处理缓冲区的状态变化和互斥访问,以避免竞态条件和死锁等并发问题。
生产者消费者模型在处理数据流和任务调度等场景中非常有用,可以有效提高系统的性能和资源利用率。
/* 生产者消费者模型(粗略的版本) */ #include <stdio.h> #include <pthread.h> #include <stdlib.h> #include <unistd.h> // 创建一个互斥量 pthread_mutex_t mutex; struct Node{ int num; struct Node *next; }; // 头结点 struct Node * head = NULL; void * producer(void * arg) { // 不断的创建新的节点,添加到链表中 while(1) { 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); usleep(100); } return NULL; } void * customer(void * arg) { while(1) { pthread_mutex_lock(&mutex); // 保存头结点的指针 struct Node * tmp = head; // 判断是否有数据 if(head != NULL) { // 有数据 head = head->next; 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; } int main() { pthread_mutex_init(&mutex, NULL); // 创建5个生产者线程,和5个消费者线程 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; }
条件变量
条件变量的类型 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); // H唤醒一个或者多个正在等待的线程 int pthread_cond_broadcast(pthread_cond_t *cond); // 唤醒所有正在等待的线程
案例
#include <pthread.h> #include <stdio.h> pthread_mutex_t mutex; // 互斥锁 pthread_cond_t cond; // 条件变量 int shared_data = 0; // 共享数据 void *thread1(void *arg) { pthread_mutex_lock(&mutex); printf("Thread 1 is running\n"); // 等待条件变量满足,如果条件不满足则阻塞并释放互斥锁 while (shared_data < 10) { pthread_cond_wait(&cond, &mutex); } printf("Thread 1 received signal, shared_data = %d\n", shared_data); pthread_mutex_unlock(&mutex); pthread_exit(NULL); } void *thread2(void *arg) { pthread_mutex_lock(&mutex); printf("Thread 2 is running\n"); // 修改共享数据 shared_data += 10; printf("Thread 2 sends signal, shared_data = %d\n", shared_data); // 发送信号通知等待该条件变量的线程 pthread_cond_signal(&cond); pthread_mutex_unlock(&mutex); pthread_exit(NULL); } int main() { pthread_mutex_init(&mutex, NULL); pthread_cond_init(&cond, NULL); pthread_t tid1, tid2; pthread_create(&tid1, NULL, thread1, NULL); pthread_create(&tid2, NULL, thread2, NULL); pthread_join(tid1, NULL); pthread_join(tid2, NULL); pthread_mutex_destroy(&mutex); pthread_cond_destroy(&cond); return 0; }
在这个案例中,我们创建了两个线程(thread1和thread2)。thread1首先获得互斥锁,然后调用pthread_cond_wait函数等待条件满足。thread2获取互斥锁后,执行一些工作后满足条件,并通过pthread_cond_signal发送一个信号通知thread1。thread1被唤醒后继续执行,并输出相应的信息。 在这个案例中,条件变量起到了线程间同步的作用,使得线程能够等待某个条件变为真,并在条件满足时被唤醒。
输出
总结:为了线程安全尽可能的使用条件变量
信号量
C++线程信号量(semaphore)是一种同步机制,用于协调多个线程之间的并发操作。它可以用于控制对临界资源的访问,确保线程以特定的顺序执行,并防止竞态条件(race condition)的发生。
信号量通过维护一个内部计数器来实现其功能。当计数器的值大于零时,线程可以继续执行;当计数器的值为零时,线程需要等待。当一个线程使用完临界资源时,它会释放信号量,使得其他等待的线程可以继续执行。
要使用信号量,可以按照以下步骤进行:
-
包含头文件
<semaphore>
。 -
创建一个信号量对象,可以是全局变量或局部变量。例如:
std::semaphore mySemaphore;
-
在需要对临界资源进行访问的代码段之前,使用
mySemaphore.acquire()
来申请获取信号量。 -
在使用完临界资源后,使用
mySemaphore.release()
来释放信号量,以允许其他线程获取资源。
需要注意的是,信号量的初始值和每次请求和释放的数量都可以根据具体需求进行调整。此外,C++标准库还提供了其他类型的信号量,如计时器信号量(std::counting_semaphore
)和二进制信号量(std::binary_semaphore
),可根据实际情况选择使用。
信号量的类型 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) //
案例
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <pthread.h> #include <semaphore.h> // 定义信号量变量 sem_t sem; void *func1(void *arg) { int i = 0; // 申请资源 将可用资源减1 sem_wait(&sem); for (i = 'A'; i <= 'Z'; i++) { putchar(i); fflush(stdout); usleep(10000); // 100ms } // 释放资源 将可用资源加1 sem_post(&sem); return NULL; } void *func2(void *arg) { int i = 0; // 申请资源 将可用资源减1 sem_wait(&sem); for (i = 'a'; i <= 'z'; i++) { putchar(i); fflush(stdout); usleep(10000); // 100ms } // 释放资源 将可用资源加1 sem_post(&sem); return NULL; } int main() { pthread_t tid1, tid2; // 初始化信号量 sem_init(&sem, 0, 1); // 创建线程 pthread_create(&tid1, NULL, func1, NULL); pthread_create(&tid2, NULL, func2, NULL); // 等待线程结束 pthread_join(tid1, NULL); pthread_join(tid2, NULL); printf("\nmain exit...\n"); // 销毁信号量 sem_destroy(&sem); return 0; }
g++ -o main main.cpp -std=c++11 -pthread
使用信号量实现生产者消费者模型
#include <pthread.h> #include <stdio.h> #include <string.h> #include <unistd.h> #include <sys/types.h> #include <time.h> #include <sys/time.h> #include <sys/wait.h> #include <stdlib.h> #include <semaphore.h> //缓冲区 int *buf; int bufSize = 100; int bufPtr; int count; //三个信号量 sem_t full, empty, mutex; //生产者线程 void *producer(void *arg) { while (bufPtr < bufSize) { //信号量模型 sem_wait(&full); sem_wait(&mutex); buf[++bufPtr] = bufPtr; sem_post(&mutex); sem_post(&empty); } } //消费者线程 void *consumer(void *arg) { while (1) { //信号量模型 sem_wait(&empty); sem_wait(&mutex); count = (count + 1) % __INT32_MAX__; printf("pid[%ld], count[%d], data[%d]\n", pthread_self(), count, buf[bufPtr--]); sem_post(&mutex); sem_post(&full); } } int main() { //初始化三个信号量 sem_init(&full, 0, bufSize); sem_init(&empty, 0, 0); sem_init(&mutex, 0, 1); //初始化读写指针、缓冲区 bufPtr = -1; count = 0; buf = (int *)malloc(sizeof(int) * bufSize); //创建6个线程,一个作生产者,5个消费者 pthread_t ppid, cpids[5]; pthread_create(&ppid, NULL, producer, NULL); for (int i = 0; i < 5; ++i) { pthread_create(&cpids[i], NULL, consumer, NULL); } //detach分离,线程自动回收资源 pthread_detach(ppid); for (int i = 0; i < 5; ++i) { pthread_detach(cpids[i]); } //主线程结束 pthread_exit(NULL); return 0; }
g++ -o main main.cpp -std=c++11 -pthread
完结撒花!