再探多线程

Posted 神佑我调参侠

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了再探多线程相关的知识,希望对你有一定的参考价值。

自己的见解

  • 其实多线程并不是说几个任务一起执行,而是将一个cpu分割成几个时间片,然后几个线程去抢这些时间片,是没有顺序的
  • 而且进程对应一个地址存储空间,而线程是多个线程对应这一个地址空间,但是对应的这个地址空间的信息,如代码,文件等都是共享的。进程包含线程。

线程函数

首先在使用这个多线程时,要先导入一个库

#include <pthread.h>

每个线程都是有自己的一个线程ID,类型为:pthread_t

创建线程

在创建线程的时候要指定一个处理函数,否则这个线程不能工作!

#include <pthread.h>
int pthread_create(pthread_t *thread, NULL ,void *(*start_routine)(void *), void *arg);
  1. 线程ID
  2. 线程属性,写NULL
  3. 回调函数
  4. 函数的实参
    看一下这个回调函数,类型是void * ,参数也是 void*。
    下面看个例子:
#include<pthread.h>
#include<string.h>
#include<iostream>

using namespace std;


void* callback(void* arg)
{
        for (int i=0;i<5;i++)
        {
                cout<<"子线程:i = "<<i<<endl;
        }
        cout<<"子线程:ID: "<<pthread_self()<<endl;

}

int main()
{
        pthread_t tid;
        pthread_create(&tid,NULL,callback,NULL);
        for(int i=0;i<5;i++)
        {
                 cout<<"主线程:i = "<<i<<endl;
        }
        cout<<"子线程:ID: "<<pthread_self()<<endl;
        return 0;
}

下面是结果:

可以看到阿,主线程是都执行完成了,但是子线程并没有都执行完毕。而是每次运行的结果都不一样,这更说明是时间片靠抢的,但是每次都是主线程抢占先机,并且抢完后就终止了。
这时可以加一个sleep函数就可以解决(ง •̀_•́)ง

线程退出

这个api其实主要是对于主线程的,因为如果是子线程退出的话,主线程并不影响,而主线程用这个函数后,会先退出,但不会干扰其他的子线程。下面说说这个函数:

void pthread_exit(void *retval);

下面我们接着上面那个函数继续书写。

int main()
{
        pthread_t tid;
        pthread_create(&tid,NULL,callback,NULL);
        cout<<"主线程:ID: "<<pthread_self()<<endl;
        pthread_exit(NULL);
        return 0;
}

如果在主线程退出后,子线程依旧运行完毕,那么我们的实验就成功了。下图说明成功了。

线程回收

作用就是:主线程回收子线程资源,但是这里面不是说全部资源,子线程的用户区的部分自动释放的,然后被其他线程接受,而内核区的部分是由主线程来回收的。

#include<pthread.h>
pthread_join(pthread_t thread,void **retval)
  1. 这个函数在一定程度上是一个堵塞函数,因为要回收子线程的资源,所以是肯定要等待子线程结束的,如果子线程没有结束,那么就会一直等待!同时这个函数只会对应一个子线程。
  2. 然后这里面还有一个细节:就是子线程运行结束的时候,其他子线程是不会接受到数据的,运行期间那就更不能传了(多线程不是一起执行),想要接收到就要做一些处理,应用pthread-exit()这个函数,这个函数如果参数是子线程的地址就是有一个返回值的,返回的是一个地址,这个函数里面对应的第二个参数就是这个地址。如果没有参数或者参数是NULL的话,那么一般是在主线程中应用
  3. 最后总结一下以上两个函数的关系即退出与堵塞,首先明白传递数据是要将这个子线程退出,然后将这个数据传出来,传出来的是地址,堵塞完毕后返回的地址,也就是说堵塞的函数的参数是二级地址。

下面看一个例子:

#include<pthread.h>
#include<string.h>
#include<iostream>

using namespace std;

struct Test
{
        int num;
        int age;
};

Test t;

void* callback(void* arg)
{
        for (int i=0;i<5;i++)
        {
                cout<<"子线程:i = "<<i<<endl;
        }
        cout<<"子线程:ID: "<<pthread_self()<<endl;
        t.num = 100;
        t.age = 6;

        pthread_exit(&t);
        return NULL;
}

int main()
{
        pthread_t tid;
        pthread_create(&tid,NULL,callback,NULL);
        cout<<"主线程:ID: "<<pthread_self()<<endl;
        //pthread_exit(NULL);

        void* ptr;
        pthread_join(tid,&ptr);
        struct Test* pt = (struct Test*)ptr;
        cout<<"num= "<<pt->num<<"age= "<<pt->age<<endl;
        return 0;
}

简单说下这个函数:从主函数看起:先定义一个线程ID,然后创建线程,然后定义一个无类型指针ptr(虽然什么都可以接收,但是用的时候要强转),这时堵塞会等待子线程结束,子线程结束会返回一个结构体的地址,然后我们这里用ptr接收,

线程分离

这个很好理解,就是互不影响,但是感觉没什么用,最后还是得加一个exit的函数!

线程同步

这个就是我们真正能用到的了,就是防止多个子线程同时访问一个数据时起作用的。比如说上洗手间,两个人同时去的洗手间,但就一个洗手间嘛,这时就得一个个来嘛。

互斥锁

这里先说一下互斥锁的技术,通俗的讲就是,这个洗手间有一把锁,那么相对应的有一把钥匙,如果一个人去了洗手间,他把锁给锁上了,钥匙也同时在他手里,那么外面的人就得等它结束后,它用钥匙打开锁,然后外面的人才能进去,然后第二个人也可以上锁,并且由他自己解锁,这让同时访问就变成了有顺序的执行了。

下面看一个例子:

#include<iostream>
#include<pthread.h>
#include<string.h>
#include<unistd.h>

using namespace std;

#define MAX 50
int number;

pthread_mutex_t mutex;

void* func1(void *)
{
        for (int i=0;i<MAX;i++){
                pthread_mutex_lock(&mutex);
                int cur = number;
                cur++;
                usleep(10);
                number = cur;
                cout<<"Thread A: ID="<<pthread_self()<<" number="<<number<<endl;
                pthread_mutex_unlock(&mutex);
        }
        return NULL;
}

void* func2(void *)
{
        for (int i=0;i<MAX;i++){
                pthread_mutex_lock(&mutex);
                int cur = number;
                cur++;
                number = cur;
                cout<<"Thread B: ID="<<pthread_self()<<" number="<<number<<endl;
                pthread_mutex_unlock(&mutex);
                usleep(5);
        }
        return NULL;
}

int main(){
        pthread_t p1,p2;
        pthread_mutex_init(&mutex,NULL);
        pthread_create(&p1,NULL,func1,NULL);
        pthread_create(&p2,NULL,func2,NULL);

        pthread_join(p1,NULL);
        pthread_join(p2,NULL);
        pthread_mutex_destroy(&mutex);
        return 0;
}


可以看到是A线程先执行完,然后在B线程在执行的,然后说一下这里面的一些函数及其用法:

  1. 首先要先定义这把锁,要在全局变量中定义
    pthread_mutex_t mutex
  2. 然后就是上锁,这里面需要注意的就是要找到那个共同应用的东西,这里的是number,然后在number的上下分别上锁和开锁pthread_mutex_lock(&mutex) XXXX pthread_mutex_unlock(&mutex)
  3. 最后就是初始化这个锁,并且在最后释放这个锁,这个初始化一定要在子线程使用的前面,并且释放要在最后面pthread_mutex_init(&mutex,NULL); pthread_mutex_destory(&mutex);

读写锁

其实读写锁本质上和互斥锁差不多的,也是一把锁,但是这把锁有两个形态而已,其实理解了上面那个,这个也应该很好理解,我们直接上案例:

#include<iostream>
#include<pthread.h>
#include<string.h>
#include<unistd.h>
#include<stdlib.h>

using namespace std;

#define MAX 50
int number;

pthread_rwlock_t rwlock;

void* func1(void *)
{
        for (int i=0;i<MAX;i++){
                pthread_rwlock_rdlock(&rwlock);
                cout<<"Thread read: ID="<<pthread_self()<<" number="<<number<<endl;
                pthread_rwlock_unlock(&rwlock);
                usleep(rand()%5);
        }
        return NULL;
}

void* func2(void *)
{
        for (int i=0;i<MAX;i++){
                pthread_rwlock_wrlock(&rwlock);
                int cur = number;
                cur++;
                number = cur;
                cout<<"Thread B: ID="<<pthread_self()<<" number="<<number<<endl;
                pthread_rwlock_unlock(&rwlock);
        }
        return NULL;
}

int main(){
        pthread_t p1[5],p2[3];
        pthread_rwlock_init(&rwlock,NULL);
        for (int i=0;i<5;i++){
                pthread_create(&p1[i],NULL,func1,NULL);
        }
        for (int i=0;i<3;i++){
                pthread_create(&p2[i],NULL,func2,NULL);
        }
        
        for (int i=0;i<5;i++){
                pthread_join(p1[i],NULL);
        }
        for (int i=0;i<3;i++){
                pthread_join(p2[i],NULL);
        }
        
        pthread_rwlock_destroy(&rwlock);
        return 0;
}

条件变量(生产者,消费者模型)

条件变量与互斥锁联合使用

#include<iostream>
#include<pthread.h>
#include<string.h>
#include<unistd.h>
#include<stdlib.h>

using namespace std;

pthread_cond_t cond;
pthread_mutex_t mutex;

struct Node
{
        int number;
        struct Node* next;
};

Node* head = NULL;

void* producer(void *)
{
        while(1){
                pthread_mutex_lock(&mutex);
                Node* new_node = (struct  Node*) malloc(sizeof(struct Node));
                new_node->number = rand()%1000;
                new_node->next = head;
                head = new_node;
                cout<<"生产者id:"<<pthread_self()<<"number: "<<new_node->number<<endl;;
                pthread_mutex_unlock(&mutex);
                pthread_cond_broadcast(&cond);
                sleep(rand()%3);
        }
        return NULL;
}

void* consumer(void *)
{
        while(1)
        {
                pthread_mutex_lock(&mutex);
                while(head == NULL){
                        pthread_cond_wait(&cond,&mutex);
                }
                struct Node* node = head;
                cout<<"消费者id:"<<pthread_self()<<"number: "<<node->number<<endl;
                head = head->next;
                free(node);
                pthread_mutex_unlock(&mutex);
                sleep(rand()%3);
        }
        return NULL;
}

int main(){
        pthread_mutex_init(&mutex,NULL);
        pthread_cond_init(&cond,NULL);
        
        pthread_t t1[5],t2[5];
        for(int i=0;i<5;i++){
                pthread_create(&t1[i],NULL,producer,NULL);
        }
        for(int i=0;i<5;i++){
                pthread_create(&t2[i],NULL,consumer,NULL);
        }

        for(int i=0;i<5;i++){
                pthread_join(t1[i],NULL);
        }
        for(int i=0;i<5;i++){
                pthread_join(t2[i],NULL);
        }
        pthread_mutex_destroy(&mutex);
        pthread_cond_destroy(&cond);
        return 0;
}

这个例子非常好,把这个搞懂了的话就可以应用了,这里面应用到了一个核心的东西就是生产消费者模型,然后这也是我首次接触到数据结构的应用,说实在的大现在我还是不太懂链表的应用,其实我概念还是很理解的,但是如何应用就是另一件事了,而像一些条件变量的应用我说实在的并不难的,因为这个和我当时的想法就差不多,但是没有加数据结构嘛,这里先说一下链表操作的那个地方:

先创建一个节点的结构体,然后定义一个头的地址为NULL;然后使用时先开辟一个节点的空间,然后写入数据,这里注意一下,有个操作是交换地址,这个操作可能是最难理解的,先是该节点链接的节点地址指向链表头的地址,然后将该节点的地址给到head,之后在新建一个节点~~~~

后面的内容等我用到在更新,现在学得足够我看懂相机文件的代码了。

以上是关于再探多线程的主要内容,如果未能解决你的问题,请参考以下文章

iOS开发之再探多线程编程:Grand Central Dispatch详解

再探 同步与互斥

再探 同步与互斥

再探Javascript事件循环及其与浏览器渲染的关系

PHP代码审计之再探 TP3 漏洞-上

PHP代码审计之再探 TP3 漏洞