目录
(一)并行和并发的区别
并行的反义词是串行,假如有一个10万数据量的数组让我们挑出这里的最大值,串行的处理方法就是对这个数组扫描一遍,找到其最大值当然这样的效率非常慢,而并行可以看作将这10万个数据分为10组,分配多个处理器进行处理找到每个数组的最大值在比较,找出最终结果,这是在多核处理下的并行虽然提高了效率但是涉及到分解,运行,合并等调度任务。在课本上的定义:
在操作系统中是指,一组程序按独立异步的速度执行,无论从微观还是宏观,程序都是一起执行的。
通常来说只有多核处理器下才满足无论从微观还是宏观程序是一起执行的。即多条程序在多个处理器下被执行,而下面的并发从宏观上可看作是多个进程被执行(并行)。
并发的反义词是顺序,就像我们小时候做的数学题:做家务烧水要10分钟,擦地要15分钟,洗衣服要20分钟,做完一共要多久,有的小孩子嘛可能会45分钟,这就是顺序处理的代表型人物,稍微聪明点的会先让洗衣机洗衣服,然后插上水壶再去拖地,这样就是20分钟,也是并发的核心思想。在我们的课本上并发的定义是:
在操作系统中,并发是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行,但任一个时刻点上只有一个程序在处理机上运行。
从定义中理解并发就是一个时刻只有一条指令被执行,多个进程快速切换执行指令,在单核处理器下,宏观上可看作是并行,并发的实现通常要靠多线程来实现,又因为线程之间是抢占式调度因此我们需要了解同步和互斥的概念。
互斥:因为在很多情况下,线程使用的是局部变量,其分配的空间在线程的栈空间内,此时这个变量只能被该线程使用。而还有些变量需要在线程空间内共享,也就是多个线程访问的公共资源(临界资源),访问临界资源的代码区叫做临界区,而互斥就是为了保护每个时刻只有一个执行流进入临界区访问临界资源,对其进行保护。执行顺序一般是无序的。实现机制通过上锁:加锁 —> 执行临界区代码 —> 释放锁。
同步:同步关系中往往包含着互斥,其控制着线程与线程间的执行顺序,例如一个线程的运行要依赖另一个线程运行后的结果,因此他们间就需要同步来协调让后者先运行而前者等待。协调多个线程合作完成任务,执行顺序一般是有序的。
(二)多线程的概念及优缺点
线程是操作系统能够进行运算调度的最小单位,它被包含在进程中,是进程(最小分配资源单位)中实际运作单位,其也被称为轻量进程(lighr weight process)通常指内核线程。
- 优点:创建代价小,占用资源少,数据共享、数据通讯方便。
- 缺点:实现难度高,健壮性降低。(相对优点来说,缺点几乎可忽略)
(三)创建线程
在创建一个进程后,会产生一个缺省的线程,通常被叫做主线程(或控制线程),在linux下c/c++程序中,主线程就是main函数进入的线程,其调用pthread_creat()
创建的线程称为子线程,子线程也可通过用户指定其入口函数,每个函数通过pthread_self()
获取其自身的线程ID。在线程模型中除主线程外,其他线程一旦被创建,其关系是同等的,每个进程可创建的线程数根据具体实现决定。
主线程和子线程的默认关系是:无论子线程执行完毕否,一旦主线程执行完毕退出,所有子线程执行都会终止。
此时该进程结束或僵死,部分线程保持终止执行但未销毁的状态,进程必须在线程销毁后销毁,此时进程进入僵死状态,线程函数执行后退出,或终止,线程进入终止状态,但为其分配的资源不一定释放,取决于线程属性。在此,主线程和子线程通常定义为如下两种关系:
1、可会和(joinable):在这种关系下,主线程需要明确执行等待操作,在子线程结束后,主线程完成等待,和子线程会和,继续执行等待后的操作。在主线程的线程函数内部调用子线程对象的wait()
函数实现,即使子线程在主线程之前完成,进入终止态,必须完成会和操作,否则系统不会主动销毁线程及其分配的系统资源。
2、相分离(detached):表示主线程和子线程无需会和,在这种情况下,子线程进入终止状态。因为让主线程逐个等待子线程结束或者安排每个子线程结束的等待顺序在子线程较多的情况下实现是困难的,因此在并发子线程较多的情况下通常使用相分离的方式。
pthread_create()
创建线程
#include<pthread.h> //头文件
int pthread_create(pthread_t *tidp,const pthread_attr_t *attr,
(void*)(*start_rtn)(void*),void *arg); //函数原型
参数说明:
第一个参数:tidp是一个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_CREAT_DETACHED。
第三个参数:start_rtn是一个函数指针,指向函数原型为void *func(void *)
的函数,也就是创建子进程要执行任务的函数。
第四个参数:arg是传给所调用的函数的参数,当有多个参数要传入时,则要封装到一个结构体,传入指向这个结构体的指针。
通过下面的例子理解如何创建线程:
#include <stdio.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
#include <errno.h>
void *fun(void *arg)
{
int *p = (int *)arg;
printf("I'm child thread, Thread ID = %lu,The shared_var = %d\n", pthread_self(), *p);
return NULL;
}
int main(void)
{
int shared_var = 1000;
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_getstackaddr() failure :%s\n", strerror(errno));
return -1;
}
pthread_create(&tid, NULL, fun, &shared_var);
printf("I'm main thread, my pid = %d\n", getpid());
pthread_attr_destroy(&thread_attr); //销毁初始化结构
pthread_join(tid,NULL); //阻塞等待线程退出
return 0;
}
运行结果如下:
panghu@Ubuntu-14:~/exercise$ gcc example.c -o thread -lpthread //线程编译时需要带-lpthread选项
panghu@Ubuntu-14:~/exercise$ ./thread
I'm main thread, my pid = 10680
I'm child thread, Thread ID = 140628718462720,The shared_var = 1000
pthread_join
()阻塞回收
int pthread_join(pthread_t thread, void **retval); // 成功返回0 失败返回错误号
// 参数线程ID 和 参数退出状态(通常为NULL)
其中pthread_join
函数 阻塞等待线程退出,获取线程退出状态 其作用,对应进程中 waitpid() 函数。因为在线程初始化的时候设置了分离模式,因此如果不阻塞等待的话运行后将会是如下结果:
panghu@Ubuntu-14:~/exercise$ gcc example.c -o thread -lpthread
panghu@Ubuntu-14:~/exercise$ ./thread
I'm main thread, my pid = 10744
pthread_detach
()线程分离
上面提到过如果不使用参数初始化的话,也可使用:
pthread_detach(tid); //让线程分离,——自动退出,无系统残留资源
但在一般情况下线程终止后,终止状态一直保留到其他线程调用:pthread_join
获取状态位置,而调用了pthread_detach
函数后县城一旦中止就会立即回收它所占用的资源而不保留终止状态,不可调用了pthread_detach
函数的线程调用pthread_join
将返回EINVAL错误,即如上两函数不可同时调用。
pthread_exit()
单线程退出
void pthread_exit(void *retval); //将参数表退出状态 通常传NULL
在线程中禁止使用exit()
函数,该函数会使进程内的所有线程全部退出,可以使用pthread_exit
,将单个线程退出。
pthread_cancel()
线程取消
int pthread_cancel(pthread_t thread); // 调用成功返回0 失败则返回调用号
pthread_cancel
可以杀死(取消)线程 其作用,对应进程中 kill() 函数。
#include <stdio.h>
#include <pthread.h>
static void* pthread_func1(void* arg)
{
while(1)
{
printf("hello world\n");
sleep(1);
}
return NULL;
}
static void* pthread_func2(void* arg)
{
int a = 0;
for( ;; )
a++;
return NULL;
}
int main(int argc, char const *argv[]) {
pthread_t tid;
pthread_create(&tid, NULL, pthread_func1, NULL);
pthread_cancel(tid);
pthread_join(tid, NULL);
return 0;
}
上面这个例子中当线程运行pthread_func1
函数时输出的是:
panghu@Ubuntu-14:~/exercise$ ./cancel
hello world
panghu@Ubuntu-14:~/exercise$
而运行pthread_func2
函数时输出结果为:
panghu@Ubuntu-14:~/exercise$ ./cancel
//在pthread_func2函数中死循环
因为pthread_cancel()
函数不是直接退出线程,而是在系统中设置一个cancelpoint
,在调用系统调用时会检查是否设置了cancelpoint
,如果设置就会退出,例如pthread_func1
中的sleep(1)
就属于系统调用,而pthread_func2
是c语言的for循环库函数,因此不会退出线程。
(四)进程与线程控制语句对比
线程 | 进程 |
---|---|
fork (创建进程 ) | pthread_create (创建线程) |
wait(进程阻塞) | pthread_join(线程阻塞) |
exit(进程退出) | pthread_exit(单个线程退出) |
getpid(返回进程ID) | pthread_self(返回线程ID) |
kill(杀死进程) | pthread_cancel(线程取消) |
参考博客:https://blog.csdn.net/qq_27396861/article/details/95752790