文章目录
在介绍多线程编程前,还是一如既往的闲扯一会,
昨晚我终于意识到思维导图以及流程图的重要性,但是对于学生党的我应该是没有闲钱买付费软件,经过小黑在Internet的遨游,终于找到了两款又免费又好用的软件,推荐给大家,接下来的博客我尽量都用思维导图以及流程图呈现知识体系以及代码流程,一款是爱莫脑图(画思维导图,也可以画流程图,但是流程图需要付费),还有一款就是著名的yed(流程图), OK接下来进入今天的正题。
1, 多线程简单介绍
1.1 线程回顾
徐小黑之前有一篇博客就是关于进程和线程的区别,有兴趣的可以看一下。
https://blog.csdn.net/weixin_46027505/article/details/104812719
在操作系统原理的术语中,线程是进程的一条执行路径。线程在Unix系统下,通常被称为轻量级的进程,
-
所有的线程都是在同一进程空间运行,这也意味着多条线程将共享该进程中的全部系统资源,如虚拟地址空间,文件描述符和信号处理等等。
-
但同一进程中的多个线程有各自的调用栈(call stack),自己的寄存器环境 (register context),自己的线程本地存储(thread-local storage)。 一个进程可以有很多线程,每条线程并行执行不同的任务。
-
典型的UNIX进程可以看成只有一个主线程: 一个进程在某一时刻只能做一件事。
有了多线程之后,我们可以让一个进程同一时刻做不止一件事,每个线程处理各自独立的任务。
- 一个进程创建后,会首先生成一个缺省的线程,通常称这个线程为主线程(或称控制线程),C/C++程序中,主线程就是通过 main函数进入的线程,由主线程调用pthread_create()创建的线程称为子线程,子线程也可以有自己的入口函数,该函数由用户 在创建的时候指定。每个线程都有自己的线程ID,可以通过pthread_self()函数获取。最常见的线程模型中,除主线程较为特殊之外,其他线程一旦被创建,相互之间就是对等关系,不存在隐含的层次关系。每个进程可创建的最大线程数由具体实现决定。
1.2 多线程的优点
线程可以提高应用程序在多核环境下处理诸如文件I/O或者socket I/O等会产生堵塞的情况的表现性能。在Unix系统中,一个 进程包含很多东西,包括可执行程序以及一大堆的诸如文件描述符地址空间等资源。在很多情况下,完成相关任务的不同代码间 需要交换数据。如果采用多进程的方式,进程的创建所花的时间片要比线程大些,另外进程间的通信比较麻烦,需要在用户空间 和内核空间进行频繁的切换,开销很大。但是如果使用多线程的方式,因为可以使用共享的全局变量,所以线程间的通信(数据交换)变得非常高效。
2, 主线程和子线程的关系
在介绍多线程编程之前, 必须了解主线程和子线程的关系。
无论在windows中还是Posix中,主线程和子线程的默认关系是:无论子线程执行完毕与否,一旦主线程执行完毕退出,所有 子线程执行都会终止。
如果不设置过,整个进程结束或僵死,部分线程保持一种终止执行但还未销毁的状态,而进程必须在其所有线程销毁 后销毁,这时进程处于僵死状态。线程函数执行完毕退出,或以其他非常方式终止,线程进入终止态,但是为线程分配的系统资 源不一定释放,可能在系统重启之前,一直都不能释放,终止态的线程仍旧作为一个线程实体存在于操作系统中,什么时候销毁,取决于线程属性。在这种情况下,主线程和子线程通常定义以下两种关系:
-
可会合(joinable):这种关系下,主线程需要明确执行等待操作,在子线程结束后,主线程的等待操作执行完毕,子线 程和主线程会合,这时主线程继续执行等待操作之后的下一步操作。主线程必须会合可会合的子线程。在主线程的线程函 数内部调用子线程对象的wait函数实现,即使子线程能够在主线程之前执行完毕,进入终止态,也必须执行会合操作,否 则,系统永远不会主动销毁线程,分配给该线程的系统资源也永远不会释放。
-
相分离(detached):表示子线程无需和主线程会合,也就是相分离的,这种情况下,子线程一旦进入终止状态,这种 方式常用在线程数较多的情况下,有时让主线程逐个等待子线程结束,或者让主线程安排每个子线程结束的等待顺序,是 很困难或不可能的,所以在并发子线程较多的情况下,这种方式也会经常使用。
线程的分离状态决定一个线程以什么样的方式来终止自己,在默认的情况下,线程是非分离状态的,这种情况下,原有的线程 等待创建的线程结束,只有当pthread_join函数返回时,创建的线程才算终止,释放自己占用的系统资源,而分离线程没有被其他的线程所等待,自己运行结束了,线程也就终止了,马上释放系统资源。
- 至于如何设置线程为分离态,以及如果线程是会合态,主线程如何设置 接下来很快就会介绍到。
3, 多线程编程
3.1 线程API函数知识体系
接下来我们先介绍前三个分支,第四个互斥锁的API函数在介绍完锁的概念后在简述
3.2 基本函数
3.2.1 pthread_creat()
这个函数是多进程编程的关键,所以先介绍这个星号函数。
创建一个线程,该线程将执行start_routine执行的函数,arg为传给该函数的参数。新创建的线程ID通过指针tidp传给主线程,attr指定新创建线程的属性。
#include <pthread.h>
int pthread_create
(
pthread_t *thread,
const pthread_attr_t *attr,
void *(*start_routine) (void *),
void *arg
);
//编译链接时加上-pthread
说明: pthreand_create()用来创建一个线程,并执行第三个参数start_routine所指向的函数。
- 第一个参数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;
//对于这些属性,我们需要设定的是线程的分离状态,如果有必要也需要修改每个线程的栈大小。相关函数下面会介绍到
- 第三个参数start_routine是一个函数指针,它指向的函数原型是 void *func(void *),这是所创建的子线程要执行的任务 (函数);
- 第四个参数arg就是传给了所调用的函数的参数,如果有多个参数需要传递给子线程则需要封装到一个结构体里传进去;
例如: pthread_create(&tid, &thread_attr, thread_worker, &value); 其中第四是传给thread_worker函数的参数
3.2.2 其他三个函数
因为剩下的几个函数较简单,就直接在接下来的示例代码介绍,具体功能上面的知识体系大图中已经提到。
主要功能就是设置creat()函数的第二个参数线程属性。
3.3 默认会合态处理API函数
每个线程创建后默认是joinable 状态,该状态需要主线程调用 pthread_join() 等待它退出,否则子线程在结束时,内存资源不能得到释放造成内存泄漏。
pthread_join(tid, NULL);
//其中pthread_t tid;是pthread_t类型的线程号
3.4 设置线程分离态API函数
我们创建线程时一般会将线程设置为分离状态,具体有两种方法:
-
- 线程里面调用 pthread_detach(pthread_self()) 这个方法最简单
-
- 将线程属性变量设置成PTHREAD_CREATE_DETACHED
pthread_attr_setdetachstate(&thread_attr, PTHREAD_CREATE_DETACHED);
//其中 thread_attr是定义的pthread_attr_t 类型的线程属性变量。
- 将线程属性变量设置成PTHREAD_CREATE_DETACHED
4, 多线程锁概念及锁API函数
4.1 多线程锁概念
因为关于线程互斥锁这块的概念相对多,也适用于作为单个知识点记忆,所以我另外写一篇博客讲述。
https://blog.csdn.net/weixin_46027505/article/details/104991860
4.2 互斥锁API函数
-
互斥锁在使用之前,需要先调用 pthread_mutex_init() 函数来初始化互斥锁;
-
互斥锁在使用完之后,我们应该调用pthread_mutex_destroy() 将他摧毁释放;
-
调用pthread_mutex_lock() 来申请锁,这里是阻塞锁,如果锁被别的线程持有则该函数不会返回;
-
在访问临界资源(shared_var)完成退出临界区时,我们调用**pthread_mutex_unlock()**来释放锁,这样其他线程才能 再次访问;
-
pthread_mutex_trylock() 来申请非阻塞锁;如果锁现在被别的线程占用则返回非0值,如果没有被占用则返回0;
5, 无锁的线程示例代码
5.1 默认会合态代码(单线程)
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
void *thread_worker1(void *args);
int main(int argc, char *argv[])
{
int value = 100;
pthread_t tid;
pthread_create(&tid, NULL, thread_worker1, &value);
pthread_join(tid, NULL); //主线程会阻塞在这里,直到子线程退出,才会运行后面的。
while (1)
{
printf("MAIN THREAD START RUN\n");
printf("main thread control %d:\n", value);
sleep(2);
if(value == 110)
{
break;
}
}
printf("main thread start exit\n");
return 0;
}
void *thread_worker1(void *args)
{
int *ptr = (int *)args;
printf("son thread [%ld] start run\n",pthread_self());
while (1)
{
printf("1111111 %s before, value++: %d\n", __FUNCTION__, *ptr);
*ptr += 1;
sleep(2);
printf("1111111 %s after, value++: %d\n", __FUNCTION__, *ptr);
if( *ptr == 105)
{
break;
}
}
printf("son pthread start exit\n");
return NULL;
}
子线程将value的值加1,当加到105,子线程就会退出。然后阻塞在join()等待子线程退出的主线程就会开始运行,但是因为子进程退出了,value值就不会变了,主进程就永远不会退出,就会不断打印105.
5.2 设置成分离态代码(单线程)
- 下面完成上面的代码一样的功能,但是设置了线程的属性,还有设置了分离态。
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
//申明子线程内调用的函数
void *thread_worker2(void *args);
int main(int argc, char *argv[])
{
int value = 100; //定义一个共享变量
pthread_t tid; //定义线程号
pthread_attr_t thread_attr; //定义create()第2个参数:线程属性变量
pthread_attr_init(&thread_attr);
pthread_attr_setstacksize(&thread_attr, 120*1024);
pthread_attr_setdetachstate(&thread_attr, PTHREAD_CREATE_DETACHED); //将线程属性设置成分离态
pthread_create(&tid, &thread_attr, thread_worker2, &value);
pthread_attr_destroy(&thread_attr);
while (1)
{
printf("MAIN THREAD START RUN\n");
printf("main thread control %d:\n", value);
sleep(2);
if(value == 110)
{
break;
}
}
printf("main thread start exit\n");
return 0;
}
void *thread_worker2(void *args)
{
int *ptr = (int *)args;
printf("son thread [%ld] start run\n",pthread_self());
while (1)
{
printf("2222222 %s before, value++: %d\n", __FUNCTION__, *ptr);
*ptr += 1;
sleep(2);
printf("2222222 %s after, value++: %d\n", __FUNCTION__, *ptr);
if( *ptr == 105)
{
break;
}
}
printf("son pthread start exit\n");
return NULL;
}
接下来编译运行
- 因为主线程和子线程是分离的,所以主线程不用等待子线程退出在运行。
- 主线程创建子线程后究竟是子线程还是主线程先执行,或究竟哪个子线程先运行系统并没有规定,这个依赖操作系统的进程 调度策略。
5.3 两个线程访问共享变量(不带锁)
接下来我们将上面两段代码结合一下,但是不加锁
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
void *thread_worker1(void *args);
void *thread_worker2(void *args);
int main(int argc, char **argv)
{
int shared_var = 100;
pthread_t tid;
pthread_attr_t thread_attr;
if( pthread_attr_init(&thread_attr) )
{
printf("pthread_attr_init() failure: %s\n", strerror(errno));
return -1;
}
if( pthread_attr_setstacksize(&thread_attr, 120*1024) )
{
printf("pthread_attr_setstacksize() failure: %s\n", strerror(errno));
return -1;
}
if( pthread_attr_setdetachstate(&thread_attr, PTHREAD_CREATE_DETACHED) )
{
printf("pthread_attr_setdetachstate() failure: %s\n", strerror(errno));
return -1;
}
pthread_create(&tid, &thread_attr, thread_worker1, &shared_var);
printf("Thread worker1 tid[%ld] created ok\n", tid);
pthread_create(&tid, NULL, thread_worker2, &shared_var);
printf("Thread worker2 tid[%ld] created ok\n", tid);
pthread_attr_destroy(&thread_attr);
pthread_join(tid, NULL);
while(1)
{
printf("Main/Control thread shared_var: %d\n", shared_var);
sleep(10);
}
}
void *thread_worker1(void *args)
{
int *ptr = (int *)args;
if( !args )
{
printf("%s() get invalid arguments\n", __FUNCTION__);
pthread_exit(NULL);
}
printf("Thread workder 1 [%ld] start running...\n", pthread_self());
while(1)
{
printf("111: %s before shared_var++: %d\n", __FUNCTION__, *ptr);
*ptr += 1;
sleep(2);
printf("111: %s after sleep shared_var: %d\n", __FUNCTION__, *ptr);
}
printf("Thread workder 1 exit...\n");
return NULL;
}
void *thread_worker2(void *args)
{
int *ptr = (int *)args;
if( !args )
{
printf("%s() get invalid arguments\n", __FUNCTION__);
pthread_exit(NULL);
}
printf("Thread workder 2 [%ld] start running...\n", pthread_self());
while(1)
{
printf("222: %s before shared_var++: %d\n", __FUNCTION__, *ptr);
*ptr += 1;
sleep(2);
printf("222: %s after sleep shared_var: %d\n", __FUNCTION__, *ptr);
}
printf("Thread workder 2 exit...\n");
return NULL;
}
我们观察运行结果发现当线程1 操作加1的运算后,就是111before和111after输出value的值, 相差了两个数,就是101直接变成了103,而我们进程1想要的效果是101,102.线程1加1的操作被打断了, 线程2同样是被打断了, 为什么会这样? 就是因为没有加锁的缘故,线程2也对共享变量share_value进行了修改,那怎么才能让两个线程自己干自己的事呢,那肯定是加锁啊。
6, 带锁的线程示例代码
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
void *thread_worker1(void *args);
void *thread_worker2(void *args);
typedef struct worker_ctx_s
{
int shared_var;
pthread_mutex_t lock;
} worker_ctx_t;
int main(int argc, char **argv)
{
worker_ctx_t worker_ctx;
pthread_t tid;
pthread_attr_t thread_attr;
worker_ctx.shared_var = 1000;
pthread_mutex_init(&worker_ctx.lock, NULL);
if( pthread_attr_init(&thread_attr) )
{
printf("pthread_attr_init() failure: %s\n", strerror(errno));
return -1;
}
if( pthread_attr_setstacksize(&thread_attr, 120*1024) )
{
printf("pthread_attr_setstacksize() failure: %s\n", strerror(errno));
return -1;
}
if( pthread_attr_setdetachstate(&thread_attr, PTHREAD_CREATE_DETACHED) )
{
printf("pthread_attr_setdetachstate() failure: %s\n", strerror(errno));
return -1;
}
pthread_create(&tid, &thread_attr, thread_worker1, &worker_ctx);
printf("Thread worker1 tid[%ld] created ok\n", tid);
pthread_create(&tid, &thread_attr, thread_worker2, &worker_ctx);
printf("Thread worker2 tid[%ld] created ok\n", tid);
while(1)
{
printf("Main/Control thread shared_var: %d\n", worker_ctx.shared_var);
sleep(10);
}
pthread_mutex_destroy(&worker_ctx.lock);
}
void *thread_worker1(void *args)
{
worker_ctx_t *ctx = (worker_ctx_t *)args;
if( !args )
{
printf("%s() get invalid arguments\n", __FUNCTION__);
pthread_exit(NULL);
}
printf("Thread workder 1 [%ld] start running...\n", pthread_self());
while(1)
{
pthread_mutex_lock(&ctx->lock);
printf("1111: %s before shared_var++: %d\n", __FUNCTION__, ctx->shared_var);
ctx->shared_var ++;
sleep(2);
printf("1111: %s after sleep shared_var: %d\n", __FUNCTION__, ctx->shared_var);
pthread_mutex_unlock(&ctx->lock);
sleep(1);
}
printf("Thread workder 1 exit...\n");
return NULL;
}
void *thread_worker2(void *args)
{
worker_ctx_t *ctx = (worker_ctx_t *)args;
if( !args )
{
printf("%s() get invalid arguments\n", __FUNCTION__);
pthread_exit(NULL);
}
printf("Thread workder 2 [%ld] start running...\n", pthread_self());
while(1)
{
if(0 != pthread_mutex_trylock(&ctx->lock) )
{
continue;
}
printf("2222: %s before shared_var++: %d\n", __FUNCTION__, ctx->shared_var);
ctx->shared_var ++;
sleep(2);
printf("2222: %s after sleep shared_var: %d\n", __FUNCTION__, ctx->shared_var);
pthread_mutex_unlock(&ctx->lock);
sleep(1);
}
printf("Thread worker 2 exit...\n");
return NULL;
}
我们观察到1111before和1111after是正常加1的,没有被打断。这就是原子操作,在加锁和释放锁这段代码是不能被打断的,是打包的操作。