线程(1)

一、线程的基本概念

1.线程:

进程内部的一条执行路径(序列),所有的进程至少有一个执行线程——主线程,即唯一的一条执行路径就是从主函数的第一行代码到最后一行。

2.进程和线程:

(1)进程是一个正在运行的程序,它为其中的一个或多个线程分配资源。线程只是一条执行路径。

(2)在进程中创建一个新线程时,新的线程有自己的栈(即有自己的局部变量),与主线程共享全局变量、文件描述符、信号处理函数和当前目录状态、


二、线程的分类

1.用户级线程:由线程库中的代码创建、调度和管理等操作,创建代价相对较小。

2.内核级线程:由内核完成对线程的创建、调度和管理,创建代价相对较大。

 

三、线程出现的原因:

主要有两个原因

1.有时,一个程序需要同时执行两个任务,例如同时从两个地方读取数据——用户级线程

2.为了更多地利用计算机上的硬件资源——内核级线程。


四、线程的相关函数

1.创建线程:pthread_create()

(1)函数原型:

 #include<pthread.h>
 intpthread_create(pthread_t *id, pthread_attr *attr, void *(*start_routine)(void), (void*)arg);

(2)参数解释:

pthread_t *id

线程标识符

pthread_attr *attr

线程属性

void *(*start_routine)(void)

线程执行函数

(void*)arg

线程执行函数的参数

创建新线程时,必须明确提供给它提供一个函数指针,新县城将在这个新位置开始执行。

pthread_create()调用成功时返回0,失败则返回错误代码。(与大部分UNIX函数不同,谨记)


2. 结束线程:pthread_exit()

(1)函数原型:

#include<pthread.h>
voidpthread_exit(void *retval);

(2)参数解释

  参数只有一个传出参数,只是将结束信息打印出来

(3)使用:

#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
#include<string.h>
#include<pthread.h>
 
void*thread_fun(void *arg)
{
        int i=0;
        for( ; i<5; i++)
        {
            printf("fun run, a =%d\n", i);
            sleep(1);
        }
}

int main()
{
        pthread_t id;
        pthread_create(&id,NULL,thread_fun,NULL);

        int i=0;
        for( ; i<3; i++)
        {
            printf("main run , a = %d\n", i);
            sleep(1);
        }
 
        exit(0);
}

  程序执行结果:

 

  主线程中打印3次,子线程中打印5次。主线程打印完之后,执行exit(0),整个进程直接结束,子进程只打印了3次也就被迫结束了。

  所以,我们选择在主线程结束后,使用pthread_exit()结束主进程,子进程得以继续执行至结束,而不是直接结束整个进程。

#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
#include<string.h>
#include<pthread.h>
 
void*thread_fun(void *arg)
{
        int i=0;
        for( ; i<5; i++)
        {
            printf("fun run, a =%d\n", i);
            sleep(1);
        }
}
int main()
{
        pthread_t id;
        pthread_create(&id,NULL,thread_fun,NULL);

        int i=0;
        for( ; i<3; i++)
        {
            printf("main run , a = %d\n", i);
            sleep(1);
        }

        pthread_exit("main over");//结束主线程

        exit(0);
}

 


但是,我们一般不退出一个主线程,所以在子线程中调用pthread_exit(),在主线程中调用pthread_join()来等待子线程结束,并且打印结束信息。

#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
#include<string.h>
#include<pthread.h>
 
void*thread_fun(void *arg)
{
        int i=0;
        for( ; i<5; i++)
        {
            printf("fun run, a =%d\n", i);
            sleep(1);
        }
	pthread_exit("fun over");//结束子线程
}

int main()
{
        pthread_t id;
        pthread_create(&id,NULL,thread_fun,NULL);


        int i=0;
        for( ; i<3; i++)
        {
            printf("main run , a = %d\n", i);
            sleep(1);
        }

        char *s = NULL;
        pthread_join( id,(void *)&s);//子线程未结束,等待子线程结束
        printf("%s\n",s);

        exit(0);
}



3. 等待线程结束:pthread_join()

(1)函数原型:

#include<pthread.h>
voidpthread_join(pthread_t id, void **thread_return);


(2)参数解释

pthread_t id

线程id

void **thread_return

二级指针,指向的指针指向线程的返回值

 

五、线程核心——并发和同步

1.并发和同步:

(1)并发:两个以上的进程或线程交替运行。并行是特殊的并行——同一时刻两个进程或线程都同时在运行。

   在单处理器上,内核级和用户级线程都在并发
   在多处理器上,内核级线程可以达到并行,用户级线程在并发

(2)同步:为了控制两个以上的进程或线程对临界资源的使用,即确保任一时刻只有一个程序使用临界资源,它们之间需要协同合作,有序地执行。

 

2.线程间同步——信号量、互斥量

#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
#include<string.h>
#include<pthread.h>
#include<semaphore.h>
 
#defineMax 5
intg = 1;
 
void*fun(void *arg)
{
        int i = 0;
        for( ; i<1000; i++)
        {
            printf("g = %d\n",g++);//连续加1000次
        }
}
 
int main()
{
        pthread_t id[Max];
        int i = 0;
        for( ; i<Max; i++)
        {
            pthread_create(&id[i],NULL,fun,NULL);//创建Max个新线程
        }
 
        for( i = 0; i<Max; i++)//等待Max个线程结束
	{
            pthread_join(id[i],NULL);
        }

        exit(0);
}



以上程序的结果,可能会出现不等于5000的情况,造成这个现象的原因,恰恰就是“线程间的同步”没有协调好。

g的值存储在磁盘中,g++操作需要将g的数据读取到CPU中,中间需要经过cathe、寄存器和内存,最后才到CPU中。所以如果两个线程都读取到g的值为2,第一个线程先一步完成+1操作,将g的值改为3,另一个线程随后完成,再去覆盖原来的数据得到的还是3,这就相当于两次操作却只加了1,浪费了一次操作。

所以,我们需要对“g++”这个语句进行同步,即当一个线程在执行这句时,其他线程要被挂起,等待先前的进程完成操作才去执行。不过,同步后的线程的执行时间要比之前慢,之间的调度增加了不少时间。


(1)信号量

#include <semaphore.h>

1.创建信号量

int sem_init(sem_t *sem, int pshared, unsigned int value)

该函数初始化由sem指向的信号量对象,设置它的共享选项pshared,一般设置为0,表示这个信号量是当前进程的局部信号量,Linux不支持进程间分享的信号量,所以pshared不能置为非0值,value是这个信号量的初始值。
2.以原子操作的方式给信号量的值+1

int sem_wait(sem_t *sem)
3.以原子操作的方式给信号量的值-1

int sem_post(sem_t *sem)

原子操作:不能被中断的操作,操作其间不会有其他指令发生,甚至是一个中断。

如果两个线程企图同时给一个信号量+1,它们之间互不干扰。
4.销毁信号量

int sem_destroy(sem_t *sem)

用完信号量后,要对它进行清理,即将其拥有的所有资源都清理掉。

(2)互斥量 

1.创建互斥量,第一个参数为互斥量标识符,第二个参数为互斥量的属性,通常默认为fast。但由于互斥量的属性会引发其他的问题(死锁状态),所以一般传NULL

int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr)
2.上锁

int pthread_mutex_lock(pthread_mutex_t *mutex)
3.解锁

int pthread_mutex_lock(pthread_mutex_t *mutex)
4.销毁互斥量

int pthread_mutex_destroy(pthread_mutex_t *mutex)


以上函数成功时返回0,失败则返回错误代码,并不设置errno


见下代码:

#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <semaphore.h>

#define Max 5
//加入信号量和互斥量之后,程序执行的时间变长了
int g = 1;

//sem_t sem;
pthread_mutex_t mutex;

void *fun(void *arg)
{
        int i = 0;
        for( ; i<1000; i++)
        {
	    //sem_wait(&sem);//p操作
            pthread_mutex_lock(&mutex);
            printf("g = %d\n",g++);//连续加1000次
            pthread_mutex_unlock(&mutex); 
	    //sem_post(&sem);//v操作
	}
        
}

int main()
{
        //sem_init(&sem,0,1);//初始化信号量
 	pthread_mutex_init(&mutex,NULL);
pthread_t id[Max]; int i = 0; for( ; i<Max; i++) { pthread_create(&id[i],NULL,fun,NULL); } for( i = 0; i<Max; i++) { pthread_join(id[i],NULL); } pthread_mutex_destroy(&mutex); //sem_destroy(&sem); exit(0);}

程序执行结果为:



3.线程安全函数

现在,主线程的工作是对字符串str进行分割,子线程的工作是对字符串arr进行分割,并且将两个线程每次的分割结果都打印出来。


见以下代码:

#include<assert.h>
#include<stdlib.h>
#include<string.h>
#include<pthread.h>
#include<semaphore.h>
 
void*fun(void *arg)
{
       char arr[] = "1 2 3 4 5 6 7 8 9 10";
       char *s = strtok (arr," ");
       printf("fun s = %s\n", s);
       while(( s = strtok(NULL," "))!= NULL)
       {
           printf("fun s = %s\n",s);
           sleep(1);
       }
}
 
int main()
{
       char str[] = "a b c d e f g h i j";
 
       pthread_t id;
       pthread_create(&id,NULL,fun,NULL);
char *p2 = NULL; char *s = strtok(str,"");//str是传递给strtok的将分割的字符变量 printf("mains = %s\n", s);
sleep(1); while((s= strtok(NULL," ")) != NULL) { printf("main s = %s\n", s); sleep(1); exit(0);}


程序执行结果:


     原因:我们知道,strtok函数的原型为——strtok(char *str, const char *delim)中,str是指定要进行分割的字符串,delim则是指定的分割字符。在主线程中,我们指定分割的字符串是str,本来strtok(NULL," ")让它去找指向字符串的静态指针,但是在创建子线程以后,又给strtok指定了字符串arr去分割,静态指针的位置发生改变,并且之后默认去寻找它的位置,对arr字符串按照“ ”进行分割。


#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <semaphore.h>

void *fun(void *arg)
{
        char arr[] = "1 2 3 4 5 6 7 8 9 10";

        char *p1 = NULL;
        char *s = strtok_r(arr," ",&p1);
        printf("fun s = %s\n", s);
        while(( s = strtok_r(NULL," ",&p1)) != NULL)
        {
            printf("fun s = %s\n", s);
            sleep(1);
        }
}

int main()
{
	char str[] = "a b c d e f g h i j";

        pthread_t id;

        char *p2 = NULL;
        char *s = strtok_r(str," ",&p2);//str是传递给strtok的将分割的字符变量

        pthread_create(&id,NULL,fun,NULL);
        printf("main s = %s\n", s);
	sleep(1);
        while((s = strtok_r(NULL," ",&p2)) != NULL)
        {
            printf("main s = %s\n", s);
            sleep(1);
        }


        exit(0);
}


利用strtok版本的线程安全函数,我们分别给arr和str指定了一个静态指针,这些静态指针记录这两个字符串的分割位置,互不影响,所以最后程序打印出来的结果便如下图:

程序执行结果:





 



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值