一、线程的基本概念
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_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);}pthread_mutex_init(&mutex,NULL);
程序执行结果为:
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;
char *p2 = NULL; char *s = strtok(str,"");//str是传递给strtok的将分割的字符变量 printf("mains = %s\n", s);pthread_create(&id,NULL,fun,NULL);
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指定了一个静态指针,这些静态指针记录这两个字符串的分割位置,互不影响,所以最后程序打印出来的结果便如下图:
程序执行结果: