本文对多线程的常用操作用法进行简单介绍,适合给进行C/C++开发使用多线程的朋友了解。
介绍三个变量
分别是线程ID,互斥锁和条件变量,此处将其组合为一个结构体使用
typedef struct S1
{
pthread_t id;//线程的ID,或者叫线程的标识符,作为传出参数使用
pthread_mutex_t mtx;//互斥锁,用于保护内部共享资源
pthread_cond_t cond;//条件变量,结合互斥锁或阻塞自身线程使用
int num;//测试用的变量
}S1;
在使用条件变量和互斥锁前需要进行初始化,初始化的函数为:
pthread_cond_init()函数和pthread_mutex_init()函数
他们都需要传2个参数,如下举例
//第一个参数是mutex或者cond的指针,第二个是要使用变量的自定义属性,一般指定为空
pthread_mutex_init(&s1->mtx,NULL);
pthread_cond_init(&s1->cond,NULL);
在mutex和cond使用后都需要调用对应的destory()函数来销毁
pthread_mutex_destroy(&s1->mtx);
pthread_cond_destroy(&s1->cond);
创建线程的函数为pthread_create();
当线程被创建出来后就会执行,而多线程的执行任务顺序是不确定的,可能主线程先也可能子线程先
//函数需要四个参数,分别是线程ID,当线程创建成功后,会将id填入这个传入的id
//第二个是线程的属性,可以手动控制一些参数,比如线程的调度策略、栈大小等。一般指定为空
//第三个是创建的线程要执行什么函数,填入的是函数的地址,这个函数可以有返回值
//第四个是线程执行函数需要什么参数,此处传入的s1是一个上方定义的S1结构体变量地址
pthread_create(&s1->id,NULL,sub11,s1);
和pthread有关的两个操作函数
pthread_join()
//假设在线程1当中调用子线程join操作,则会等到子线程执行完毕后再继续往下执行,其中的threadID变量类型为之前定义结构体当中的thread_t,第二个传入参数是子线程工作完毕后的返回值
//因此只有当子线程执行完毕后才会输出下方的123
pthread_join(thread_id, &value_ptr)
printf("1234\n");
pthread_detach()
//对子线程detach操作,则当前的线程不会等待子线程结束,子线程直接分离由系统接管处理结果,意味着会直接输出接下来的123
pthread_detach(threadID)
printf("123\n");
如果你能够保证在main函数退出前,子线程操作的结果是无误的或者子线程操作的参数与其他线程或者main函数无关,那就可以不使用join和detach操作,子线程执行从被create的时机开始,并非从join或者detach开始。
函数操作目的是防止某条创建了子线程的线程退出时,销毁了某些数据,导致子线程操作的数据异常。
互斥锁的解锁和加锁用法,相关的三个常用函数
pthread_mutex_trylock(&s1->mtx);
trylock函数会对一个初始化好的mutex进行加锁尝试,对其尝试加锁成功会有返回值为0,尝试加锁失败返回值是非0数值
pthread_mutex_lock(&s1->mtx);
lock函数也是对一个初始化好的mutex进行加锁尝试,对其尝试加锁成功会有返回值为0,尝试加锁失败返回值是非0数值。
和trylock的区别在于,trylock加锁失败后会继续执行后续代码而非不断尝试加锁,而lock是会不断的尝试加锁。
假设A线程对mutex加锁,B线程也对mutex加锁,假设A线程先加锁成功,B线程直接阻塞,直到线程A解锁,B才可以加锁成功
//如果线程A对mutex进行lock加锁,之后A又lock加锁一次,此时就会发生死锁,无法解开,如果使用trylock则是发现无法加锁直接往下走了
pthread_mutex_unlock(&s1->mtx);
unlock函数会对mutex进行解锁尝试,解锁成功返回0,解锁失败返回非0
条件变量的相关使用
pthread_cond_wait()函数
该函数需要两个参数,一个是条件变量,一个是mutex
pthread_cond_wait(&s1->cond,&s1->mtx);
调用wait函数时,会对这一个mutex进行解锁操作,之后阻塞自身线程
举例:
A线程和B线程都要对mtx进行操作,A线程成功抢先执行
A线程对mtx进行加锁操作,那么B就阻塞在lock中,之后当A调用了wait函数,wait函数传入了cond和mtx参数
此时wait函数会先解锁mtx,条件变量会阻塞A线程,B线程加锁成功,B执行了某些操作后通过下方函数唤醒了条件变量,并进行解锁
pthread_cond_signal(&s1->cond);
此时的A被唤醒后,会对mtx进行加锁操作,加锁成功后继续执行代码。
假设线程B是先放信号再解锁,对线程A来说无非就是再等待一会,只要B进行了解锁操作,线程A就可以在wait唤醒后进行加锁,之后依旧正常运行。以下的代码可以论证
void* sub12(void* arg)
{
printf("子线程被创建\n");
S1 *s1 = (S1*)arg;
pthread_mutex_lock(&s1->mtx);
printf("子线程加锁了\n");
pthread_cond_signal(&s1->cond);
printf("子线程放信号了\n");
printf("子线程睡眠3S\n");
Sleep(3000);//假设进行了某些操作
s1->num = 100;
pthread_mutex_unlock(&s1->mtx);
printf("子线程end\n");
return NULL;
}
int main()
{
S1* s1= (S1*)malloc(sizeof (S1));
s1->num = 0;
pthread_mutex_init(&s1->mtx,NULL);
pthread_cond_init(&s1->cond,NULL);
pthread_create(&s1->id,NULL,sub12,s1);
pthread_mutex_lock(&s1->mtx);
printf("主线程加锁了\n");
printf("主线程睡眠5S\n");
Sleep(5000);//假设进行了某些操作
s1->num = 10;
printf("主线程操作后的num:%d\n",s1->num);
printf("主线程阻塞等待信号\n");
pthread_cond_wait(&s1->cond,&s1->mtx);
printf("主线程收到信号解除了阻塞\n");
printf("主线程end\n");
return 0;
}
需要注意的是,如果B先发送信号,而B不进行解锁,那A没有加锁机会,就直接造成程序死锁了
模拟工作流程
结合先前的结构体,对互斥锁,信号量的应用,这个函数不涉及join和detach操作,最后正常结束,也证明如果能保证子线程正常,可以不使用join或者detach操作
定义一个子线程调用的操作函数
void* sub11(void* arg)
{
printf("子线程被创建\n");
S1 *s1 = (S1*)arg;
printf("子线程内部打印的线程id:%d\n",s1->id);
pthread_mutex_lock(&s1->mtx);
printf("子线程加锁了\n");
printf("子线程睡眠3S\n");
Sleep(3000);//假设进行了某些操作
s1->num = 100;
printf("子线程操作后的num:%d\n",s1->num);
pthread_mutex_unlock(&s1->mtx);
printf("子线程发出信号\n");
pthread_cond_signal(&s1->cond);
return NULL;
}
主函数如下
int main()
{
S1* s1= (S1*)malloc(sizeof (S1));
s1->num = 0;
pthread_mutex_init(&s1->mtx,NULL);
pthread_cond_init(&s1->cond,NULL);
printf("创建子线程\n");
pthread_create(&s1->id,NULL,sub11,s1);
printf("主线程打印的子线程id:%d\n",s1->id);
pthread_mutex_lock(&s1->mtx);
printf("主线程加锁了\n");
printf("主线程睡眠5S\n");
Sleep(5000);//假设进行了某些操作
s1->num = 10;
printf("主线程操作后的num:%d\n",s1->num);
printf("主线程阻塞等待信号\n");
pthread_cond_wait(&s1->cond,&s1->mtx);
printf("主线程收到信号解除了阻塞\n");
//尝试加锁
int res = pthread_mutex_trylock(&s1->mtx);
printf("主线程调用了wait被唤醒后尝试加锁后的状态:%d\n",res);
pthread_mutex_unlock(&s1->mtx);
printf("主线程解锁了\n");
res = pthread_mutex_trylock(&s1->mtx);
printf("主线程解锁后尝试加锁后的状态:%d\n",res);
res = pthread_mutex_unlock(&s1->mtx);
printf("主线程解锁的状态:%d\n",res);
printf("最终的num:%d\n",s1->num);
return 0;
}
有兴趣研究的朋友可以尝试单步调试依次查看执行顺序
主函数对结构体成员初始化后,创建一条子线程,子线程的id为1,此时主线程和子线程同时进行工作
由于是主线程被先运行,因此主线程尝试了加锁操作,加锁成功继续向下执行代码,并睡眠5S模拟处理动作,之后对num进行赋值10。
在主线程加锁成功的时候,子线程也是处于工作状态,子线程尝试对同一个mutex加锁,加锁失败则会继续尝试加锁,直到成功加锁为止。
当主线程对num赋值10之后,调用了wait函数,wait函数先对mutex进行解锁,之后阻塞自身线程(在这里自身线程就是主线程),而子线程是处于对mutex的加锁状态,此时加锁成功,子线程执行相关的任务。
主线程处于阻塞的状态,当子线程执行相关任务结束后,解锁,释放了信号,通知主线程的条件变量解除阻塞,此时主线程会收到信号解除阻塞,解除阻塞后wait函数会对mutex进行加锁,由trylock的加锁失败可以验证,该mutex已经被锁过。之后往下执行再解锁,打印通过了子线程操作后的num。通过返回值得知,程序均正常结束,且没有调用join和detach操作
结语
上述是C语言常用的多线程使用方式,需要注意的问题无非是join或者detach,以及mutex的解锁问题,对于其他编程语言的多线程流程使用方式和上述类似,函数名会变动。