线程操作
一、线程的概念:
1、进程简要回顾
进程可以看作只有一个线程,一个进程同一时间只能进行一件事情的操作,有了多个线程后,程序同一时间可以进行多个不同的事情
进程是程序执行的实例,是担当分配系统资源的基本单位。进程本身不是基本运行单位,而是线程的容器。
进程有独立的内存空间,一个进程崩溃后,在保护模式下不会影响其他进程的执行。
**线程是一个进程的多个执行路径,每个线程都有自己独立的堆栈,和局部变量,**但是线程没有独立的内存空间,一个线程崩溃后,所有的线程都会崩溃。所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。
线程相当与是寄生在进程里面。
“进程——资源分配的最小单位,线程——程序执行的最小单位”
2、使用线程原因
使用多线程的理由之一是和进程相比,它是一种非常"节俭"的多任务操作方式。我们知道,在Linux系统下,启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段,这是一种"昂贵"的多任务工作方式。而运行于一个进程中的多个线程,它们彼此之间使用相同的地址空间,共享大部分数据,启动一个线程所花费的空间远远小于启动一个进程所花费的空间,而且,线程间彼此切换所需的时间也远远小于进程间切换所需要的时间。
使用多线程的理由之二是线程间方便的通信机制。对不同进程来说,它们具有独立的数据空间,要进行数据的传递只能通过通信的方式进行,这种方式不仅费时,而且很不方便。线程则不然,由于同一进程下的线程之间共享数据空间,所以一个线程的数据可以直接为其它线程所用,这不仅快捷,而且方便。当然,数据的共享也带来其他一些问题,有的变量不能同时被两个线程所修改,有的子程序中声明为static的数据更有可能给多线程程序带来灾难性的打击,这些正是编写多线程程序时最需要注意的
二、线程的API
其涉及的多线程开发的最基本概念主要包含三点:线程,互斥锁,条件。其中,线程操作又分线程的创建,退出,等待 3 种。互斥锁则包括 4 种操作,分别是创建,销毁,加锁和解锁。条件操作有 5 种操作:创建,销毁,触发,广播和等待。其他的一些线程扩展概念,如信号灯等,都可以通过上面的三个基本元素的基本操作封装出来。
1、线程
(1)、线程的创建
#include <pthread.h>
int pthread_create(pthread_t *restrict tidp, const pthread_attr_t *restrict attr, void *(*start_rtn)(void *), void *restrict arg);
当pthread_create成功返回时,由tidp指向的内存单元被设置为新创建线程的线程ID。attr参数用于定制各种不同的线程属性,暂可以把它设置为**NULL,**以创建默认属性的线程。
新创建的线程从start_rtn函数的地址开始运行,该函数只有一个无类型指针参数arg。如果需要向start_rtn函数传递的参数不止一个,那么需要把这些参数放到一个结构中,然后把这个结构的地址作为arg参数传入。
1.线程创建例子
(void*)&a 是把a的地址强制转换成任意类型的指针变量
pthread_self()表示当前自己的线程id号
一般需要强制转换后使用(unsigned long)pthread_self();
#include <stdio.h>
#include <pthread.h>
void * fun1(void * parm)
{
static int ret = 11;
/*********************
进行字符串的传递
static char *ret = "return";
/********************
使用指针变量
static int a = 10;
static int *p = &a;
*********************/
printf("fun1 线程号:%ld\n",(unsigned long)pthread_self());
printf("fun1: %d\n",*((int *)parm));//把parm强制转换成int型指针并取出地址里面的内容
//printf("fun1: %s\n",(char *)parm);
//进程退出
pthread_exit((void *)&ret);//把ret地址强制转换成void *的指针变量
/*************************
进行字符串的传递
pthread_exit((void *)ret);
************************/
}
int main()
{
pthread_t t1;//定义一个pthread_t 类型的指针变量
int pth;//定义一个返回值 线程
int parm=100;
int *pret;//接受从fun1进程送回的信息
//char *pret;
pth = pthread_create(&t1,NULL,fun1,(void *)&parm);
if(pth==0){
printf("main 创建成功线程\n");
}
printf("main 线程号:%ld\n",(unsigned long)pthread_self());
pthread_join(t1,(void **)&pret);//将该指针的地址强制转换成void**指针
printf("main pret = %d\n",*pret);//取出返回的值
//printf("main pret = %s\n",pret);//取出返回的字符串
return 0;
}
2.static 关键字
程序一
1.在函数中赋值相当于没有执行过
#include<stdio.h>
void fun(int i)
{
static int value=0;
printf("%d\n",value);
value = i++;
printf("%d\n",value);
}
int main( )
{
fun(0);
fun(1);
fun(2);
return 0;
}
输出结果是0 0
0 1
1 2
程序二
#include <stdio.h>
int fun(void){
static int count = 10; // 事实上此赋值语句从来没有执行过
return count--;
}
int count = 1;
int main(void)
{
printf("global\t\tlocal static\n");
for(; count <= 10; ++count)
printf("%d\t\t%d\n", count, fun());
return 0;
}
结果:
程序三
#include <stdio.h>
void fn(void)
{
int n = 10;
printf("n=%d\n", n);
n++;
printf("n++=%d\n", n);
}
void fn_static(void)
{
static int n = 10;
printf("static n=%d\n", n);
n++;
printf("n++=%d\n", n);
}
int main(void)
{
fn();
printf("--------------------\n");
fn_static();
printf("--------------------\n");
fn();
printf("--------------------\n");
fn_static();
return 0;
}
输出的结果是:
-> % ./a.out
n=10
n++=11
--------------------
static n=10
n++=11
--------------------
n=10
n++=11
--------------------
static n=11
n++=12
对于static关键字有了一个新的认识
函数每次使用普通变量的时候都是重新进行分配空间的
而静态局部变量保持上次调用的值不变
在上面的程序中
一个函数的参数使用了static
一个函数的参数没有使用static
/第一个使用fn函数之后 输出的结果 和第二次的输出结果一样
表明函数使用普通变量的时候都是重新进行空间的分配/
而使用的fn_static 函数之后 第一次的结果 得到的n等于11
因为静态局部变量不会进行空间的分配因此使用的还是以前的保留下来的值 经过上次计算的到的n的值不会因为函数的重新调用而消失
static全局变量只初使化一次,防止在其他文件单元中被引用;
(2)、线程的退出
单个线程可以通过以下三种方式退出,在不终止整个进程的情况下停止它的控制流:
1)线程只是从启动例程中返回,返回值是线程的退出码。
2)线程可以被同一进程中的其他线程取消。
3)线程调用pthread_exit
#include <pthread.h>
int pthread_exit(void *rval_ptr);
static int ret =10;
pthread_exit((void*)&ret);
**rval_ptr是一个无类型指针,**与传给启动例程的单个参数类似。进程中的其他线程可以通过调用pthread_join函数访问到这个指针
3.验证线程使用共享内存
创建两个线程进行记录
#include <stdio.h>
#include <pthread.h>
int g_data=0;//定义一个全局变量
void *fun1()
{
while(1){
printf("t1 : g_data=%d\n",g_data++);
sleep(1);
}
}
void *fun2()
{
while(1){
printf("t2 : g_data=%d\n",g_data++);
sleep(1);
}
}
int main()
{
pthread_t t1;
pthread_t t2;
int pth;
//创建进程 默认不进行数据传参
pthread_create(&t1,NULL,fun1,NULL);
pthread_create(&t2,NULL,fun2,NULL);
while(1){
printf("main : g_data=%d\n",g_data++);
sleep(1);
}
//等待线程
pthread_join(t1,NULL);//等待线程1
pthread_join(t2,NULL);//等待线程2
return 0;
}
输出的结果是 线程之间的内存空间是共享的
main : g_data=0
t2 : g_data=1
t1 : g_data=2
main : g_data=3
t2 : g_data=4
t1 : g_data=5
main : g_data=6
t2 : g_data=7
t1 : g_data=8
main : g_data=9
(3)、线程的等待
函数原型
#include <pthread.h>
int pthread_join(pthread_t thread, void **rval_ptr);
// 返回:若成功返回0,否则返回错误编号
int *pret;
pthread_join(t1,(void**)&pret);
printf("main pret:%d\n",*pret);
调用这个函数的线程将一直阻塞,直到指定的线程调用pthread_exit、从启动例程中返回或者被取消。如果例程只是从它的启动例程返回i,rval_ptr将包含返回码。如果线程被取消,由rval_ptr指定的内存单元就置为PTHREAD_CANCELED。
可以通过调用pthread_join自动把线程置于分离状态,这样资源就可以恢复。如果线程已经处于分离状态,pthread_join调用就会失败,返回EINVAL。
如果对线程的返回值不感兴趣,可以把rval_ptr置为NULL。在这种情况下,调用pthread_join函数将等待指定的线程终止,但并不获得线程的终止状态。
(4)、线程的脱离
一个线程或者是可汇合(joinable,默认值),或者是脱离的(detached)。当一个可汇合的线程终止时,它的线程ID和退出状态将留存到另一个线程对它调用pthread_join。脱离的线程却像守护进程,当它们终止时,所有相关的资源都被释放,我们不能等待它们终止。如果一个线程需要知道另一线程什么时候终止,那就最好保持第二个线程的可汇合状态。
pthread_detach函数把指定的线程转变为脱离状态。
#include <pthread.h>
int pthread_detach(pthread_t thread);
// 返回:若成功返回0,否则返回错误编号
本函数通常由想让自己脱离的线程使用,就如以下语句:
pthread_detach(pthread_self());
(5)、线程ID获取及比较
#include <pthread.h>
pthread_t pthread_self(void);
// 返回:调用线程的ID
对于线程ID比较,为了可移植操作,我们不能简单地把线程ID当作整数来处理,因为不同系统对线程ID的定义可能不一样。我们应该要用下边的函数:
#include <pthread.h>
int pthread_equal(pthread_t tid1, pthread_t tid2);
// 返回:若相等则返回非0值,否则返回0
2、互斥锁相关的API
1.Linux提供一把互斥锁mutex(也称之为互斥量)
2.每个线程在对资源操作前都尝试先加锁,成功加锁才能操作,操作结束后解锁。
3.资源还是共享的,线程间也还是竞争的,但通过锁将资源的访问变为互斥操作,而后与时间有关的错误也不会在产生了。
4.如果释放互斥锁时有多个线程阻塞,所有在该互斥锁上的阻塞线程都会变成可运行状态,第一个变为可运行状态的线程可以对互斥量加锁,其他线程将会看到互斥锁依然被锁住,只能回去等待它重新变为可用。多个线程同时进行竞争
是应该注意:同一个时刻,只能有一个线程持有该锁。
(1)创建及销毁互斥锁
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
int pthread_mutex_destroy(pthread_mutex_t* mutex);
// 返回:若成功返回0,否则返回错误编号
//要用默认的属性初始化互斥量,只需把attr设置为NULL
(2)加锁及解锁
加锁保证的是加锁的代码段执行的时候其他的线程代码需要进行等待 该线程的代码执行完毕后,其他的线程的代码才会执行
该进程执行完成后不会再次执行
加锁之后保证加锁的代码执行完成后其他线程的代码才会执行
进入到加锁空间之后,会保证锁里面的代码运行完成后再运行其他线程
#include <pthread.h>
//加锁
int pthread_mutex_lock(pthread_mutex_t* mutex);
int pthread_mutex_trylock(pthread_mutex_t* mutex);
//解锁
int pthread_mutex_unlock(pthread_mutex_t* mutex);
// 返回:若成功返回0,否则返回错误编号
(3.1)例子一
pthread_mutex_t mutex;//定义一个全局锁
void *fun1()
{
//加🔓
pthread_mutex_lock(&mutex);
int i;
for(i=0;i<3;i++){
printf("t1 : g_data=%d\n",g_data++);
sleep(1);
}
//解🔓
pthread_mutex_unlock(&mutex);
}
int main()
{
pthread_mutex_init(&mutex,NULL);//创建互斥锁
pthread_mutex_destroy(&mutex);//销毁互斥锁
return 0;
}
(3.2)例子二
void *fun1()
{
pthread_mutex_lock(&mutex);
int i=0;
i++;
printf("是否进入while\n");
while(1){
printf("t1 : g_data=%d\n",g_data++);
sleep(1);
printf("i=%d\n",i);
if(g_data==3){
//解🔓
pthread_mutex_unlock(&mutex);
printf("ti=========================\n");
}
}
}
void *fun2()
{
while(1){
pthread_mutex_lock(&mutex);//进入到线程二对g_data进行操作的时候会限制 加🔓
printf("t2 : g_data=%d\n",g_data);
g_data++;
pthread_mutex_unlock(&mutex);//解🔓
sleep(1);
}
}
通过上面的例子进入线程后,只会不断执行while里面的东西不会再次执行整个函数。
当进入到线程一之后加锁 g_data等于3的时候解锁 在此之前线程二想解锁需要一直等待 知道线程一的锁解锁完成后才能进行运行。
(4)死锁的产生
产生死锁,一个互斥锁无法形成互斥锁,大于一个互斥锁才能形成
当线程1获得一把互斥锁1,以后想要获得另一把互斥锁2的时候
线程二获得了互斥锁2,然后线程二也想要去获得互斥锁1的时候,造成了谁都不能去解锁,造成了线程无法进行。
3、条件的API
(1)创建及销毁条件变量
#include <pthread.h>
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
int pthread_cond_destroy(pthread_cond_t *cond);
// 返回:若成功返回0,否则返回错误编号
(2)等待
#include <pthread.h>
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, cond struct timespec *restrict timeout);
// 返回:若成功返回0,否则返回错误编号
pthread_cond_wait等待条件变为真。如果在给定的时间内条件不能满足,那么会生成一个代表一个出错码的返回变量。传递给pthread_cond_wait的互斥量对条件进行保护,调用者把锁住的互斥量传给函数。函数把调用线程放到等待条件的线程列表上,然后对互斥量解锁,这两个操作都是原子操作。这样就关闭了条件检查和线程进入休眠状态等待条件改变这两个操作之间的时间通道,这样线程就不会错过条件的任何变化。pthread_cond_wait返回时,互斥量再次被锁住。
当使用pthread_cond_wait的时候,因为使用该函数得到了互斥锁,但是条件不成立,此时其他的线程也无法得到该锁,造成堵塞, 这时就会释放互斥锁,让其他的线程去得到锁, 当其他的线程使用pthread_cond_signal产生了条件之后,就会被唤醒,
等待线程的过程
pthread_cond_wait 首先会进行加锁
pthread_cond_wait内部会解锁,然后等待条件变量将其激如果让其进入了阻塞,会解锁,让其他的线程去得到锁,产生触发条件
pthread_cond_wait触发激活后,会自动加锁,
pthread_cond_timedwait函数的工作方式与pthread_cond_wait函数类似,只是多了一个timeout。timeout指定了等待的时间,它是通过timespec结构指定。
(3)触发
#include <pthread.h>
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);
// 返回:若成功返回0,否则返回错误编号
这两个函数可以用于通知线程条件已经满足。pthread_cond_signal函数将唤醒等待该条件的某个线程,而pthread_cond_broadcast函数将唤醒等待该条件的所有进程。
(4)例子
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
int set=0;
pthread_mutex_t mutex;//创建互斥锁
pthread_cond_t cond;//创建一个条件
void *fun1()
{
while(1){
//等待条件和🔓 当线程2执行到g_data=3后产生触发条件
//会进入到线程1执行下面的语句,后再次执行线程2
pthread_cond_wait(&cond,&mutex);
printf("t1 : g_data=%d\n",g_data);
sleep(1);
printf("t1:=================================\n");
g_data=0;
}
}
void *fun2()
{
while(1){
pthread_mutex_lock(&mutex);//进入到线程二对g_data进行操作的时候会限制 加🔓
printf("t2 : g_data=%d\n",g_data);
g_data++;
if(g_data==3){
sleep(1);
pthread_cond_signal(&cond);//当g_data等于3的时候产生触发条件
}
pthread_mutex_unlock(&mutex);//解🔓
sleep(1);
}
}
int main()
{
int pth;
pthread_t t1; //线程一
pthread_t t2;//线程二
int pthre=100;
//创建一个互斥锁
pthread_mutex_init(&mutex,NULL);
//创建一个条件
pthread_cond_init(&cond,NULL);
//创建线程一
pth=pthread_create(&t1,NULL,fun1,(void*)&pthre);
if(pth==0){
printf("create success\n");
}
//创建线程二
pth=pthread_create(&t2,NULL,fun2,(void*)&pthre);
if(pth==0){
printf("create success\n");
}
pthread_join(t1,NULL);//等待线程一完成
pthread_join(t2,NULL);//等待线程二完成
pthread_mutex_destroy(&mutex);//销毁线程
pthread_cond_destroy(&cond);//销毁条件
return 0;
}
运行结果
(5)静态和动态赋值
静态
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
动态
pthread_cond_t cond;
pthread_cond_init(&cond,NULL);
pthread_mutex_t mutex;
pthread_mutex_init(&mutex,NULL);
4、使用system写一个程序执行多少次的C
1、文件测试
int main(int argr,char *argv[])
{
int time=atoi(argv[1]);
int i;
for(i=1;i<time;i++){
system("text");//使用的可执行文件
}
}
执行命令 ./a.out >>text.ret.txt &
把运行完成的文件导入到text.ret.txt文件里面 加& 可以执行操作 不等待