You actually need a little bit more and lets cover a bit more of the mechanism as several things work together here.
First task switching will normaly happen during an interrupt or syscall, when the task is interrupted and the cpu enters kernel mode. For a purely cooperative multitasking you would have a yield() that would switch tasks. Since you probably also want a sleep() you can implement yield() as sleep(0). The next step up is to have a timer interrupt and when it fires you switch tasks. The timer can be on a fixed intervall or you can compute the time till the next task switch and programm it fresh each time. Taking this even further to true preemption you would give tasks priorities and you switch tasks whenever a higher priority task wants to do work. That means you include the time when a task will wake up from a sleep when calculating the time till the next switch. Also syscalls and interrupts can cause a task to wake up and if it has higher priority than the current task you switch.
What do all of those have in common? The interrupt and syscall handlers will save registers and other cpu state on the stack and will restore that on return. So when you change the stack then returning from an interrupt or syscall will restore the new tasks state and run the new task.
But just switching the stack is not quite correct unless you do it in the asm code for the interrupt handler right before returning. Think about what happens when the asm code for the interrupt calls other functions: Data and return addresses are store on the stack (that's fine). But also registers are changed and some of those are callee saved. If you switch the stacks somewhere down the call tree during an interrupt then you will leak the callee saved registers from on task to the next and when returning to the original task they will probably be changed. So what you have to do is push the callee saved registers to the stack, then switch stacks and finally restore the callee saved registers.
Last you have to create new tasks from time to time. And that is probably the hardest thing to implement. You have to create a stack with the right content so that when you switch to it it can run. If you are switching stacks right before returning from interrupt then you have to push an interrupt stack frame, register and cpu state onto the stack exactly as if an interrupt had happened. As return address you put the start function of the new task. And then you insert the new task into the scheduler queue and wait for it to get run. When you switch task further down the call tree then faking the stack for the whole backtrace is basically impossible. Instead it is simpler to start with an empty stack, create a stack frame with return address to a start_thread() function and put the new task into the queue (or switch to it directly). Then when start_thread() is called it will switch to user mode with interrupts enabled and call the threads start function. When/If that returns it calls sys_exit() to end the task.
Note: It's best to create a switch_task(stack **old_stack, stack *new_stack) function in asm that pushes the callee saved registers to the stack, saves the stack pointer in *old_stack and loads new_stack into the stack pointer before returning. Don't try to fight the C compiler on this with inline asm, it's not worth it. The scheduler can then call switch_task(¤t_task->stack, next_task->stack) to do the switch.